--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/_utmp.c Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,57 @@
+#include <Python.h>
+
+#include <utmp.h>
+
+#include <stdio.h>
+
+PyObject *parse (PyObject *self, PyObject *args) {
+ char *bytes;
+ size_t size;
+
+ struct utmp *item;
+
+ /* not unicode */
+ if (!PyArg_ParseTuple(args, "t#", &bytes, &size))
+ return NULL;
+
+ if (size != sizeof(struct utmp)) {
+ PyErr_SetString(PyExc_ValueError, "given buffer is of the wrong length");
+ return NULL;
+ }
+
+ item = (struct utmp *) bytes;
+
+ /* parse utmp from bytes to result */
+ return Py_BuildValue("hIs#s#s#s#(hh)i(ii)s#",
+ item->ut_type, item->ut_pid,
+ item->ut_line, sizeof(item->ut_line),
+ item->ut_id, sizeof(item->ut_id),
+ item->ut_user, sizeof(item->ut_user),
+ item->ut_host, sizeof(item->ut_host),
+ item->ut_exit.e_termination, item->ut_exit.e_exit,
+ item->ut_session,
+ item->ut_tv.tv_sec, item->ut_tv.tv_usec,
+ item->ut_addr_v6, sizeof(item->ut_addr_v6)
+ );
+}
+
+PyObject *size (PyObject *self, PyObject *args) {
+ /* return the size of an UTMP struct */
+
+ if (!PyArg_ParseTuple(args, ""))
+ return NULL;
+
+ return PyInt_FromSsize_t(sizeof(struct utmp));
+}
+
+static PyMethodDef module_methods[] = {
+ {"parse", parse, METH_VARARGS, "parse a utmp struct from a byte string"},
+ {"size", size, METH_VARARGS, "return the size of an utmp record in bytes"},
+ {NULL}
+};
+
+PyMODINIT_FUNC init_utmp(void) {
+ PyObject *m;
+
+ m = Py_InitModule3("_utmp", module_methods, "utmp struct parsing");
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/api.py Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,212 @@
+from twisted.internet import protocol, reactor
+from twisted.python import log
+from datetime import datetime
+import sys
+
+import buffer
+
+from api_secret import secret
+
+class ModuleInfo (object) :
+ """
+ Some info about a module
+ """
+
+ # module's name
+ name = None
+
+ # module's version, as a 16-bit integer
+ version = None
+
+ # list of valid event types (strings)
+ event_types = None
+
+ def __str__ (self) :
+ return "Module %s:%d" % (self.name, self.version)
+
+ def __repr__ (self) :
+ return "<module %s:%d with events: %s>" % (self.name, self.version, ", ".join(self.event_types))
+
+class Event (object) :
+ # the ModuleInfo object
+ module = None
+
+ # the event type as a string
+ type = None
+
+ # event message as a string (under 255 bytes in length!)
+ msg = None
+
+ # timestamp as a datetime.datetime
+ when = None
+
+ def __init__ (self, module, type, msg) :
+ assert type in module.event_types, "Invalid event-type %s for %r" % (type, self.module)
+
+ self.module = module
+ self.type = type
+ self.msg = msg
+
+ self.when = datetime.now()
+
+ def __str__ (self) :
+ return "[%s] %s" % (self.type, self.msg)
+
+ def __repr__ (self) :
+ return "%s @ %s" % (self.type, self.when)
+
+CLIENT_COMMANDS = [
+ "module_init",
+ "module_event",
+]
+
+SERVER_COMMANDS = [
+ "module_ok",
+]
+
+class ServerProtocol (buffer.StreamProtocol, protocol.Protocol) :
+ RECV_COMMANDS = CLIENT_COMMANDS
+ SEND_COMMANDS = SERVER_COMMANDS
+
+ VALID_STATES = [
+ "wait_init",
+ "wait_event"
+ ]
+
+ # proto state
+ state = None
+
+ # module info
+ module = None
+
+ def _assert (self, condition, msg) :
+ if not condition :
+ self.transport.loseConnection()
+ log.err("assert failed in APIProtocol for %s: %s" % (self.module, msg))
+
+ def connectionMade (self) :
+ log.msg("Client connected")
+
+ def connectionLost (self, reason) :
+ log.msg("Connection lost: %s" % reason)
+
+ if self.module :
+ self.factory.nexus.unregisterModule(self.module, reason.getErrorMessage())
+
+ def on_module_init (self, i) :
+ self._assert(not self.module, "module_init with non-None self.module")
+
+ peer_secret = i.readVarLen('B')
+
+ self._assert(peer_secret == secret, "Mismatching API secrets!")
+
+ m = ModuleInfo()
+
+ m.name = i.readVarLen('B')
+ m.version = i.readItem('H')
+
+ m.event_types = list(buffer.readStringStream(i, 'B'))
+ m.addr = self.transport.getPeer()
+
+ self.module_name = m.name
+
+ log.msg("Got mod_init for %r" % m)
+
+ self.factory.nexus.registerModule(m, self)
+
+ self.module = m
+
+ o = self.startCommand('module_ok')
+
+ self.send(o)
+
+ def on_module_event (self, i) :
+ self._assert(self.module, "module_event with None self.module!")
+
+ event_type = i.readEnum(self.module.event_types)
+ event_msg = i.readVarLen('B')
+
+ e = Event(self.module, event_type, event_msg)
+
+# log.msg("Got mod_event of %r" % (e))
+
+ self.factory.nexus.handleEvent(e)
+
+ def logPrefix (self) :
+ if self.module :
+ return str(self.module)
+ else :
+ return super(ServerProtocol, self).logPrefix()
+
+class ClientProtocol (buffer.StreamProtocol, protocol.Protocol) :
+ RECV_COMMANDS = SERVER_COMMANDS
+ SEND_COMMANDS = CLIENT_COMMANDS
+
+ def connectionMade (self) :
+ log.msg("Connected to API server, sending module init message")
+
+ o = self.startCommand('module_init')
+ o.writeVarLen('B', secret)
+ o.writeVarLen('B', self.factory.name)
+ o.writeItem("H", self.factory.version)
+ buffer.writeStringStream(o, 'B', self.factory.event_types)
+
+ self.send(o)
+
+ def sendEvent (self, event) :
+ o = self.startCommand('module_event')
+ o.writeEnum(self.factory.event_types, event.type)
+ o.writeVarLen('B', event.msg)
+
+ self.send(o)
+
+ def on_module_ok (self, i) :
+ log.msg("Registration OK")
+
+ self.factory.connected(self)
+
+ def logPrefix (self) :
+ return "module %s:%d client" % (self.factory.name, self.factory.version)
+
+class Module (ModuleInfo, protocol.ClientFactory) :
+ protocol = ClientProtocol
+
+ def __init__ (self) :
+ log.msg("Connecting to %s:%d" % (SERVER_HOST, PORT))
+ reactor.connectTCP(SERVER_HOST, PORT, self)
+
+ self.connection = None
+
+ def run (self) :
+ log.startLogging(sys.stderr)
+
+ reactor.run()
+
+ def connected (self, connection) :
+ log.msg("Connected!")
+ self.connection = connection
+
+ self.handleConnect()
+
+ def disconnect (self) :
+ self.connection.transport.loseConnection()
+
+ def sendEvent (self, type, msg) :
+ self.connection.sendEvent(self.buildEvent(type, msg))
+
+ def buildEvent (self, type, msg) :
+ return Event(self, type, msg)
+
+ def handleConnect (self) :
+ """
+ Do something
+ """
+
+ pass
+
+class ServerFactory (protocol.ServerFactory) :
+ protocol = ServerProtocol
+
+ def __init__ (self, nexus) :
+ self.nexus = nexus
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/buffer.py Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,417 @@
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+import struct
+
+# prefixed to all struct format strings
+STRUCT_PREFIX = '!'
+
+def hex (bytes) :
+ return ' '.join(['%#04x' % ord(b) for b in bytes])
+
+class NotEnoughDataError (Exception) :
+ pass
+
+class IStreamBase (object) :
+ # prefixed to all struct format strings
+ STRUCT_PREFIX = '!'
+
+class IReadStream (IStreamBase) :
+ """
+ IReadStream simply provides various interfaces for reading bytes from a
+ stream in various ways
+ """
+
+ def read (self, size=None) :
+ """
+ Read and return up to the given amount of bytes, or all bytes
+ available if no size given.
+ """
+
+ abstract
+
+ def readStruct (self, fmt) :
+ """
+ Reads the correct amount of data and then unpacks it according to
+ the given format. Note that this always returns a tuple, for single
+ items, use readItem
+ """
+
+ fmt = self.STRUCT_PREFIX + fmt
+
+ fmt_size = struct.calcsize(fmt)
+ data = self.read(fmt_size)
+
+ return struct.unpack(fmt, data)
+
+ def readItem (self, fmt) :
+ """
+ Reads the correct amount of data, unpacks it according to the
+ given format, and then returns the first item.
+ """
+
+ return self.readStruct(fmt)[0]
+
+ def readVarLen (self, len_type) :
+ """
+ Return the data part of a <length><data> structure.
+ len_type indicates what type length has (struct format code).
+
+ In the case of <length> being zero, returns an empty string.
+ """
+
+ size = self.readItem(len_type)
+
+ if size :
+ return self.read(size)
+ else :
+ return ""
+
+ def readEnum (self, enum) :
+ """
+ Returns the item from the given list of enum values that corresponds
+ to a single-byte value read from the stream
+ """
+
+ return enum[self.readItem('B')]
+
+class ISeekableStream (IStreamBase) :
+ """
+ Extends IStreamBase to provide the ability to seek backwards into the
+ stream (which still does not know it's length, and thence cannot seek
+ forwards).
+ """
+
+ _position = None
+
+ def tell (self) :
+ """
+ Return the current offset into the seekable stream
+ """
+
+ abstract
+
+ def seek (self, pos) :
+ """
+ Seek to the given position in the stream.
+ """
+
+ abstract
+
+ def mark (self) :
+ """
+ Set a mark that can be later rolled back to with .reset()
+ """
+
+ self._position = self.tell()
+
+ def unmark (self) :
+ """
+ Remove the mark without affecting the current position
+ """
+
+ self._position = None
+
+ def reset (self) :
+ """
+ Rolls the buffer back to the position set earlier with mark()
+ """
+
+ if self._position is not None :
+ self.seek(self._position)
+ self._position = None
+
+ else :
+ raise Exception("reset() without mark()")
+
+class ISeekableReadStream (ISeekableStream, IReadStream) :
+ def peek (self, len=None) :
+ """
+ Return a string representing what buf.read(len) would return, but do
+ not affect future read operations
+ """
+
+ pos = self.tell()
+
+ data = self.read(len)
+
+ self.seek(pos)
+
+ return data
+
+
+class INonBlockingReadStream (IReadStream) :
+ """
+ Otherwise identical to IReadStream, but read will either return size
+ bytes, or raise a NotEnoughDataError
+ """
+
+ pass
+
+class IWriteStream (IStreamBase) :
+ """
+ IWriteStream provides various ways to write data to a byte stream
+ """
+
+ def write (self, data) :
+ """
+ Write the given bytes to the stream
+ """
+
+ abstract
+
+ def writeStruct (self, fmt, *args) :
+ """
+ Pack the given arguments with the given struct format, and write it
+ to the stream.
+ """
+
+ self.write(struct.pack(self.STRUCT_PREFIX + fmt, *args))
+
+ writeItem = writeStruct
+
+ def writeVarLen (self, len_type, data) :
+ """
+ Write a <length><data> field into the buffer. Len_type is the
+ struct format code for the length field.
+ """
+
+ self.writeStruct(len_type, len(data))
+ self.write(data)
+
+ def writeEnum (self, enum, name) :
+ """
+ Write the single-byte value correspnding to the given name's
+ position in the given enum
+ """
+
+ self.writeStruct('B', enum.index(name))
+
+class IBufferBase (ISeekableStream) :
+ """
+ A buffer simply provides a way to read and write data to/from a byte
+ sequence stored in memory.
+ """
+
+ def tell (self) :
+ return self._buf.tell()
+
+ def seek (self, offset) :
+ return self._buf.seek(offset)
+
+ def getvalue (self) :
+ """
+ Returns the value of the buffer, i.e. a string with the contents of
+ the buffer from position zero to the end.
+ """
+
+ return self._buf.getvalue()
+
+class ReadBuffer (INonBlockingReadStream, ISeekableReadStream, IBufferBase) :
+ """
+ A read-only buffer. Can be initialized with a given value and then later
+ replaced in various ways, but cannot be modified.
+ """
+
+ def __init__ (self, data="") :
+ """
+ Initialize the buffer with the given data
+ """
+
+ self._buf = StringIO(data)
+
+ def read (self, size=None) :
+ """
+ Return the given number of bytes, or raise a NotEnoughDataError
+ """
+
+ if size == 0 :
+ raise ValueError("can't read zero bytes")
+
+ if size :
+ ret = self._buf.read(size)
+ else :
+ ret = self._buf.read()
+
+ if size and len(ret) < size :
+ raise NotEnoughDataError()
+
+ return ret
+
+ def append (self, data) :
+ """
+ Modify the buffer such that it contains the old data from this
+ buffer, and the given data at the end. The read position in the buffer
+ is kept the same.
+ """
+
+ pos = self.tell()
+
+ self._buf = StringIO(self._buf.getvalue() + data)
+
+ self.seek(pos)
+
+ def chop (self) :
+ """
+ Discard the data in the buffer before the current read position.
+ Also removes any marks.
+ """
+
+ self._position = None
+
+ self._buf = StringIO(self.read())
+
+ def processWith (self, func) :
+ """
+ Call the given function with this buffer as an argument after
+ calling mark(). If the function
+ a) returns None, the buffer is .chop()'d, and we repeat the
+ process.
+ b) raises a NotEnoughDataError, whereupon the buffer is rolled
+ back to where it was before calling the function with
+ chop().
+ c) raises a StopIteration, whereupon we chop the buffer and
+ return.
+ d) returns something (i.e. ret is not None), whereupon we
+ return that (and leave the current buffer position untouched).
+ """
+ ret = None
+
+ try :
+ while ret is None :
+ self.mark() # mark the position of the packet we are processing
+ ret = func(self)
+
+ if ret is None :
+ # discard the processed packet and proceed to the next one
+ self.chop()
+
+ except NotEnoughDataError, e :
+ self.reset() # reset position back to the start of the packet
+ return e
+
+ except StopIteration, e:
+ self.chop()
+ return e # processed ok, but we don't want to process any further packets
+
+ else :
+ return ret
+
+class WriteBuffer (IWriteStream, IBufferBase) :
+ """
+ A write-only buffer. Data can be written to this buffer in various
+ ways, but cannot be read from it except as a whole.
+ """
+
+ def __init__ (self) :
+ """
+ Initialize the buffer
+ """
+
+ self._buf = StringIO()
+
+ def write (self, data) :
+ """
+ Write the given data to the current position in the stream,
+ overwriting any previous data in that position, and extending
+ the buffer if needed
+ """
+
+ return self._buf.write(data)
+
+def readStringStream (stream, varlen_type) :
+ """
+ Does readVarLen on an IReadStream until it returns something that evaluates to false ( == zero-length string)
+ """
+
+ while True :
+ item = stream.readVarLen(varlen_type)
+
+ if item :
+ yield item
+ else :
+ return
+
+def writeStringStream (stream, varlen_type, strings) :
+ """
+ Writes strings from the given iterable into the given stream using the given varlen_type, ending with a null-length token
+ """
+
+ for item in strings :
+ stream.writeVarLen(varlen_type, item)
+
+ stream.writeItem(varlen_type, 0)
+
+class StreamProtocol (object) :
+ """
+ A mixin to let you use Buffer with twisted.internet.protocol.Protocol
+ """
+
+ # a list of receivable command names
+ RECV_COMMANDS = None
+
+ # a list of sendable command names
+ SEND_COMMANDS = None
+
+ def __init__ (self) :
+ """
+ Initialize the cross-dataReceived buffer
+ """
+
+ self.in_buffer = ReadBuffer()
+
+ def send (self, buf) :
+ """
+ Write the contents of the given WriteBuffer to the transport
+ """
+
+ self.transport.write(buf.getvalue())
+
+ def dataReceived (self, data) :
+ """
+ Buffer the incoming data and then try and process it
+ """
+
+ self.in_buffer.append(data)
+
+ ret = self.in_buffer.processWith(self.processPacket)
+
+ def processPacket (self, buf) :
+ """
+ Call processCommand with the buffer, handling the return value (either None or a deferred)
+ """
+
+ ret = self.processCommand(buf)
+
+ if ret :
+ ret.addCallback(self.send)
+
+ def processCommand (self, buf) :
+ """
+ Process a command from the given buffer. May return a callback
+ """
+
+ return self.readMethod(buf, self.RECV_COMMANDS, buf)
+
+ # conveniance read/write
+ def startCommand (self, cmd) :
+ buf = WriteBuffer()
+
+ buf.writeEnum(self.SEND_COMMANDS, cmd)
+
+ return buf
+
+ def readMethod (self, buf, methods, *args, **kwargs) :
+ """
+ Reads a single-byte <methods>-enum value from the given buffer and
+ use it to find the corresponding method (as <prefix>_<method-name>,
+ prefix can be overriden with a keyword argument and defaults to
+ 'on'. If any extra arguments are given, they will be passed to the
+ method.
+ """
+
+ prefix = kwargs.pop("prefix", "on")
+
+ return getattr(self, "%s_%s" % (prefix, buf.readEnum(methods)))(*args, **kwargs)
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/example.py Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,24 @@
+from twisted.internet import protocol, reactor
+from twisted.python import log
+import sys
+
+import api
+
+class ExampleModule (api.Module) :
+ name = "example"
+ version = 0x0001
+
+ event_types = [
+ "example"
+ ]
+
+ def handleConnect (self) :
+ self.sendEvent("example", "this is an example event")
+ self.disconnect()
+
+if __name__ == '__main__' :
+ log.startLogging(sys.stderr)
+
+ module = ExampleModule()
+ reactor.run()
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/fifo.py Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,100 @@
+# read a stream from a fifo
+
+from twisted.internet import reactor, interfaces
+from twisted.python import log
+from zope.interface import implements
+
+import os, fcntl, errno
+
+class EOF (Exception) : pass
+
+BUF_SIZE = 2048
+
+class Fifo (object) :
+ implements(interfaces.IReadDescriptor)
+
+ def __init__ (self, path) :
+ self.path = path
+ self._open()
+
+ def _open (self) :
+ self.fd = os.open(self.path, os.O_RDONLY | os.O_NONBLOCK)
+
+ reactor.addReader(self)
+
+ log.msg("opened fifo %s as %d" % (self.path, self.fd))
+
+ def _close (self) :
+ if self.fd :
+ reactor.removeReader(self)
+ os.close(self.fd)
+
+ log.msg("closed fifo %d at %s" % (self.fd, self.path))
+
+ self.fd = None
+ close = _close
+
+ def reopen (self) :
+ """
+ Close and re-open the fifo. This is useful for handling EOF
+ """
+ self._close()
+ self._open()
+
+ def _read (self, length) :
+
+ log.msg("(read %d bytes from %d:%s)" % (length, self.fd, self.path))
+ try :
+ data = os.read(self.fd, length)
+
+ except OSError, e :
+ if e.errno == errno.EAGAIN :
+ log.msg("\tEAGAIN")
+ return None
+ else :
+ log.msg("\tERROR: %s" % e)
+ raise
+
+ if not data :
+ log.msg("\tEOF")
+ raise EOF()
+
+ log.msg("\tDATA: %d: %r" % (len(data), data))
+ return data
+
+ def fileno (self) :
+ return self.fd
+
+ def doRead (self) :
+ while True :
+ log.msg("fifo doRead loop")
+
+ try :
+ data = self._read(BUF_SIZE)
+ except EOF :
+ self.handleEOF()
+ return
+
+ if data :
+ self.dataReceived(data)
+ else :
+ break
+
+ def dataReceived (self, data) :
+ pass
+
+ def handleEOF (self) :
+ pass
+
+ def connectionLost (self, reason) :
+ self.close()
+
+ def logPrefix (self) :
+ return "FIFO:%d:%s" % (self.fd, self.path)
+
+ def __del__ (self) :
+ """
+ !!! this is important
+ """
+ self.close()
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/irc.py Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,156 @@
+from twisted.words.protocols import irc
+from twisted.internet import protocol
+from twisted.python import log
+import traceback
+
+import buffer
+
+class ReplyException (Exception) :
+ def __init__ (self, reply) :
+ self.reply = reply
+
+class Client (irc.IRCClient, object) :
+ """
+ Fixme IRC bot
+ """
+
+ def __init__ (self) :
+ # can't config yet
+ pass
+
+ # housekeeping
+ def connectionMade (self) :
+ self.nexus = self.factory.nexus
+
+ self.nickname = self.factory.nickname
+ self.username = self.factory.username
+ self.channel = self.factory.channel
+
+ log.msg("IRC Client connected, using username=%s, nickname=%s" % (self.username, self.nickname))
+
+ super(Client, self).connectionMade()
+
+ def connectionLost (self, reason) :
+ log.msg("Connection lost: %s" % reason)
+ super(Client, self).connectionLost(reason)
+
+ def signedOn (self) :
+ log.msg("Signed on, joining channel %s" % self.channel)
+ self.join(self.channel)
+
+ def joined (self, channel) :
+ log.msg("Joined channel %s" % channel)
+
+ self.factory.connected(self)
+
+ # our actual functionality
+ def send (self, msg) :
+ msg = str(msg)
+
+ if len(msg) > 480 :
+ log.msg("truncating: %s" % msg)
+ msg = msg[:480] + "..."
+
+ msg = msg.replace("\n", "\\n").replace("\r", "\\r").replace("\0", "\\0")
+
+ self.notice(CHANNEL, msg)
+
+ def sendEvent (self, event) :
+ self.send("[%s.%s] %s" % (event.module.name, event.type, event.msg))
+
+ def moduleConnected (self, module, addr) :
+ self.send("{modules.%s} connected from %s:%d, version %s" % (module.name, addr.host, addr.port, module.version))
+
+ def moduleDisconnected (self, module, reason) :
+ self.send("{modules.%s} disconnected: %s" % (module.name, reason))
+
+ class _noDefault : pass
+
+ def _lookupCommand (self, command, default=_noDefault) :
+ if '.' in command :
+ raise ReplyException("No support for module commands yet :P")
+ else :
+ method = getattr(self, "cmd_%s" % command, None)
+
+ if method :
+ return method
+ elif default is self._noDefault :
+ raise ReplyException("No such command '%s'. See `help commands'" % command)
+ else :
+ return default
+
+ def privmsg (self, user, channel, message) :
+ if message.lower().startswith(self.nickname.lower() + ':'):
+ me, command = message.split(":", 1)
+
+ args = command.strip().split()
+ command = args.pop(0)
+
+ try :
+ method = self._lookupCommand(command)
+
+ reply = method(*args)
+
+ if reply :
+ self.send(reply)
+
+ except ReplyException, e :
+ self.send(e.reply)
+
+ except Exception, e :
+ self.send("Error: %s: %s" % (e.__class__.__name__, e))
+ traceback.print_exc()
+
+ def cmd_help (self, cmd="help") :
+ """help <command|module> - Display help about the given command or module"""
+
+ method = self._lookupCommand(cmd, None)
+
+ if method :
+ return method.__doc__
+ else :
+ try :
+ module, addr = self.nexus.getModuleInfo(cmd)
+
+ return "%s is version %d from %s:%d. Events: %s. See `commands %s' for a list of commands" % (module.name, module.version, addr.host, addr.port, ', '.join(module.event_types), module.name)
+
+ except KeyError :
+ raise ReplyException("No command/module called `%s'. See `help commands'" % cmd)
+
+ def cmd_commands (self, module=None) :
+ """commands [<module>] - Show primary commands, or commands in the given module (see `help modules')"""
+
+ if module :
+ raise ReplyException("No support for module commands yet :P")
+ else :
+ return "Commands: %s" % ', '.join(
+ attr_name.split('_', 1)[1]
+ for attr_name in Client.__dict__.iterkeys()
+ if attr_name.startswith("cmd_")
+ )
+
+ def cmd_modules (self) :
+ """modules - Show a list of connected modules"""
+
+ return "Modules: %s" % ', '.join(
+ module.name
+ for module in self.nexus.getModules()
+ )
+
+class Factory (protocol.ClientFactory) :
+ protocol = Client
+
+ def __init__ (self, nexus, nickname, username, channel) :
+ self.nexus = nexus
+
+ # config
+ self.nickname = nickname
+ self.username = username
+ self.channel = channel
+
+ # don't have a connection yet
+ self.connection = None
+
+ def connected (self, connection) :
+ self.connection = connection
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/logwatch_filters.py Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,66 @@
+import re
+
+class FullFilter (object) :
+ def __init__ (self, event_type) :
+ self.event_type = event_type
+
+ def test (self, line) :
+ return line
+
+class NullFilter (object) :
+ def __init__ (self, pattern, flags=None) :
+ self.regexp = re.compile(pattern, flags)
+
+ def test (self, line) :
+ match = self.regexp.search(line)
+
+ if match :
+ return False
+
+class SimpleFilter (object) :
+ def __init__ (self, event_type, pattern, format) :
+ self.event_type = event_type
+
+ self.regexp = re.compile(pattern)
+ self.format = format
+
+ def test (self, line) :
+ match = self.regexp.search(line)
+
+ if match :
+ return self._filter(match)
+
+ def _filter (self, match) :
+ return self.format % match.groupdict()
+
+_timestamp = "\w{3} [0-9 ]\d \d{2}:\d{2}:\d{2}"
+
+all = FullFilter("all")
+
+all_wo_timestamps = SimpleFilter(
+ "all",
+ "^" + _timestamp + " (?P<line>.+)$",
+ "%(line)s"
+)
+
+sudo = SimpleFilter(
+ "sudo",
+ "(?P<hostname>\S+)\s+sudo:\s*(?P<username>\S+) : TTY=(?P<tty>\S+) ; PWD=(?P<pwd>.+?) ; USER=(?P<target_user>\S+) ; COMMAND=(?P<command>.*)",
+ "%(username)s:%(tty)s - %(target_user)s@%(hostname)s:%(pwd)s - %(command)r"
+)
+
+ssh = SimpleFilter(
+ "ssh",
+ "(?P<success>Accepted|Failed) password for (?P<username>\S+) from (?P<ip>\S+) port (?P<port>\S+) (?P<proto>\S+)",
+ "%(success)s login for %(username)s from %(ip)s:%(port)s proto %(proto)s"
+)
+
+cron_killer = NullFilter(
+ "^" + _timestamp + " \S+\s+(CRON|su)\[\d+\]: \(\w+\) session (opened|closed) for user \w+( by \(uid=\d+\))?$",
+ re.IGNORECASE
+)
+
+su_nobody_killer = NullFilter(
+ "^" + _timestamp + " \S+\s+su\[\d+\]: (Successful su for nobody by root|\+ \?\?\? root:nobody)$",
+ re.IGNORECASE
+)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/logwatch_sources.py Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,91 @@
+from twisted.internet import reactor, protocol
+from twisted.python import log
+
+import fifo
+
+class LogSource (object) :
+ def __init__ (self, name, filters) :
+ # set later on
+ self.module = None
+
+ # what filters to apply
+ self.filters = filters
+
+ # name, for display purposes
+ self.name = name
+
+ # used to gather data together into lines
+ self.buf = ""
+
+ def setModule (self, module) :
+ self.module = module
+
+ def handleError (self, msg) :
+ log.err(msg)
+ self.module.error(msg)
+
+ def handleData (self, data) :
+ data = self.buf + data
+
+ while "\n" in data :
+ line, data = data.split("\n", 1)
+
+ self.handleLine(line)
+
+ self.buf = data
+
+ def handleLine (self, line) :
+ log.msg("Matching line `%s'..." % line)
+
+ for filter in self.filters :
+ out = filter.test(line)
+
+ if out :
+ log.msg("\t%s: %s" % (filter.event_type, out))
+ self.module.sendEvent(filter.event_type, out)
+
+ break
+ elif out is False :
+ return
+ else : # None
+ continue
+
+class File (LogSource, protocol.ProcessProtocol) :
+ def __init__ (self, name, path, filters) :
+ super(File, self).__init__(name, filters)
+
+ self.path = path
+
+ log.msg("spawning tail process for %s:%s" % (name, path))
+
+ reactor.spawnProcess(self, "/usr/bin/tail", ["tail", "-n0", "--follow=name", path])
+
+ def errReceived (self, data) :
+ self.handleError("tail for %s: %s" % (self.name, data))
+
+ def outReceived (self, data) :
+ self.handleData(data)
+
+ def processEnded (self, reason) :
+ self.handleError("tail process for %s quit: %s" % (self.name, reason.getErrorMessage()))
+
+class Fifo (LogSource, fifo.Fifo) :
+ def __init__ (self, name, path, filters) :
+ LogSource.__init__(self, name, filters)
+
+ log.msg("opening fifo for %s:%s" % (name, path))
+
+ fifo.Fifo.__init__(self, path)
+
+ def dataReceived (self, data) :
+ self.handleData(data)
+
+ def handleEOF (self) :
+ self.handleError("!!! EOF on fifo %s, re-opening" % self.name)
+ self.reopen()
+
+ def connectionLost (self, reason) :
+ super(Fifo, self).connectionLost(reason)
+ self.handleError("lost fifo for %s: %s" % (self.name, reason.getErrorMessage()))
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/logwatcher.py Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,28 @@
+from twisted.internet import protocol, reactor
+from twisted.python import log
+import sys
+
+import api
+import logwatch_config as config
+
+class LogWatchModule (api.Module) :
+ name = "logs"
+ version = 0x0005
+
+ event_types = [
+ "error",
+ "sudo",
+ "ssh",
+ "all"
+ ]
+
+ def handleConnect (self) :
+ for source in config.sources() :
+ source.setModule(self)
+
+ def error (self, msg) :
+ self.sendEvent("error", msg)
+
+if __name__ == '__main__' :
+ LogWatchModule().run()
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/nexus.py Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,70 @@
+from twisted.application import internet, service
+from twisted.internet import reactor, protocol
+from twisted.python import log
+import sys
+
+import irc, api
+
+class Nexus (object) :
+ def __init__ (self) :
+ """
+ Must set .irc/.api attrs to irc.Factory/api.ServerFactory instances
+ """
+
+ self.modules = dict()
+
+
+ def registerModule (self, module, transport) :
+ self.modules[module.name] = (module, transport)
+
+ self.irc.connection.moduleConnected(module, transport.transport.getPeer())
+
+ def unregisterModule (self, module, reason) :
+ del self.modules[module.name]
+
+ self.irc.connection.moduleDisconnected(module, reason)
+
+ def handleEvent (self, event) :
+ self.irc.connection.sendEvent(event)
+
+ def getModules (self) :
+ return (module for (module, transport) in self.modules.itervalues())
+
+ def getModuleInfo (self, name) :
+ module, connection = self.modules[name]
+
+ return module, connection.transport.getPeer()
+
+def makeService (config) :
+ n = Nexus()
+ s = service.MultiService()
+
+ # the IRC side
+ n.irc = irc.Factory(n,
+ config['irc-nickname'],
+ config['irc-username'],
+ config['irc-channel']
+ )
+
+ log.msg("Connecting to IRC server on [%s:%d]", config['irc-hostname'], config['irc-port'])
+ irc_client = internet.TCPClient(config['irc-hostname'], config['irc-port'], n.irc)
+
+ irc_client.setServiceParent(s)
+
+ # the API side
+ n.api = api.ServerFactory(n)
+
+ log.msg("Starting API server on [%s:%d]", config['api-port'], config['api-listen'])
+ api_server = internet.TCPServer(config['api-port'], n.api, interface=config['api-listen'])
+
+ api_server.setServiceParent(s)
+
+ # return the service collection
+ return s
+
+if __name__ == '__main__' :
+ log.startLogging(sys.stderr)
+
+ nexus = Nexus()
+ reactor.run()
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/setup.py Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,10 @@
+from distutils.core import setup, Extension
+
+_utmp = Extension('_utmp', sources=['_utmp.c'])
+
+setup(
+ name = "FixBot",
+ version = "0.1",
+ description = "IRC bot for sysadmin things",
+ ext_modules = [_utmp],
+)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/tap.py Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,25 @@
+from twisted.python import usage
+
+import nexus
+
+class NexusOptions (usage.Options) :
+ optParameters = [
+ ( "uid", "u", "fixbot", "user to run as" ),
+ ( "gid", "g", "nogroup", "group to run as" ),
+ ( "irc-hostname", "s", "irc.fixme.fi", "IRC server hostname", ),
+ ( "irc-port", "p", 6667, "IRC server port", int ),
+ ( "irc-nickname", "n", "FixBotDev", "IRC nickname", ),
+ ( "irc-username", "U", "fixbot", "IRC username", ),
+ ( "irc-channel", "c", "#fixme-test", "IRC channel", ),
+ ( "api-listen" "l", "127.0.0.1", "address for API server to listen on" ),
+ ( "api-port", "P", 34888, "port for API server to listen on", int ),
+ ]
+
+ optFlags = [
+
+ ]
+
+def makeService (config) :
+ # app = service.Application('fixbot', uid=config['uid'], gid=config['gid'])
+ return nexus.makeService(config)
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/fixbot/utmp.py Mon Sep 15 00:27:05 2008 +0300
@@ -0,0 +1,125 @@
+from twisted.internet import protocol, reactor
+from twisted.python import log
+import socket, os, time
+
+import _utmp, api, sys
+
+POLL_INTERVAL = 5
+
+def getPidCmd (pid) :
+ try :
+ fh = open("/proc/%d/cmdline" % pid, "r")
+ cmdline = fh.read()
+ fh.close()
+
+ return repr("%d:%s" % (pid, cmdline))
+ except IOError, OSError :
+ return "[%d]" % pid
+
+class WtmpEntry (object) :
+ _ATTRS = (
+ "type", "pid", "line", "id", "user", "host", "exit", "session", "tv", "addr"
+ )
+
+ @staticmethod
+ def fromFile (file) :
+ bytes = file.read(_utmp.size())
+
+ if not bytes :
+ return
+
+ result = _utmp.parse(bytes)
+
+ wtmp = WtmpEntry()
+
+ for name, field in zip(wtmp._ATTRS, result) :
+ if isinstance(field, str) and name not in ("addr", ) :
+ field = field.rstrip("\0")
+
+ setattr(wtmp, name, field)
+
+ # convert the address
+ family = socket.AF_INET
+ addr = wtmp.addr[0:4]
+ for byte in wtmp.addr[4:] :
+ if byte :
+ family = socket.AF_INET6
+ addr = wtmp.addr
+ break
+
+ wtmp.addr = socket.inet_ntop(family, addr)
+ wtmp.pid = getPidCmd(wtmp.pid)
+
+ return wtmp
+
+ def __str__ (self) :
+ return " ".join("%s=%s" % (key, getattr(self, key)) for key in self._ATTRS)
+
+class WtmpFile (object) :
+ def __init__ (self, path="/var/log/wtmp") :
+ self.path = path
+ self.file = open(path)
+ self.file.seek(0, os.SEEK_END)
+
+ def tryRead (self) :
+ return WtmpEntry.fromFile(self.file)
+
+def read_entries (file) :
+ while True :
+ wtmp = read_entry(file)
+
+ if wtmp :
+ yield wtmp
+ else :
+ return
+
+def follow_main () :
+ wtmp = WtmpFile()
+
+ while True :
+ item = wtmp.tryRead()
+
+ if item :
+ print item
+ else :
+ time.sleep(2)
+
+def test_main () :
+ fh = open("/var/log/wtmp", "r")
+
+ for item in read_entries(fh) :
+ print item
+
+class WtmpModule (api.Module) :
+ name = "wtmp"
+ version = 0x0001
+
+ event_types = [
+ "wtmp",
+ ]
+
+ def handleConnect (self) :
+ log.msg("Following default wtmp file...")
+
+ self.wtmp = WtmpFile()
+
+ log.msg("Starting poll timer")
+
+ self.poll()
+
+ log.msg("Running")
+
+ def poll (self) :
+
+ while True :
+ item = self.wtmp.tryRead()
+
+ if item :
+ log.msg(" -- %s" % item)
+ self.sendEvent("wtmp", str(item))
+ else :
+ return reactor.callLater(POLL_INTERVAL, self.poll)
+
+if __name__ == '__main__' :
+ WtmpModule().run()
+