commit 299372fd59737229213b2d7832d2dc6427ed04ba Author: Georg Hopp Date: Thu Mar 17 16:29:13 2016 +0100 initial checkin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cdbe749 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +showmyad.sh diff --git a/LdapService.py b/LdapService.py new file mode 100755 index 0000000..01406c5 --- /dev/null +++ b/LdapService.py @@ -0,0 +1,143 @@ +#!/usr/bin/python + +import time +import random +import mmap +import sys, getopt +from struct import pack +from collections import deque + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(realpath(__file__)) + '/lib') + +from Server import Server + +from Event.EventHandler import EventHandler +from Event.EventDispatcher import EventDispatcher +from Communication.EndPoint import CommunicationEndPoint + +from Protocol.Http.Http import Http +from Protocol.Websocket.Websocket import Websocket + +from LdapTree import LdapTree + +class Application(EventHandler): + def __init__(self, hosturi, binddn, basedn, password): + super(Application, self).__init__() + + self._event_methods = { + EventDispatcher.eventId('heartbeat') : self._heartbeat, + CommunicationEndPoint.eventId('new_msg') : self._handle_data, + CommunicationEndPoint.eventId('close') : self._handle_close, + CommunicationEndPoint.eventId('upgrade') : self._upgrade + } + + self._websockets = [] + + self._wstest = open('websocket.html', 'r+b') + self._wstestmm = mmap.mmap(self._wstest.fileno(), 0) + + random.seed() + + self.ldaptree = LdapTree(hosturi, binddn, basedn, password, False) + + def __del__(self): + self._wstestmm.close() + self._wstest.close() + + def _upgrade(self, event): + self._websockets.append(event.subject) + # let other also handle the upgrade .. no return True + + def _heartbeat(self, event): + now = pack('!d', time.time()) + for event.subject in self._websockets: + self.issueEvent(event.subject, 'send_msg', now) + + return True + + def _handle_data(self, event): + protocol = event.subject.getProtocol() + + if event.subject.hasProtocol(Http): + if event.data.isRequest(): + if event.data.getUri() == '/': + resp = protocol.createResponse(event.data, 200, 'OK') + resp.setBody(self._wstestmm[0:]) + elif event.data.getUri() == '/ldap': + resp = protocol.createResponse(event.data, 200, 'OK') + resp.setHeader('Content-Type', 'image/svg+xml') + resp.setBody(self.ldaptree.graph()) + else: + resp = protocol.createResponse(event.data, 404, 'Not Found') + resp.setBody('

404 - Not Found

') + + self.issueEvent(event.subject, 'send_msg', resp) + + return True + + def _handle_close(self, event): + if event.subject in self._websockets: + print 'websocket closed...' + self._websockets = [w for w in self._websockets if w!=event.subject] + + return True + +def usage(): + print "Usage: " + sys.argv[0] + " -[HDbhpk] bindip bindport\n" + print "Create a tree representation of all DNs starting with a given base DN." + print "Only simple binds to the directory with DN and password are supported." + print "If no password OPTION is given the password will be asked interactive." + print "If no outfile the given the result will be written to stdout.\n" + print "Required OPTIONS are:\n" + print " {:30s} : {:s}".format('-H, --hosturi=URI', 'The URI to the ldap server to query in the form:') + print " {:30s} {:s}".format('', 'ldap[s]://host.uri[:port]') + print " {:30s} : {:s}".format('-D, --binddn=DN', 'The DN to use for the LDAP bind.') + print " {:30s} : {:s}".format('-p, --password=PASSWORD', 'The password to use for the LDAP bind.') + print " {:30s} : {:s}\n".format('-b, --basedn=DN', 'The DN to start the tree with.') + print "Optional OPTIONS are:\n" + print " {:30s} : {:s}".format('-h, --help', 'Show this help page') + +def main(): + try: + opts, args = getopt.getopt( + sys.argv[1:], + 'hH:D:b:p:', + ['help', 'hosturi=', 'binddn=', 'basedn=', 'password=']) + except getopt.GetoptError as err: + print str(err) + usage() + sys.exit(2) + + hosturi = binddn = basedn = password = None + + for o, a in opts: + if o in ["-h", "--help"]: + usage() + sys.exit(0) + elif o in ["-H", "--hosturi"]: + hosturi = a + elif o in ["-D", "--binddn"]: + binddn = a + elif o in ["-b", "--basedn"]: + basedn = a + elif o in ["-p", "--password"]: + password = a + else: + print "unknown parameter: " + a + usage() + sys.exit(2) + + if not hosturi or not binddn or not basedn or not password: + usage() + sys.exit(2) + + server = Server(Application(hosturi, binddn, basedn, password)) + server.bindTcp(args[0], int(args[1]), Http()) + server.start(1.0) + +if __name__ == '__main__': + main() + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/README.md b/README.md new file mode 100644 index 0000000..64b87a0 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# ldapscan + +# summary + +This is some python code to scan and visualize an ldap tree structure. \ No newline at end of file diff --git a/ldaptree.py b/ldaptree.py new file mode 100755 index 0000000..765eefb --- /dev/null +++ b/ldaptree.py @@ -0,0 +1,85 @@ +#!/usr/bin/python +from os.path import dirname, realpath +import getopt, sys +sys.path.append(dirname(realpath(__file__)) + '/lib') + +import getpass +from LdapTree import LdapTree + +def usage(): + print "Usage: " + sys.argv[0] + " OPTION...\n" + print "Create a tree representation of all DNs starting with a given base DN." + print "Only simple binds to the directory with DN and password are supported." + print "If no password OPTION is given the password will be asked interactive." + print "If no outfile the given the result will be written to stdout.\n" + print "Required OPTIONS are:\n" + print " {:30s} : {:s}".format('-H, --hosturi=URI', 'The URI to the ldap server to query in the form:') + print " {:30s} {:s}".format('', 'ldap[s]://host.uri[:port]') + print " {:30s} : {:s}".format('-D, --binddn=DN', 'The DN to use for the LDAP bind.') + print " {:30s} : {:s}\n".format('-b, --basedn=DN', 'The DN to start the tree with.') + print "Optional OPTIONS are:\n" + print " {:30s} : {:s}".format('-h, --help', 'Show this help page') + print " {:30s} : {:s}".format('-p, --password=PASSWORD', 'The password to use for the LDAP bind.') + print " {:30s} : {:s}".format('-o, --outfile=FILENAME', 'File to write the result to.') + print " {:30s} : {:s}".format('-k, --kerberos', 'Use gssapi auth.') + +def main(): + try: + opts, args = getopt.getopt( + sys.argv[1:], + 'hkgH:D:b:p:o:', + ['help', 'kerberos', 'hosturi=', 'binddn=', 'basedn=', 'password=', 'outfile=']) + except getopt.GetoptError as err: + print str(err) + usage() + sys.exit(2) + + hosturi = binddn = basedn = password = outfile = None + creategraph = False + use_gssapi = False + + for o, a in opts: + if o in ["-h", "--help"]: + usage() + sys.exit(0) + elif o in ["-H", "--hosturi"]: + hosturi = a + elif o in ["-D", "--binddn"]: + binddn = a + elif o in ["-b", "--basedn"]: + basedn = a + elif o in ["-p", "--password"]: + password = a + elif o in ["-o", "--outfile"]: + outfile = a + elif o == "-g": + creategraph = True + elif o in ["-k", "--kerberos"]: + use_gssapi = True; + else: + print "unknown parameter: " + a + usage() + sys.exit(2) + + if not hosturi or (not binddn and not use_gssapi) or not basedn: + usage() + sys.exit(2) + + if not password and not use_gssapi: + password = getpass.getpass() + + info = LdapTree(hosturi, binddn, basedn, password, use_gssapi) + + if not creategraph: + if outfile: + info.text(outfile) + else: + print info.text() + else: + if outfile: + info.graph(outfile) + else: + print info.graph() + +if __name__ == "__main__": + main() diff --git a/lib/Communication/ConnectEntryPoint.py b/lib/Communication/ConnectEntryPoint.py new file mode 100644 index 0000000..cbaadd4 --- /dev/null +++ b/lib/Communication/ConnectEntryPoint.py @@ -0,0 +1,41 @@ +""" +Associate a physical transport layer with a protocol. + +Author: Georg Hopp +""" + +from EndPoint import CommunicationEndPoint + +from Transport import Transport + +class ConnectEntryPoint(CommunicationEndPoint): + _EVENTS = {'acc_ready': 0x01} + + def __init__(self, transport, protocol): + super(ConnectEntryPoint, self).__init__(transport, protocol) + self._accepted = [] + + self._transport.bind() + + def accept(self): + con = self._transport.accept() + + if not con: + return False + + while con: + self._accepted.append(con) + try: + con = self._transport.accept() + except Transport.Error as error: + con = None + + return True + + def pop(self): + try: + return self._accepted.pop() + except IndexError: + return None + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Communication/Connection.py b/lib/Communication/Connection.py new file mode 100644 index 0000000..872bc05 --- /dev/null +++ b/lib/Communication/Connection.py @@ -0,0 +1,66 @@ +""" +Associate a physical transport layer with a protocol. + +Author: Georg Hopp +""" + +from EndPoint import CommunicationEndPoint + +from Transport import Transport + +class Connection(CommunicationEndPoint): + _EVENTS = { 'new_con' : 0x01 } + + def __init__(self, transport, protocol, read_chunk_size=8192): + super(Connection, self).__init__(transport, protocol, read_chunk_size) + self._current_msg = None + self._read_buffer = '' + self._write_buffer = '' + + def hasPendingData(self): + return '' != self._write_buffer + + def __iter__(self): + return self + + def next(self): + """ + iterate through all available data and return all messages that can + be created from it. This is destructive for data. + """ + if not self._current_msg or self._current_msg.ready(): + self._current_msg = self._protocol.createMessage( + self.getTransport().remote) + + end = self._protocol.getParser().parse( + self._current_msg, self._read_buffer) + + if 0 == end: + raise StopIteration + + self._read_buffer = self._read_buffer[end:] + if not self._current_msg.ready(): + raise StopIteration + + return self._current_msg + + def compose(self, message): + try: + self._write_buffer += self._protocol.getComposer().compose(message) + except Exception: + return False + + return True + + def appendReadData(self, data_remote): + self._read_buffer += data_remote[0] + + def nextWriteData(self): + buf = self._write_buffer + self._write_buffer = '' + return (buf, None) + + def appendWriteData(self, data_remote): + self._write_buffer += data_remote[0] + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Communication/Connector.py b/lib/Communication/Connector.py new file mode 100644 index 0000000..6f61d21 --- /dev/null +++ b/lib/Communication/Connector.py @@ -0,0 +1,37 @@ +""" +Handles the acc_ready event. Accept as long as possible on subject. +For each successfull accept assign protocol and emit a new_con event +holding the new connection. + +Author: Georg Hopp +""" + +from Connection import Connection +from ConnectEntryPoint import ConnectEntryPoint + +from Event.EventHandler import EventHandler +from Transport import Transport + +class Connector(EventHandler): + def __init__(self): + super(Connector, self).__init__() + + self._event_methods = { + ConnectEntryPoint.eventId('acc_ready') : self._accept + } + + def _accept(self, event): + try: + protocol = event.subject.getProtocol() + if event.subject.accept(): + con = event.subject.pop() + while con: + new_con = Connection(con, protocol) + self.issueEvent(new_con, 'new_con') + con = event.subject.pop() + except Transport.Error as error: + self.issueEvent(event.subject, 'close') + + return True + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Communication/DatagramEntryPoint.py b/lib/Communication/DatagramEntryPoint.py new file mode 100644 index 0000000..7eddb77 --- /dev/null +++ b/lib/Communication/DatagramEntryPoint.py @@ -0,0 +1,14 @@ +""" +Associate a physical transport layer with a protocol. + +Author: Georg Hopp +""" +from DatagramService import DatagramService + +class DatagramEntryPoint(DatagramService): + def __init__(self, transport, protocol, read_chunk_size=8192): + super(DatagramEntryPoint, self).__init__( + transport, protocol, read_chunk_size) + self._transport.bind() + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Communication/DatagramService.py b/lib/Communication/DatagramService.py new file mode 100644 index 0000000..97ae40a --- /dev/null +++ b/lib/Communication/DatagramService.py @@ -0,0 +1,68 @@ +""" +Associate a physical transport layer with a protocol. + +Author: Georg Hopp +""" +from collections import deque + +from EndPoint import CommunicationEndPoint +from Transport import Transport + +class DatagramService(CommunicationEndPoint): + _EVENTS = {} + + def __init__(self, transport, protocol, read_chunk_size=8192): + super(DatagramService, self).__init__( + transport, protocol, read_chunk_size) + self._read_buffer = deque([]) + self._write_buffer = deque([]) + self._transport.open() + + def hasPendingData(self): + return self._write_buffer + + def __iter__(self): + return self + + def next(self): + """ + here a message has to be fit into a single packet, so no multiple + reads are done.. if a message was not complete after a read the + data will be dropped silently because it can't be guaranteed + that we got the rest somehow in the correct order. + """ + if not self._read_buffer: + raise StopIteration + + msginfo = self._read_buffer.popleft() + message = self._protocol.createMessage(msginfo[1]) + if not message: + raise StopIteration + + end = self._protocol.getParser().parse(message, msginfo[0]) + if 0 == end: raise StopIteration + + return message + + def compose(self, message): + try: + data = self._protocol.getComposer().compose(message) + self.appendWriteData((data, message.getRemote())) + except Exception: + return False + + return True + + def appendReadData(self, data_remote): + self._read_buffer.append(data_remote) + + def nextWriteData(self): + if not self._write_buffer: + return ('', None) + + return self._write_buffer.popleft() + + def appendWriteData(self, data_remote): + self._write_buffer.append(data_remote) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Communication/EndPoint.py b/lib/Communication/EndPoint.py new file mode 100644 index 0000000..c31aa3a --- /dev/null +++ b/lib/Communication/EndPoint.py @@ -0,0 +1,98 @@ +""" +Associate a physical transport layer with a protocol. + +Author: Georg Hopp +""" +import errno + +from Event.EventSubject import EventSubject + +class CommunicationEndPoint(EventSubject): + _EVENTS = { + 'read_ready' : 0x01, + 'write_ready' : 0x02, + 'upgrade' : 0x03, + 'new_data' : 0x04, + 'pending_data' : 0x05, + 'end_data' : 0x06, + 'new_msg' : 0x07, + 'send_msg' : 0x08, + 'shutdown_read' : 0x09, + 'shutdown_write' : 0x10, + 'close' : 0x11 + } + + def __init__(self, transport, protocol, read_chunk_size=8192): + super(CommunicationEndPoint, self).__init__() + self.setProtocol(protocol) + self._transport = transport + self._read_chunk_size = read_chunk_size + self._do_close = False + + def setClose(self): + self._do_close = True + + def hasProtocol(self, protocol): + return isinstance(self.getProtocol(), protocol) + + def hasPendingData(self): + return False + + def shouldClose(self): + return self._do_close + + def getTransport(self): + return self._transport + + def setProtocol(self, protocol): + self._protocol = protocol + + def getProtocol(self): + return self._protocol + + def getHandle(self): + return self.getTransport().getHandle() + + def appendReadData(self, data_remote): + pass + + def nextWriteData(self): + return None + + def appendWriteData(self, data_remote): + pass + + def bufferRead(self): + data_remote = self._transport.recv(self._read_chunk_size) + + if not data_remote: + return False + + while data_remote: + self.appendReadData(data_remote) + data_remote = self._transport.recv(self._read_chunk_size) + + return True + + def writeBuffered(self): + data, remote = self.nextWriteData() + send = 0 + + while data: + current_send = self._transport.send(data, remote) + if 0 == current_send: + if data: + self.appendWriteData((data, remote)) + break + + send += current_send + data = data[send:] + if not data: + data, remote = self.nextWriteData() + + if 0 == send: + return False + + return True + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Communication/Manager.py b/lib/Communication/Manager.py new file mode 100644 index 0000000..6df75df --- /dev/null +++ b/lib/Communication/Manager.py @@ -0,0 +1,175 @@ +""" +Manage Communication Events. + +The events handled here are: + new_con: + +@author Georg Hopp +""" +import threading + +from EndPoint import CommunicationEndPoint as EndPoint +from Connection import Connection + +from Event.EventHandler import EventHandler +from Event.EventDispatcher import EventDispatcher as Dispatcher + +class CommunicationManager(EventHandler): + def __init__(self): + super(CommunicationManager, self).__init__() + + self._cons = {} + self._listen = {} + self._wcons = [] + self._rcons = [] + self._ready = ([],[],[]) + self._cons_lock = threading.Lock() + + self._event_methods = { + Dispatcher.eventId('data_wait') : self._select, + Dispatcher.eventId('shutdown') : self._shutdown, + Connection.eventId('new_con') : self._addCon, + Connection.eventId('pending_data') : self._enableWrite, + Connection.eventId('end_data') : self._disableWrite, + EndPoint.eventId('close') : self._close, + EndPoint.eventId('shutdown_read') : self._shutdownRead, + EndPoint.eventId('shutdown_write') : self._shutdownWrite + } + + def addEndPoint(self, end_point): + handle = end_point.getHandle() + self._cons_lock.acquire() + if handle not in self._listen and handle not in self._cons: + if end_point.getTransport().isListen(): + self._listen[handle] = end_point + else: + self._cons[handle] = end_point + self._rcons.append(handle) + self._cons_lock.release() + + def _addCon(self, event): + self.addEndPoint(event.subject) + return True + + def _enableWrite(self, event): + handle = event.subject.getHandle() + fin_state = event.subject.getTransport()._fin_state + + if handle not in self._wcons and 0 == fin_state & 2: + self._wcons.append(handle) + return True + + def _disableWrite(self, event): + handle = event.subject.getHandle() + fin_state = event.subject.getTransport()._fin_state + + if handle in self._wcons: + self._wcons.remove(handle) + + if 1 == fin_state & 1: + self.issueEvent(event.subject, 'shutdown_write') + return True + + def _select(self, event): + import select + + try: + timeout = event.data + if timeout is None: + timeout = event.subject.getDataWaitTime() + + self._cons_lock.acquire() + if timeout < 0.0: + self._ready = select.select(self._rcons, self._wcons, []) + else: + self._ready = select.select(self._rcons, self._wcons, [], timeout) + self._cons_lock.release() + except select.error: + self._cons_lock.release() + pass + + + for handle in self._ready[0]: + if handle in self._listen: + self.issueEvent(self._listen[handle], 'acc_ready') + if handle in self._cons: + self.issueEvent(self._cons[handle], 'read_ready') + + for handle in self._ready[1]: + if handle in self._cons: + self.issueEvent(self._cons[handle], 'write_ready') + + return True + + def _shutdown(self, event): + for handle in self._listen: + self.issueEvent(self._listen[handle], 'close') + + for handle in self._cons: + self.issueEvent(self._cons[handle], 'close') + + self._rcons = self._wcons = [] + + return False + + """ + shutdown and close events...these are handled here because the communication + end points need to be remove for the according lists here. So this is the + highest abstraction level that needs to react on this event. + """ + def _shutdownRead(self, event): + handle = event.subject.getHandle() + if handle in self._rcons: + self._rcons.remove(handle) + + if 3 == event.subject.getTransport().shutdownRead(): + """ + close in any case + """ + self.issueEvent(event.subject, 'close') + elif not event.subject.hasPendingData(): + """ + If there is pending data we will handle a disable_write later on. + There this event will be fired. In that case. + """ + self.issueEvent(event.subject, 'shutdown_write') + else: + """ + Flag this endpoint as subject to close when there is nothing more + to do with it. After this is set all pending IO may finish and then + a close event should be issued + """ + event.subject.setClose() + return False + + def _shutdownWrite(self, event): + handle = event.subject.getHandle() + if handle in self._wcons: + self._wcons.remove(handle) + + if 3 == event.subject.getTransport().shutdownWrite(): + self.issueEvent(event.subject, 'close') + # a read will be done anyway so no special handling here. + # As long as the socket is ready for reading we will read from it. + return False + + def _close(self, event): + self._cons_lock.acquire() + event.subject.getTransport().shutdown() + + handle = event.subject.getHandle() + if handle in self._rcons: + self._rcons.remove(handle) + if handle in self._wcons: + self._wcons.remove(handle) + + if handle in self._listen: + del(self._listen[handle]) + else: + del(self._cons[handle]) + + event.subject.getTransport().close() + self._cons_lock.release() + return False + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Communication/ProtocolHandler.py b/lib/Communication/ProtocolHandler.py new file mode 100644 index 0000000..bbc472c --- /dev/null +++ b/lib/Communication/ProtocolHandler.py @@ -0,0 +1,72 @@ +""" +@author Georg Hopp +""" +from contextlib import contextmanager + +from Connection import Connection +from Event.EventHandler import EventHandler + +class ProtocolHandler(EventHandler): + def __init__(self): + super(ProtocolHandler, self).__init__() + + self._event_methods = { + Connection.eventId('new_data') : self._parse, + Connection.eventId('send_msg') : self._compose, + Connection.eventId('upgrade') : self._upgrade + } + + def _parse(self, event): + for message in event.subject: + try: + """ + only because websockets currently have no message + class which would handle this correctly...so we + just ignore this problem here. + """ + if message.isCloseMessage(): + self.issueEvent(event.subject, 'new_msg', message) + if message.isResponse(): + event.subject.setClose() # setting this results in + # closing the endpoint as + # soon as everything was tried + elif message.isUpgradeMessage(): + if message.isRequest(): + protocol = event.subject.getProtocol() + response = protocol.createUpgradeResponse(message) + self.issueEvent(event.subject, 'send_msg', response) + else: + protocol = event.subject.getProtocol() + self.issueEvent(event.subject, 'upgrade', message) + else: + self.issueEvent(event.subject, 'new_msg', message) + except Exception: + pass + + def _compose(self, event): + endpoint = event.subject + message = event.data + + if endpoint.compose(message): + self.issueEvent(endpoint, 'write_ready') + + try: + """ + only because websockets currently have no message + class which would handle this correctly...so we + just ignore this problem here. + """ + if message.isResponse(): + if message.isCloseMessage(): + endpoint.setClose() + if message.isUpgradeMessage(): + self.issueEvent(endpoint, 'upgrade', message) + except Exception: + pass + + def _upgrade(self, event): + protocol = event.subject.getProtocol() + new_proto = protocol.upgrade(event.data) + event.subject.setProtocol(new_proto) + +# vim: set ft=python et ts=4 sw=4 sts=4: diff --git a/lib/Communication/__init__.py b/lib/Communication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/DnsClient.py b/lib/DnsClient.py new file mode 100644 index 0000000..6a34b87 --- /dev/null +++ b/lib/DnsClient.py @@ -0,0 +1,32 @@ +""" +get our current external IP via HTTP + +@author Georg Hopp + +@copyright (C) 2014 Copperfasten Technologies +""" +import struct + +from SimpleClient import SimpleClient +from Protocol.Dns.Dns import Dns +from Communication.DatagramService import DatagramService +from Transport.UdpSocket import UdpSocket + +class DnsClient(object): + def __init__(self, host, port): + self._proto = Dns() + self._client = SimpleClient( + DatagramService(UdpSocket(host, port), self._proto) + ) + + def getIp(self, name, timeout=3.0): + request = self._proto.createRequest(self._client.getRemoteAddr()) + request.addQuery(name) + response = self._client.issue(request, timeout) + + if not response or not response._answers: + raise Exception('no valid response') + + return '.'.join('%d'%i + for i in struct.unpack( + '4B', response._answers[0][4])) diff --git a/lib/Event/Event.py b/lib/Event/Event.py new file mode 100644 index 0000000..f8d4e6f --- /dev/null +++ b/lib/Event/Event.py @@ -0,0 +1,21 @@ +""" +This holds a generated Event. + +Author: Georg Hopp +""" + +class Event(object): + _SERIAL = 0 + + def __init__(self, name, type, subject): + self.name = name + self.type = type + self.subject = subject + self.data = None + self.sno = Event._SERIAL + Event._SERIAL += 1 + + def setData(self, data): + self.data = data + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Event/EventDispatcher.py b/lib/Event/EventDispatcher.py new file mode 100644 index 0000000..37893da --- /dev/null +++ b/lib/Event/EventDispatcher.py @@ -0,0 +1,133 @@ +""" +Dispatch Events to registered handlers. + +Author: Georg Hopp +""" +import sys +import time +import threading +from collections import deque + +from Event import Event +from EventHandler import EventHandler +from EventSubject import EventSubject + +class DefaultHandler(EventHandler): + def handleEvent(self, event): + return True + +SERVER = 0x00 +CLIENT = 0x01 + +class EventDispatcher(EventSubject): + _EVENTS = { + 'heartbeat' : 0x01, + 'user_wait' : 0x02, + 'data_wait' : 0x03, + 'shutdown' : 0x04 + } + + def __init__(self, mode = SERVER, default_handler = DefaultHandler()): + super(EventDispatcher, self).__init__() + + self._events = deque([]) + self._handler = {} + self._default_handler = default_handler + self._running = False + self._heartbeat = 0.0 + self._nextbeat = 0.0 + self._mode = mode + self._queue_lock = threading.Lock() + self._event_wait = threading.Condition() + self._data_wait_id = EventDispatcher.eventId('data_wait') + self._user_wait_id = EventDispatcher.eventId('user_wait') + + def registerHandler(self, handler): + for eid in handler.getHandledIds(): + if eid in self._handler: + self._handler[eid].append(handler) + else: + self._handler[eid] = [handler] + + handler.setDispatcher(self) + + def setHeartbeat(self, heartbeat): + self._heartbeat = heartbeat + if self._heartbeat: + self._nextbeat = time.time() + self._heartbeat + else: + self._nextbeat = 0.0 + + def getBeattime(self): + return self._nextbeat - time.time() + + def getDataWaitTime(self): + if self._mode == SERVER: + return self.getBeattime() + + # here comes a timeout into play.... currently I expect + # the stuff to work... + # TODO add timeout + return 0.0 + + def queueEvent(self, event): + self._queue_lock.acquire() + self._events.append(event) + self._queue_lock.release() + self._event_wait.acquire() + self._event_wait.notify_all() + self._event_wait.release() + + def start(self, name): + self._running = True + + while self._running or self._events: + now = time.time() + if self._nextbeat and self._nextbeat <= now: + self._nextbeat += self._heartbeat + self.queueEvent(self.emit('heartbeat')) + + current = None + if not self._events: + if not name: + if self._mode == CLIENT: + current = self.emit('user_wait') + else: + current = self.emit('data_wait') + else: + self._event_wait.acquire() + self._event_wait.wait() + self._event_wait.release() + + self._queue_lock.acquire() + if (not current) and self._events: + current = self._events.popleft() + self._queue_lock.release() + + if current: + if current.type not in self._handler: + #print '[%s] handle: %s(%d) on %s: %s' % ( + # name, current.name, current.sno, hex(id(current.subject)), 'default') + self._default_handler.handleEvent(current) + else: + for handler in self._handler[current.type]: + #print '[%s] handle: %s(%d) on %s: %s' % ( + # name, current.name, current.sno, hex(id(current.subject)), + # handler.__class__.__name__) + if handler.handleEvent(current): + break + + # if we leave the loop eventually inform all other threads + # so they can quit too. + self._event_wait.acquire() + self._event_wait.notify_all() + self._event_wait.release() + + def stop(self): + self._running = False + + def shutdown(self): + self.queueEvent(self.emit('shutdown')) + self.stop() + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Event/EventHandler.py b/lib/Event/EventHandler.py new file mode 100644 index 0000000..6ae9398 --- /dev/null +++ b/lib/Event/EventHandler.py @@ -0,0 +1,28 @@ +""" +Base event handler + +Author: Georg Hopp +""" +class EventHandler(object): + def __init__(self): + self._dispatcher = [] + self._event_methods = {} + + def setDispatcher(self, dispatcher): + self._dispatcher.append(dispatcher) + + def getHandledIds(self): + return self._event_methods.keys() + + def issueEvent(self, eventSource, ident, data = None): + event = eventSource.emit(ident, data) + #print 'issue %s(%d) on %s: %s' % ( + # ident, event.sno, hex(id(event.subject)), self.__class__.__name__) + for dispatcher in self._dispatcher: + dispatcher.queueEvent(event) + + def handleEvent(self, event): + if event.type not in self._event_methods: + return False + + return self._event_methods[event.type](event) diff --git a/lib/Event/EventSubject.py b/lib/Event/EventSubject.py new file mode 100644 index 0000000..7d4f892 --- /dev/null +++ b/lib/Event/EventSubject.py @@ -0,0 +1,35 @@ +""" +Methodology to craete Events, that can be uniquely identified. + +@Author: Georg Hopp +""" +from Event import Event + +class EventSubject(object): + _EVENTS = {} + + @classmethod + def eventId(cls, ident): + """ + Get a unique event identifier based on the class of the event source + and the found map value of ident. If there is no mapping in the + current class its EventSource bases super classes are queried until + an event id can be found... if you derive one event source from + multiple others that provide the same event identifier this means that + you can't predict which one will be created. + I guess that there might be a more pythonic way to do this with + something like a generator expression. + """ + if ident in cls._EVENTS: + return (id(cls) << 8) | cls._EVENTS[ident] + else: + for base in [b for b in cls.__bases__ if issubclass(b, EventSubject)]: + event_id = base.eventId(ident) + if event_id: return event_id + + def emit(self, ident, data = None): + event = Event(ident, type(self).eventId(ident), self) + if data: event.setData(data) + return event + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Event/EventThread.py b/lib/Event/EventThread.py new file mode 100644 index 0000000..740b001 --- /dev/null +++ b/lib/Event/EventThread.py @@ -0,0 +1,19 @@ +""" +Dispatch Events to registered handlers. + +Author: Georg Hopp +""" +import threading + +class EventThread(threading.Thread): + def __init__(self, dispatcher, name): + super(EventThread, self).__init__() + self._dispatcher = dispatcher + self._name = name + + def run(self): + print 'start thread' + self._dispatcher.start(self._name) + print 'stop thread' + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Event/Signal.py b/lib/Event/Signal.py new file mode 100644 index 0000000..defc1c8 --- /dev/null +++ b/lib/Event/Signal.py @@ -0,0 +1,17 @@ +import signal + +def initSignals(dispatcher): + def signalHandler(num, frame): + #signal.signal(num, signal.SIG_IGN) + dispatcher.shutdown() + + signal.signal(signal.SIGTERM, signalHandler) + signal.signal(signal.SIGINT, signalHandler) + signal.signal(signal.SIGQUIT, signalHandler) + signal.signal(signal.SIGABRT, signalHandler) + + signal.signal(signal.SIGHUP, signal.SIG_IGN) + signal.signal(signal.SIGALRM, signal.SIG_IGN) + signal.signal(signal.SIGURG, signal.SIG_IGN) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Event/__init__.py b/lib/Event/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/LdapTree.py b/lib/LdapTree.py new file mode 100644 index 0000000..7c59f7b --- /dev/null +++ b/lib/LdapTree.py @@ -0,0 +1,96 @@ +import ldap +import pygraphviz as pgv + +class LdapTree(object): + def __init__(self, hosturi, binddn, basedn, password, use_gssapi): + #ldap.set_option(ldap.OPT_DEBUG_LEVEL, 1) + self._ldap = ldap.initialize(hosturi) + """ + Setting ldap.OPT_REFERRALS to 0 was neccessary to query a samba4 + active directory... Currently I don't know if it is a good idea + to keep it generally here. + """ + self._ldap.set_option(ldap.OPT_REFERRALS, 0) + if use_gssapi: + sasl_auth = ldap.sasl.sasl({},'GSSAPI') + self._ldap.sasl_interactive_bind_s("", sasl_auth) + else: + self._ldap.bind(binddn, password, ldap.AUTH_SIMPLE) + self._basedn = basedn + self._ldap_result = [] + + def text(self, filename = None): + """ + Returns a text representing the directory. + If filename is given it will be written in that file. + """ + if filename: + with open(filename, "w") as text_file: + text_file.write(self._text(self._basedn, 0)) + else: + return self._text(self._basedn, 0) + + def graph(self, filename = None): + """ + Returns an svg representing the directory. + If filename is given it will be written in that file. + """ + graph = pgv.AGraph( + directed=True, charset='utf-8', fixedsize='true', ranksep=0.1) + + graph.node_attr.update( + style='rounded,filled', width='0', height='0', shape='box', + fillcolor='#E5E5E5', concentrate='true', fontsize='8.0', + fontname='Arial', margin='0.03') + + graph.edge_attr.update(arrowsize='0.55') + + self._graph(graph, self._basedn) + + graph.layout(prog='dot') + if filename: + graph.draw(path=filename, format='svg') + return None + else: + return graph.draw(format='svg') + + def _text(self, dn, level): + """ + Recursive function that returns a string representation of the + directory where each depth is indicated by a dash. + """ + result = self._ldap.search_s(dn, ldap.SCOPE_ONELEVEL) + indent = '-' * level + text = indent + dn + "\n" + + for entry in (entry[0] for entry in result): + if entry: + text += self._text(entry, level + 1) + + return text + + def _graph(self, graph, dn): + """ + Recursive function creating a graphviz graph from the directory. + """ + result = self._ldap.search_s(dn, ldap.SCOPE_ONELEVEL) + minlen = thislen = 1 + edge_start = dn + + for entry in (entry[0] for entry in result): + if entry: + point = entry + '_p' + sub = graph.add_subgraph() + sub.graph_attr['rank'] = 'same' + sub.add_node( + point, shape='circle', fixedsize='true', width='0.04', + label='', fillcolor='transparent') + sub.add_node(entry) + graph.add_edge(edge_start, point, arrowhead='none', + minlen=str(minlen)) + graph.add_edge(point, entry) + edge_start = point + minlen = self._graph(graph, entry) + thislen += minlen + + return thislen diff --git a/lib/MultiEndClient.py b/lib/MultiEndClient.py new file mode 100644 index 0000000..b094dca --- /dev/null +++ b/lib/MultiEndClient.py @@ -0,0 +1,70 @@ +import time + +from Event.EventDispatcher import EventDispatcher, CLIENT +from Event.EventHandler import EventHandler +import Event.Signal as Signal + +from Communication.Manager import CommunicationManager +from Communication.EndPoint import CommunicationEndPoint +from Communication.ProtocolHandler import ProtocolHandler +from Transport.IoHandler import IoHandler + +class MultiEndClient(EventHandler): + def __init__(self): + self._event_methods = { + EventDispatcher.eventId('user_wait') : self._userInteraction, + CommunicationEndPoint.eventId('new_msg') : self._handleData + } + + self._con_mngr = CommunicationManager() + + self._dispatcher = EventDispatcher(CLIENT) + self._dispatcher.registerHandler(self._con_mngr) + self._dispatcher.registerHandler(IoHandler()) + self._dispatcher.registerHandler(ProtocolHandler()) + self._dispatcher.registerHandler(self) + Signal.initSignals(self._dispatcher) + + self._end_point = None + self._timeout = None + self._starttime = None + self._request = None + self._response = None + self._sendIssued = False + + + def issue(self, end_point, request, timeout): + self._starttime = time.time() + self._timeout = timeout + self._request = request + self._response = None + self._sendIssued = False + self._end_point = end_point + self._con_mngr.addEndPoint(end_point) + self._dispatcher.start() + + return self._response + + def _userInteraction(self, event): + if self._sendIssued: + now = time.time() + + if self._response or self._timeout <= (now - self._starttime): + event.subject.stop() + else: + self.issueEvent( + event.subject, + 'data_wait', + self._timeout - (now - self._starttime) + ) + else: + self.issueEvent(self._end_point, 'send_msg', self._request) + self._sendIssued = True + return True + + def _handleData(self, event): + if event.data.isResponse(): + self._response = event.data + return True + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Protocol/Dns/Composer.py b/lib/Protocol/Dns/Composer.py new file mode 100644 index 0000000..d8ea0b8 --- /dev/null +++ b/lib/Protocol/Dns/Composer.py @@ -0,0 +1,103 @@ +""" + @author Georg Hopp + +""" +import struct + +class Composer(object): + def __init__(self): + self._name_ofs = {} + + def compose(self, message): + self._name_ofs = {} + + header = struct.pack( + '!HHHHHH', + message._msg_id, + message._flags, + len(message._queries), + len(message._answers), + len(message._authoritys), + len(message._additionals) + ) + + queries = answers = authoritys = additionals = '' + ofs = len(header) + if message._queries: + queries = self._composeQueries(message, ofs) + + ofs += len(queries) + if message._answers: + answers = self._composeAnswers(message, ofs) + + ofs += len(answers) + if message._authoritys: + authoritys = self._composeAuthoritys(message, ofs) + + ofs += len(authoritys) + if message._additionals: + additionals = self._composeAdditionals(message, ofs) + + return header + queries + answers + authoritys + additionals + + def _composeQueries(self, message, ofs): + encoded = '' + + for query in message._queries: + name, typ, cls = query + ename = self._encodeName(name, ofs) + + query = struct.pack('!%dsHH'%len(ename), ename, typ, cls) + ofs += len(query) + encoded += query + + return encoded + + def _composeAnswers(self, message, ofs): + encoded = '' + + for answer in message._answers: + record = self._composeResourceRecord(answer, ofs) + ofs += len(record) + encoded += record + + return encoded + + def _composeAuthoritys(self, message, ofs): + encoded = '' + + for authority in message._authoritys: + record = self._composeResourceRecord(authority, ofs) + ofs += len(record) + encoded += record + + return encoded + + def _composeAdditionals(self, message, ofs): + encoded = '' + + for additional in message._additionals: + record = self._composeResourceRecord(additional, ofs) + ofs += len(record) + encoded += record + + return encoded + + def _composeResourceRecord(self, record, ofs): + name, typ, cls, ttl, data = record + ename = self._encodeName(name, ofs) + return struct.pack('!%dsHHLH%ds'%(len(ename), len(data)), + ename, typ, cls, ttl, len(data), data) + + def _encodeName(self, name, ofs): + if name in self._name_ofs: + name = struct.pack('!H', + int('1100000000000000', 2) | self._name_ofs[name]) + else: + self._name_ofs[name] = ofs + name = ''.join([struct.pack('B%ds'%len(p), len(p), p) + for p in name.split('.')]) + '\x00' + + return name + +# vim: set ft=python et ts=4 sw=4 sts=4: diff --git a/lib/Protocol/Dns/Dns.py b/lib/Protocol/Dns/Dns.py new file mode 100644 index 0000000..0a564fb --- /dev/null +++ b/lib/Protocol/Dns/Dns.py @@ -0,0 +1,37 @@ +""" +@author Georg Hopp + +""" +from ..Protocol import Protocol + +from Parser import Parser +from Composer import Composer +from Message import Message + +class Dns(Protocol): + def __init__(self): + self.parser = Parser() + self.composer = Composer() + + def getParser(self): + return self.parser + + def getComposer(self): + return self.composer + + def createMessage(self, remote = None): + return Message(remote) + + def createRequest(self, remote = None): + return Message(remote) + + def createResponse(self, req, remote = None): + return Message(remote, req) + + def upgrade(self, message): + ''' + there is no upgrade mechanism for DNS + ''' + pass + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Protocol/Dns/Info.txt b/lib/Protocol/Dns/Info.txt new file mode 100644 index 0000000..4b663b9 --- /dev/null +++ b/lib/Protocol/Dns/Info.txt @@ -0,0 +1,103 @@ + A simple DNS message and response implementation. + It only supports name queries. + + good informations about dns: + rfc1035 + http://technet.microsoft.com/en-us/library/dd197470(v=ws.10).aspx + serveral more could be found via google. + + What we need: + dns header 6 * 16bit + 16bit ID + 16bit Flags + 1bit request/response indicator (0 = request) + 4bit operation code / what operation to be done (0 = query) + 1bit authoritive answer / obviosly only used for responses + 1bit truncation / indicate that the message was to large for a UDP datagram + 1bit recursion desired / 1 to recurse the request (we normally want this) + 1bit recursion available / obvious + 3bit reserved / set to 000 + 4bit return code / 0 means successfull, currently all other are wrong for us + 16bit Question count + 16bit Answer count + 16bit Authority count + 16bit Additional count + + 1 question resource record (valriable len) our would look like this. + question name: 0x09localhost0x00 + 16bit question type: 0x0001 (for A record question) + 16bit question class: 0x0001 (represents the IN question class) + +TYPE value and meaning +======================================================== +(removed all obsolete and experimental codes) +A 1 a host address +NS 2 an authoritative name server +CNAME 5 the canonical name for an alias +SOA 6 marks the start of a zone of authority +WKS 11 a well known service description +PTR 12 a domain name pointer +HINFO 13 host information +MINFO 14 mailbox or mail list information +MX 15 mail exchange +TXT 16 text strings + +QTYPE values +======================================================== +QTYPE fields appear in the question part of a query. QTYPES are a +superset of TYPEs, hence all TYPEs are valid QTYPEs. In addition, the +following QTYPEs are defined: + +AXFR 252 A request for a transfer of an entire zone +* 255 A request for all records + +CLASS values +======================================================== +IN 1 the Internet +CH 3 the CHAOS class +HS 4 Hesiod [Dyer 87] + + + Our hardcoded request message: + 434301000001000000000000096C6F63616C686F73740000010001 + ^ ^ ^ ^ ^ ^ + ID | | | | | + flags | | | | + one query | | | + query name (localhost) | | + type | + class + + OK, as i analyse the response i realize that my request was repeated back along + with the answer. For now I assume this is the default behaviour of DNS. + At least I can be sure that our DNS will always respond that way. + + The last 4 bytes of the answer record represent the ip address. We can savely + assume this as currently we only query IPv4 A records. With these this should + be always true. + + out complete response was: + 434381800001000100000000096c6f63616c686f73740000010001c00c000100010000000f00040a0100dc + ^ ^ ^ + no error | | + one request | + one response + + We cut of the headers and the request (as it was our own...we do not care about + it), leaving us with: + c00c000100010000000f00040a0100dc + ^ ^ ^ ^ ^ ^ + nref | | | | | + type | | | | + class | | | + TTL | | + resource date len | + here starts our ip + + nref => is a reference of the name queried corresponding the + DNS Packet Compression Schema: + 2bits: compression indicator (11 when compression is active) + rest: offset to name + + In our case this means the offset is 0x0c (12). The offset is the offset from + the start of the message. diff --git a/lib/Protocol/Dns/Message.py b/lib/Protocol/Dns/Message.py new file mode 100644 index 0000000..4ff95a1 --- /dev/null +++ b/lib/Protocol/Dns/Message.py @@ -0,0 +1,79 @@ +""" + @author Georg Hopp + +""" +import struct +import random + +from ..Message import Message as BaseMessage + +class Message(BaseMessage): + TYPE_A = 1 + TYPE_NS = 2 + TYPE_CNAME = 5 + TYPE_SOA = 6 + TYPE_WKS = 11 + TYPE_PTR = 12 + TYPE_HINFO = 13 + TYPE_MINFO = 14 + TYPE_MX = 15 + TYPE_TXT = 16 + + CLASS_IN = 1 + CLASS_CH = 3 + CLASS_HS = 4 + + OP_QUERY = 1 + + FLAG_QR = int('1000000000000000', 2) + + def __init__(self, remote, msg=None): + super(Message, self).__init__(remote) + """ + if we want to create a response we initialize the message with the request. + """ + if msg: + if not msg.isRequest(): + raise Exception('initialize with non request') + + self._msg_id = msg._msg_id + self._flags = msg._flags | Message.FLAG_QR + self._queries = list(msg._queries) + else: + random.seed + self._msg_id = random.randint(0, 0xffff) + self._flags = 0 + self._queries = [] + + self._answers = [] + self._authoritys = [] + self._additionals = [] + + def isRequest(self): + return 0 == self._flags & Message.FLAG_QR + + def isResponse(self): + return not self.isRequest() + + def isCloseMessage(self): + return False + + def isUpgradeMessage(self): + return False + + def setRepsonse(self): + self._flags |= Message.FLAG_QR + + def addQuery(self, name, typ=TYPE_A, cls=CLASS_IN): + self._queries.append((name, typ, cls)) + + def addAnswer(self, name, typ, cls, ttl, data): + self._answers.append((name, typ, cls, ttl, data)) + + def getResponseCode(self): + return 0 + + def getResponseMessage(self): + return None + +# vim: set ft=python et ts=4 sw=4 sts=4: diff --git a/lib/Protocol/Dns/Parser.py b/lib/Protocol/Dns/Parser.py new file mode 100644 index 0000000..493268b --- /dev/null +++ b/lib/Protocol/Dns/Parser.py @@ -0,0 +1,90 @@ +""" + @author Georg Hopp + +""" +import struct + +class Parser(object): + def __init__(self): + self._ofs_names = {} + + def parse(self, message, data): + self._ofs_names = {} + + message._msg_id, \ + message._flags, \ + nqueries, \ + nanswers, \ + nauthorities, \ + nadditionals = struct.unpack('!HHHHHH', data[0:12]) + + ofs = 12 + ofs = self._parseQueries(message, data, ofs, nqueries) + ofs = self._parseAnswers(message, data, ofs, nanswers) + ofs = self._parseAuthorities(message, data, ofs, nauthorities) + self._parseAdditionals(message, data, ofs, nadditionals) + + def _parseQueries(self, message, data, ofs, count): + while 0 < count: + name, ofs = self._decodeName(data, ofs) + typ, cls = struct.unpack('!HH', data[ofs:ofs+4]) + ofs += 4 + count -= 1 + message._queries.append((name, typ, cls)) + + return ofs + + def _parseAnswers(self, message, data, ofs, count): + while 0 < count: + record, ofs = self._parseResourceRecord(message, data, ofs) + count -= 1 + message._answers.append(record) + + return ofs + + def _parseAuthorities(self, message, data, ofs, count): + while 0 < count: + record, ofs = self._parseResourceRecord(message, data, ofs) + count -= 1 + message._authorities.append(record) + + return ofs + + def _parseAdditionals(self, message, data, ofs, count): + while 0 < count: + record, ofs = self._parseResourceRecord(message, data, ofs) + count -= 1 + message._additionals.append(record) + + return ofs + + def _parseResourceRecord(self, message, data, ofs): + name, ofs = self._decodeName(data, ofs) + typ, cls, ttl, rrlen = struct.unpack('!HHLH', data[ofs:ofs+10]) + ofs += 10 + record = data[ofs:ofs+rrlen] + ofs += rrlen + + return ((name, typ, cls, ttl, record), ofs) + + def _decodeName(self, data, ofs): + idx = ofs + compressed = struct.unpack('!H', data[ofs:ofs+2])[0] + + if compressed & int('1100000000000000', 2): + idx = compressed & int('0011111111111111', 2) + name = (self._ofs_names[idx], ofs+2) + else: + length = struct.unpack('B', data[ofs])[0] + parts = [] + while 0 != length: + parts.append(data[ofs+1:ofs+1+length]) + ofs += 1+length + length = struct.unpack('B', data[ofs])[0] + + name = ('.'.join(parts), ofs+1) + self._ofs_names[idx] = name[0] + + return name + +# vim: set ft=python et ts=4 sw=4 sts=4: diff --git a/lib/Protocol/Dns/__init__.py b/lib/Protocol/Dns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/Protocol/Http/Composer.py b/lib/Protocol/Http/Composer.py new file mode 100644 index 0000000..f38f904 --- /dev/null +++ b/lib/Protocol/Http/Composer.py @@ -0,0 +1,55 @@ +""" +@author Georg Hopp + +""" + +import Message + +class Composer(object): + """ + compose to HTTP + ===================================================================== + """ + def composeStartLine(self, message): + """ + compose a HTTP message StartLine... currently this does no check for + the validity of the StartLine. + + returns str The composed HTTP start line (either Request or Status) + + @message: HttpMessage The message that should be composed. + """ + return message.getStartLine() + '\r\n' + + def composeHeaders(self, message): + """ + this creates header lines for each key/value[n] pair. + + returns str All headers composed to an HTTP string. + + @message: HttpMessage The message to compose the header from. + """ + headers = message.getHeaders() + return '\r\n'.join([':'.join(h) for h in headers]) + '\r\n' + + def composeStartLineHeaders(self, message): + """ + Compose the start line and the headers. + + returns str The start line and the headers as HTTP string. + + @message: HttpMessage The message to be composed. + """ + return self.composeStartLine(message) + self.composeHeaders(message) + + def compose(self, message): + """ + Compose the whole message to an HTTP string. + + returns str The whole message as an HTTP string. + + @message: HttpMessage The message to be composed. + """ + return self.composeStartLineHeaders(message) + "\r\n" + message.getBody() + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Protocol/Http/Http.py b/lib/Protocol/Http/Http.py new file mode 100644 index 0000000..4a6af1d --- /dev/null +++ b/lib/Protocol/Http/Http.py @@ -0,0 +1,100 @@ +""" +@author Georg Hopp + +""" +from base64 import b64encode, b64decode +from hashlib import sha1 + +from ..Protocol import Protocol + +from Parser import Parser +from Composer import Composer +from Message import Message + +from Protocol.Websocket.Websocket import Websocket + +class Http(Protocol): + def __init__(self): + self.parser = Parser() + self.composer = Composer() + + def getParser(self): + return self.parser + + def getComposer(self): + return self.composer + + def createMessage(self, remote): + return Message(remote) + + def createRequest(self, method=Message.METHOD_GET, uri='/', remote=None): + request = self.createMessage(remote) + request.setRequestLine(method, uri, 'HTTP/1.1') + self._addCommonHeaders(request) + + return request + + def createResponse(self, request, code=200, resp_message='OK', remote=None): + version = request.getHttpVersion() + response = self.createMessage(remote) + response.setStateLine(version, code, resp_message) + + self._addCommonHeaders(response) + response.setHeader('Content-Length', '0') + + con_header = request.getHeader('Connection').lower() + if 'keep-alive' in con_header: + response.setHeader('Connection', 'Keep-Alive') + if 'close' in con_header: + response.setHeader('Connection', 'Close') + + return response + + def createUpgradeRequest(self, host, subprotocol=None): + """ + currently only for websocket updates + """ + request = self.createRequest() + request.setHeaders([ + ('Host', host), + ('Connection', 'Upgrade'), + ('Upgrade', 'websocket'), + ('Sec-WebSocket-Version', '13'), + ('Sec-WebSocket-Key', b64encode(''.join(chr(randint(0,255)) + for _ in range(16))))]) + if subprotocol: + request.setHeader('Sec-WebSocket-Protocol', protocol) + return request + + def createUpgradeResponse(self, request): + """ + currently only for websocket updates + """ + key = request.getHeader('Sec-WebSocket-Key') + if not key: + response = self.createResponse(request, 400, 'Bad Request') + else: + response = self.createResponse(request, 101, 'Switching Protocols') + response.setHeaders([ + ('Connection', 'Upgrade'), + ('Upgrade', 'websocket'), + ('Sec-WebSocket-Accept', b64encode(sha1(key+Websocket.WS_UUID).digest()))]) + + return response + + def upgrade(self, message): + """ + TODO decide by the message which protocol to upgrade to. + """ + return Websocket() + + + def _addCommonHeaders(self, message): + from wsgiref.handlers import format_date_time + from datetime import datetime + from time import mktime + + date = format_date_time(mktime(datetime.now().timetuple())) + message.setHeader('Date', date) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Protocol/Http/Message.py b/lib/Protocol/Http/Message.py new file mode 100644 index 0000000..624aea1 --- /dev/null +++ b/lib/Protocol/Http/Message.py @@ -0,0 +1,273 @@ +""" +@author Georg Hopp + +""" +from ..Message import Message as BaseMessage + +class Message(BaseMessage): + START_READY = 0x01 + HEADERS_READY = 0x02 + BODY_READY = 0x04 + + METHODS = ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT') + METHOD_OPTIONS = METHODS.index('OPTIONS') + METHOD_GET = METHODS.index('GET') + METHOD_HEAD = METHODS.index('HEAD') + METHOD_POST = METHODS.index('POST') + METHOD_PUT = METHODS.index('PUT') + METHOD_DELETE = METHODS.index('DELETE') + METHOD_TRACE = METHODS.index('TRACE') + METHOD_CONNECT = METHODS.index('CONNECT') + + def __init__(self, remote): + super(Message, self).__init__(remote) + self.state = 0 + + self._chunk_size = 0 + self._chunked = False + + self._headers = {} + self._body = '' + + self._http = None + self._method = None + self._uri = None + self._code = None + self._message = None + + """ + cleaner + ===================================================================== + """ + def resetStartLine(self): + self._http = None + self._uri = None + self._code = None + self._message = None + self.state &= ~Message.START_READY + + def resetHeaders(self): + self._headers = {} + self.state &= ~Message.HEADERS_READY + + def resetBody(self): + self._body = '' + self.state &= ~Message.BODY_READY + self._chunked = False + self._chunk_size = 0 + + def reset(self): + self.resetStartLine() + self.resetHeaders() + self.resetBody() + + def removeHeadersByKey(self, key): + """ + Remove HTTP headers to a given key. This will remove all headers right + now associated to that key. Keys are alwasys stored lower case and + cenverted to title case during composition. + + returns None + + @key: str The header key to remove. + """ + if key.lower() in self._headers: + del(self._headers[key.lower()]) + + def removeHeader(self, header): + """ + Remove a header. + + returns None + + @header: tuple Holds key and value of the header to remove. + """ + key = header[0].lower() + if key in self._headers: + if header[1] in self._headers[key]: + self._headers[key].remove(header[1]) + + """ + setter + ===================================================================== + """ + def setRequestLine(self, method, uri, http): + if self.isResponse(): + raise Exception('try to make a request from a response') + self._method = method + self._uri = uri + self._http = http + + def setStateLine(self, http, code, message): + if self.isRequest(): + raise Exception('try to make a response from a request') + self._http = http + self._code = code + self._message = message + + def setHeader(self, key, value): + """ + Add a header to the message. + Under some circumstances HTTP allows to have multiple headers with + the same key. Thats the reason why the values are handled in a list + here. + + Returns None + + key: The header key (The part before the colon :). + value: The header value (The part behind the colon :). + Value might also be a list a values for this key. + """ + key = key.lower() + if key in self._headers: + self._headers[key] += [v.strip() for v in value.split(',') + if v.strip() not in self._headers[key]] + else: + self._headers[key.lower()] = [v.strip() for v in value.split(',')] + + def replaceHeader(self, key, value): + self._headers[key.lower()] = [v.strip() for v in value.split(',')] + + def setHeaders(self, headers): + """ + This sets a bunch of headers at once. It will add the headers and not + override anything. It is neccessary to clear the headers before calling + this if only the headers given here should be in the message. + + Returns None + + headers: Either a list of tuples [(key,value),...] or + a dictionary {key:value,...}. + In both cases the values should be a list again. + """ + if type(headers) == dict: + headers = headers.items() + + for h in headers: + self.setHeader(h[0], h[1]) + + def setBody(self, data): + """ + Set the body of a message. Currently we do not support sending + chunked message so this is simple... + + Returns None + + data: The data to set in the message body. + """ + self.replaceHeader('Content-Length', '%d'%len(data)) + self._body = data + + """ + getter + ===================================================================== + """ + def getHttpVersion(self): + return self._http + + def getMethod(self): + return self._method + + def getUri(self): + return self._uri + + def getResponseCode(self): + return self._code + + def getResponseMessage(self): + return self._message + + def getStartLine(self): + line = '' + if self.isRequest(): + method = Message.METHODS[self._method] + line = ' '.join((method, self._uri, self._http)) + elif self.isResponse(): + line = ' '.join((self._http, str(self._code), self._message)) + return line + + def getHeaders(self): + return [(k, self.getHeader(k)) for k in self._headers] + + def getHeader(self, key): + """ + Get all values currently associated to this header key. + + returns list All values to the given key. + + @key: str The key to get values for. + """ + key = key.lower() + if key not in self._headers: return '' + return ', '.join(self._headers[key]) + + def getBody(self): + return self._body + + + """ + checker + ===================================================================== + """ + def headerKeyExists(self, key): + return key.lower() in self._headers + + def startlineReady(self): + return Message.START_READY == self.state & Message.START_READY + + def headersReady(self): + return Message.HEADERS_READY == self.state & Message.HEADERS_READY + + def bodyReady(self): + return Message.BODY_READY == self.state & Message.BODY_READY + + def ready(self): + return self.headersReady() and self.bodyReady() + + def isRequest(self): + return self._method is not None + + def isResponse(self): + return self._code is not None + + def isCloseMessage(self): + if self.isRequest(): + # HTTP always expects a response to be send, so a request is + # never the close message. + return False + else: + con_header = self.getHeader('Connection').lower() + if self._http == 'HTTP/1.0': + return 'keep-alive' not in con_header + else: + return 'close' in con_header + + def isUpgradeMessage(self): + con_header = self.getHeader('Connection').lower() + return 'upgrade' in con_header + + def isOptions(self): + return Message.METHOD_OPTIONS == self.getMethod() + + def isGet(self): + return Message.METHOD_GET == self.getMethod() + + def isHead(self): + return Message.METHOD_HEAD == self.getMethod() + + def isPost(self): + return Message.METHOD_POST == self.getMethod() + + def isPut(self): + return Message.METHOD_PUT == self.getMethod() + + def isDelete(self): + return Message.METHOD_DELETE == self.getMethod() + + def isTrace(self): + return Message.METHOD_TRACE == self.getMethod() + + def isConnect(self): + return Message.METHOD_CONNECT == self.getMethod() + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Protocol/Http/Parser.py b/lib/Protocol/Http/Parser.py new file mode 100644 index 0000000..1f5c7d2 --- /dev/null +++ b/lib/Protocol/Http/Parser.py @@ -0,0 +1,169 @@ +""" +@author Georg Hopp + +""" + +import re +from Message import Message + +class Parser(object): + def __init__(self): + self._header_exp = re.compile(r"([^:]+):(.+)\r\n") + self._chunk_exp = re.compile(r"([\da-f]+).*\r\n") + self._req_exp = re.compile( + r".*(%s) +([^ ]+) +(HTTP/\d\.\d)\r\n"%'|'.join(Message.METHODS)) + self._state_exp = re.compile(r".*(HTTP/\d\.\d) *(\d{3}) *(.*)\r\n") + + def parse(self, message, data): + """ + Parse data into this message. + + Returns 0 when the Message is already complete or the amount of the + successfully parsed data. + + @message: An HttpMessage instance where the data is parsed into. + @data: The data to be parsed. + """ + end = 0 + + if 0 == message.state: + if message.isRequest() or message.isResponse(): + message.reset() + end += self.parseStartLine(message, data) + + if message.startlineReady() and not message.headersReady(): + end += self.parseHeaders(message, data[end:]) + + if message.headersReady() and not message.bodyReady(): + end += self.parseBody(message, data[end:]) + + return end + + def parseStartLine(self, message, data): + """ + Parse data into the HTTP message startline, either a Request- or a + Statusline. This will set the message start_line if the given data + matches the start_exp expression. In that case it will also set + the start_ready flag. + + Returns the position of the data that is not parsed. + + @message: An HttpMessage instance where the data is parsed into. + @data: The data to be parsed. + """ + end = 0 + + match = self._parseRequest(message, data) + if match: end = match.end() + + match = self._parseResponse(message, data) + if match: end = match.end() + + if 0 != end: + message.state |= Message.START_READY + else: + end = self._checkInvalid(message, data[end:]) + + return end + + def parseHeaders(self, message, data): + """ + Parse data into the headers of a message. + + Returns the position of the data that is not parsed. + + @message: An HttpMessage instance where the data is parsed into. + @data: The data to be parsed. + """ + end = 0 + + match = self._header_exp.match(data[end:]) + while match and "\r\n" != data[end:end+2]: + message.setHeader(match.group(1).strip(), match.group(2).strip()) + end += match.end() + match = self._header_exp.match(data[end:]) + + if "\r\n" == data[end:end+2]: + # a single \r\n at the beginning indicates end of headers. + if message.headerKeyExists('Content-Length'): + message._chunk_size = int(message.getHeader('Content-Length')) + elif message.headerKeyExists('Transfer-Encoding') and \ + 'chunked' in message.getHeader('Transfer-Encoding'): + message._chunked = True + else: + message.state |= Message.BODY_READY + + message.state |= Message.HEADERS_READY + end += 2 + else: + end += self._checkInvalid(message, data[end:]) + + return end + + def parseBody(self, message, data): + """ + Parse data into the body of a message. This is also capable of + handling chunked bodies as defined for HTTP/1.1. + + Returns the position of the data that is not parsed. + + @message: An HttpMessage instance where the data is parsed into. + @data: The data to be parsed. + """ + readlen = 0 + + if message._chunked and 0 == message._chunk_size: + match = self._chunk_exp.match(data) + + if match is None: + return 0 + + message._chunk_size = int(match.group(1), 16) + readlen += match.end() + data = data[match.end():] + + if 0 == self._chunk_size: + message.state |= Message.BODY_READY + return readlen + 2 + + available_data = len(data[0:message._chunk_size]) + message._chunk_size -= available_data + readlen += available_data + message._body += data[0:available_data] + + if 0 == message._chunk_size: + if not message._chunked: + message.state |= Message.BODY_READY + return readlen + else: + readlen += 2 + + return readlen + + def _parseRequest(self, message, data): + match = self._req_exp.search(data) + if match: + message._method = Message.METHODS.index(match.group(1)) + message._uri = match.group(2) + message._http = match.group(3) + return match + + def _parseResponse(self, message, data): + match = self._state_exp.search(data) + if match: + message._http = match.group(1) + message._code = int(match.group(2)) + message._message = match.group(3) + return match + + def _checkInvalid(self, message, data): + end = 0 + nl = data.find("\r\n") + if -1 != nl: + # We received an invalid message...ignore it and start again + # TODO This should be logged. + message.reset() + end = nl + 2 + return end + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Protocol/Http/__init__.py b/lib/Protocol/Http/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/Protocol/Message.py b/lib/Protocol/Message.py new file mode 100644 index 0000000..2b75b93 --- /dev/null +++ b/lib/Protocol/Message.py @@ -0,0 +1,12 @@ +""" +@author Georg Hopp + +""" +class Message(object): + def __init__(self, remote): + self._remote = remote + + def getRemote(self): + return self._remote + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Protocol/Protocol.py b/lib/Protocol/Protocol.py new file mode 100644 index 0000000..5412dc2 --- /dev/null +++ b/lib/Protocol/Protocol.py @@ -0,0 +1,7 @@ +""" +@author: Georg Hopp +""" +class Protocol(object): + pass + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Protocol/Websocket/Composer.py b/lib/Protocol/Websocket/Composer.py new file mode 100644 index 0000000..6fe13a1 --- /dev/null +++ b/lib/Protocol/Websocket/Composer.py @@ -0,0 +1,22 @@ +""" +@author Georg Hopp + +""" + +import struct + +class Composer(object): + def compose(self, message): + """ + for now I only encode messages of len less than 126 and + final...this is just for testing. + """ + msglen = len(message) + if msglen > 125: + raise Exception('messages bigger than 125 bytes not supported') + + frame = struct.pack('BB%ds'%msglen, int('10000010', 2), msglen, message) + + return frame + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Protocol/Websocket/Message.py b/lib/Protocol/Websocket/Message.py new file mode 100644 index 0000000..9e142d1 --- /dev/null +++ b/lib/Protocol/Websocket/Message.py @@ -0,0 +1,21 @@ +""" +@author Georg Hopp + +""" +from ..Message import Message as BaseMessage + +class Message(BaseMessage): + def __init__(self, remote): + super(Message, self).__init__(remote) + _data = None + + def getData(self): + return self._data + + def setData(self, data): + self._data = data + + def ready(self): + return True + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Protocol/Websocket/Parser.py b/lib/Protocol/Websocket/Parser.py new file mode 100644 index 0000000..b5402c9 --- /dev/null +++ b/lib/Protocol/Websocket/Parser.py @@ -0,0 +1,13 @@ +""" +@author Georg Hopp + +""" + +class Parser(object): + def __init__(self): + pass + + def parse(self, message, data): + return len(data) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Protocol/Websocket/Websocket.py b/lib/Protocol/Websocket/Websocket.py new file mode 100644 index 0000000..10d7073 --- /dev/null +++ b/lib/Protocol/Websocket/Websocket.py @@ -0,0 +1,39 @@ +""" +Websocket protocol + +Author: Georg Hopp +""" +from random import seed, randint +from base64 import b64encode, b64decode +from hashlib import sha1 + +from ..Protocol import Protocol + +from Parser import Parser +from Composer import Composer +from Message import Message + +class Websocket(Protocol): + WS_UUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + + @staticmethod + def isHandshake(request): + con = request.getHeader('Connection').lower() + up = request.getHeader('Upgrade').lower() + + return 'upgrade' in con and 'websocket' in up + + def __init__(self): + self._parser = Parser() + self._composer = Composer() + + def getParser(self): + return self._parser + + def getComposer(self): + return self._composer + + def createMessage(self, remote=None): + return Message(remote) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Protocol/Websocket/__init__.py b/lib/Protocol/Websocket/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/Protocol/__init__.py b/lib/Protocol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/Server.py b/lib/Server.py new file mode 100644 index 0000000..6530d56 --- /dev/null +++ b/lib/Server.py @@ -0,0 +1,48 @@ +import time + +from Event.EventDispatcher import EventDispatcher +from Event.EventHandler import EventHandler +import Event.Signal as Signal + +from Communication.Manager import CommunicationManager +from Communication.EndPoint import CommunicationEndPoint +from Communication.ConnectEntryPoint import ConnectEntryPoint +from Communication.DatagramEntryPoint import DatagramEntryPoint +from Communication.ProtocolHandler import ProtocolHandler +from Communication.Connector import Connector + +from Transport.IoHandler import IoHandler +from Transport.TcpSocket import TcpSocket +from Transport.UdpSocket import UdpSocket + +class Server(object): + def __init__(self, application): + self._con_mngr = CommunicationManager() + self._dispatcher = EventDispatcher() + + self._dispatcher.registerHandler(self._con_mngr) + self._dispatcher.registerHandler(Connector()) + self._dispatcher.registerHandler(IoHandler()) + self._dispatcher.registerHandler(ProtocolHandler()) + self._dispatcher.registerHandler(application) + Signal.initSignals(self._dispatcher) + + def addEndpoint(self, endpoint): + self._con_mngr.addEndPoint(endpoint) + + def bindTcp(self, ip, port, protocol): + self.addEndpoint(ConnectEntryPoint(TcpSocket(ip, port), protocol)) + + def bindUdp(self, ip, port, protocol): + self.addEndpoint(DatagramEntryPoint(UdpSocket(ip, port), protocol)) + + def addHandler(self, handler): + self._dispatcher.registerHandler(handler) + + def start(self, heartbeat = None): + if heartbeat: + self._dispatcher.setHeartbeat(heartbeat) + + self._dispatcher.start(None) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/SimpleClient.py b/lib/SimpleClient.py new file mode 100644 index 0000000..5d7223c --- /dev/null +++ b/lib/SimpleClient.py @@ -0,0 +1,83 @@ +import time + +from Event.EventDispatcher import EventDispatcher, CLIENT +from Event.EventHandler import EventHandler +import Event.Signal as Signal + +from Communication.Manager import CommunicationManager +from Communication.EndPoint import CommunicationEndPoint +from Communication.ProtocolHandler import ProtocolHandler +from Transport.IoHandler import IoHandler +from Transport.TcpSocket import TcpSocket + +class SimpleClient(EventHandler): + def __init__(self, end_point): + super(SimpleClient, self).__init__() + + self._event_methods = { + EventDispatcher.eventId('user_wait') : self._userInteraction, + CommunicationEndPoint.eventId('new_msg') : self._handleData + } + + self._end_point = end_point + if isinstance(self._end_point.getTransport(), TcpSocket): + self._end_point.getTransport().connect() + + self._remote_addr = end_point.getTransport().getAddr() + + con_mngr = CommunicationManager() + con_mngr.addEndPoint(self._end_point) + + dispatcher = EventDispatcher(CLIENT) + dispatcher.registerHandler(con_mngr) + dispatcher.registerHandler(IoHandler()) + dispatcher.registerHandler(ProtocolHandler()) + dispatcher.registerHandler(self) + Signal.initSignals(dispatcher) + + self._timeout = None + self._starttime = None + self._request = None + self._response = None + self._sendIssued = False + + + def issue(self, request, timeout): + self._starttime = time.time() + self._timeout = timeout + self._request = request + self._response = None + self._sendIssued = False + self._dispatcher[0].start(None) + + return self._response + + def getRemoteAddr(self): + return self._remote_addr + + def getProtocol(self): + return self._end_point.getProtocol() + + def _userInteraction(self, event): + if self._sendIssued: + now = time.time() + + if self._response or self._timeout <= (now - self._starttime): + event.subject.stop() + else: + self.issueEvent( + event.subject, + 'data_wait', + self._timeout - (now - self._starttime) + ) + else: + self.issueEvent(self._end_point, 'send_msg', self._request) + self._sendIssued = True + return True + + def _handleData(self, event): + if event.data.isResponse(): + self._response = event.data + return True + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/ThreadedServer.py b/lib/ThreadedServer.py new file mode 100644 index 0000000..0f18491 --- /dev/null +++ b/lib/ThreadedServer.py @@ -0,0 +1,21 @@ +import time + +from Server import Server +from Event.EventThread import EventThread + +class ThreadedServer(Server): + def __init__(self, application, threads = 1): + super(ThreadedServer, self).__init__(application) + self._threads = [] + + for num in range(1, threads): + self._threads.append( + EventThread(self._dispatcher, 'th' + str(num))) + + def start(self, heartbeat = None): + for thread in self._threads: + thread.start() + + super(ThreadedServer, self).start(heartbeat) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/lib/Transport/IoHandler.py b/lib/Transport/IoHandler.py new file mode 100644 index 0000000..d3a1300 --- /dev/null +++ b/lib/Transport/IoHandler.py @@ -0,0 +1,46 @@ +""" +@author Georg Hopp + +""" +from contextlib import contextmanager + +import Transport + +from Event.EventHandler import EventHandler +from Communication.EndPoint import CommunicationEndPoint + +class IoHandler(EventHandler): + def __init__(self): + super(IoHandler, self).__init__() + + self._event_methods = { + CommunicationEndPoint.eventId('read_ready') : self._read, + CommunicationEndPoint.eventId('write_ready') : self._write + } + + @contextmanager + def _doio(self, subject, shutdown_type): + try: + yield + except Transport.Error as error: + if Transport.Error.ERR_REMOTE_CLOSE == error.errno: + self.issueEvent(subject, shutdown_type) + else: + self.issueEvent(subject, 'close') + + def _read(self, event): + with self._doio(event.subject, 'shutdown_read'): + if event.subject.bufferRead(): + self.issueEvent(event.subject, 'new_data') + + def _write(self, event): + with self._doio(event.subject, 'shutdown_write'): + if event.subject.writeBuffered(): + if event.subject.hasPendingData(): + self.issueEvent(event.subject, 'pending_data') + else: + self.issueEvent(event.subject, 'end_data') + if event.subject.shouldClose(): + self.issueEvent(event.subject, 'close') + +# vim: set ft=python et ts=4 sw=4 sts=4: diff --git a/lib/Transport/Socket.py b/lib/Transport/Socket.py new file mode 100644 index 0000000..f1a593e --- /dev/null +++ b/lib/Transport/Socket.py @@ -0,0 +1,130 @@ +""" +@author Georg Hopp + +""" + +import socket +import errno +import sys + +import Transport + +from contextlib import contextmanager + +CONTINUE = (errno.EAGAIN, errno.EWOULDBLOCK) +if 'win32' == sys.platform: + CONTINUE = CONTINUE + (errno.WSAEWOULDBLOCK) + +class Socket(object): + def __init__(self, host, port, socket_type, con_ttl=30): + self.socket = None + self._host = host + self._port = port + self._con_ttl = con_ttl + self._socket_type = socket_type + self._listen = False + self._fin_state = 0 + + def isListen(self): + return self._listen + + def isFin(self): + # TODO important, create something sane here. + return 0 != self._fin_state + + def readReady(self): + return 0 == self._fin_state & 1 + + def writeReady(self): + return 0 == self._fin_state & 2 + + def getHandle(self): + return self.socket + + def getHost(self): + return self._host + + def getPort(self): + return self._port + + def getAddr(self): + return (self._host, self._port) + + @contextmanager + def _addrinfo(self, flags=0): + for res in socket.getaddrinfo( + self._host, self._port, + socket.AF_UNSPEC, self._socket_type, + 0, flags): + af, socktype, proto, canonname, self._sa = res + + try: + if not self.socket: + self.socket = socket.socket(af, socktype, proto) + except socket.error as error: + current_exception = error + self.socket = None + continue + + try: + yield socktype + except socket.error as error: + current_exception = error + self.socket.close() + self.socket = None + continue + break + + if not self.socket: + raise Transport.Error(Transport.Error.ERR_FAILED) + + def bind(self): + with self._addrinfo(socket.AI_PASSIVE): + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind(self._sa) + self.socket.setblocking(0) + + def open(self): + with self._addrinfo(socket.AI_PASSIVE): + self.socket.setblocking(0) + + def shutdownRead(self): + try: + if 0 == self._fin_state & 1: + self.socket.shutdown(socket.SHUT_RD) + self._fin_state |= 1 + except socket.error as error: + self._fin_state |= 3 + return self._fin_state + + def shutdownWrite(self): + try: + if 0 == self._fin_state & 2: + self.socket.shutdown(socket.SHUT_WR) + self._fin_state |= 2 + except socket.error as error: + self._fin_state |= 3 + return self._fin_state + + def shutdown(self): + try: + if 0 == self._fin_state: + self.socket.shutdown(socket.SHUT_RDWR) + else: + self.shutdownRead() + self.shutdownWrite() + except socket.error as error: + pass + self._fin_state |= 3 + return self._fin_state + + def close(self): + try: + self.shutdown() + self.socket.close() + except socket.error as error: + pass + + self.socket = None + +# vim: set ft=python et ts=4 sw=4 sts=4: diff --git a/lib/Transport/TcpSocket.py b/lib/Transport/TcpSocket.py new file mode 100644 index 0000000..4e850a6 --- /dev/null +++ b/lib/Transport/TcpSocket.py @@ -0,0 +1,91 @@ +""" +@author Georg Hopp + +""" + +import errno +import socket + +import Transport +from Socket import Socket, CONTINUE + +ACC_CONTINUE = CONTINUE + ( + errno.ENETDOWN, + errno.EPROTO, + errno.ENOPROTOOPT, + errno.EHOSTDOWN, + errno.ENONET, + errno.EHOSTUNREACH, + errno.EOPNOTSUPP +) + +class TcpSocket(Socket): + def __init__(self, host, port, con_ttl=30): + super(TcpSocket, self).__init__(host, port, socket.SOCK_STREAM, con_ttl) + self.remote = None + + def bind(self): + super(TcpSocket, self).bind() + self.socket.listen(128) + self._listen = True + + def connect(self): + with self._addrinfo(): + self.socket.settimeout(self._con_ttl) + self.socket.connect(self._sa) + self.socket.settimeout(None) + self.socket.setblocking(0) + + def accept(self): + try: + con, remote = self.socket.accept() + except socket.error as error: + if error.errno not in ACC_CONTINUE: + raise Transport.Error(Transport.Error.ERR_FAILED) + return None + + try: + host, port = con.getpeername() + except Exception as error: + # Here we should destinguish the addr_family... + # Port is only available for INET and INET6 but not for UNIX. + # Currently I don't support UNIX so i don't change it now. + host = addr[0] + port = addr[1] + + con.setblocking(0) + newsock = type(self)(host, port, self._con_ttl) + newsock.socket = con + newsock.remote = remote + + return newsock + + def recv(self, size): + data = '' + try: + data = self.socket.recv(size) + except socket.error as error: + if error.errno not in CONTINUE: + raise Transport.Error(Transport.Error.ERR_FAILED) + return None + + if not data: + raise Transport.Error(Transport.Error.ERR_REMOTE_CLOSE) + + return (data, self.remote) + + def send(self, data, remote=None): + send = 0 + try: + if self.socket: + send = self.socket.send(data) + except socket.error as error: + if error.errno not in CONTINUE: + if error.errno == errno.ECONNRESET: + raise Transport.Error(Transport.Error.ERR_REMOTE_CLOSE) + else: + raise Transport.Error(Transport.Error.ERR_FAILED) + + return send + +# vim: set ft=python et ts=4 sw=4 sts=4: diff --git a/lib/Transport/Transport.py b/lib/Transport/Transport.py new file mode 100644 index 0000000..79480c6 --- /dev/null +++ b/lib/Transport/Transport.py @@ -0,0 +1,24 @@ +""" +Common things for all possible transports... +Currently our only transport is TCP but in theory there might be others... + +Author: Georg Hopp +""" + +class Error(Exception): + """ + This simplifies all the possible transport problems down to two cases. + Either the transport has failed completely or the remote side has shutdown + it's endpoint for the operation we are attemting. + """ + ERR_FAILED = 1 + ERR_REMOTE_CLOSE = 2 + + messages = { + ERR_FAILED : 'transport operation failed', + ERR_REMOTE_CLOSE : 'remote endpoint closed' + } + + def __init__(self, errno): + super(Error, self).__init__(Error.messages[errno]) + self.errno = errno diff --git a/lib/Transport/UdpSocket.py b/lib/Transport/UdpSocket.py new file mode 100644 index 0000000..bee8d3c --- /dev/null +++ b/lib/Transport/UdpSocket.py @@ -0,0 +1,50 @@ +""" +@author Georg Hopp + +""" +import socket + +import Transport +from Socket import Socket, CONTINUE + +class UdpSocket(Socket): + def __init__(self, host, port, con_ttl=30): + super(UdpSocket, self).__init__(host, port, socket.SOCK_DGRAM, con_ttl) + + """ + TODO: recv and send are pretty similar to the TcpSocket implementation. + It might be a good idea to unify them into the Socket class. + Think about this. + At the end it seems that from the application programmer perspective + there is not really much difference between Udp and Tcp Sockets...well + I guess thats the whole idea behind the Socket API... :D + """ + def recv(self, size): + data_remote = None + try: + data_remote = self.socket.recvfrom(size) + except socket.error as error: + if error.errno not in CONTINUE: + raise Transport.Error(Transport.Error.ERR_FAILED) + return None + + if not data_remote: + raise Transport.Error(Transport.Error.ERR_REMOTE_CLOSE) + + return data_remote + + def send(self, data, remote): + send = 0 + try: + if self.socket: + send = self.socket.sendto(data, remote) + except socket.error as error: + if error.errno not in CONTINUE: + if error.errno == errno.ECONNRESET: + raise Transport.Error(Transport.Error.ERR_REMOTE_CLOSE) + else: + raise Transport.Error(Transport.Error.ERR_FAILED) + + return send + +# vim: set ft=python et ts=4 sw=4 sts=4: diff --git a/lib/Transport/__init__.py b/lib/Transport/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/TestAll.py b/tests/TestAll.py new file mode 100644 index 0000000..edec116 --- /dev/null +++ b/tests/TestAll.py @@ -0,0 +1,34 @@ +import unittest +import mock + +import TestCommunicationEndPoint +import TestConnection +import TestConnectEntryPoint +import TestConnector +import TestDatagramEntryPoint +import TestDatagramService +import TestCommunicationManager +import TestProtocolHandler +import TestEventHandler +import TestEventSubject +import TestEventDispatcher +import TestDnsClient + +suite = unittest.TestSuite() + +suite.addTest(TestCommunicationEndPoint.suite()) +suite.addTest(TestConnection.suite()) +suite.addTest(TestConnectEntryPoint.suite()) +suite.addTest(TestConnector.suite()) +suite.addTest(TestDatagramEntryPoint.suite()) +suite.addTest(TestDatagramService.suite()) +suite.addTest(TestCommunicationManager.suite()) +suite.addTest(TestProtocolHandler.suite()) +suite.addTest(TestEventHandler.suite()) +suite.addTest(TestEventSubject.suite()) +suite.addTest(TestEventDispatcher.suite()) +suite.addTest(TestDnsClient.suite()) + +unittest.TextTestRunner(verbosity=1).run(suite) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/tests/TestCommunicationEndPoint.py b/tests/TestCommunicationEndPoint.py new file mode 100644 index 0000000..4ce0117 --- /dev/null +++ b/tests/TestCommunicationEndPoint.py @@ -0,0 +1,87 @@ +import unittest +import mock + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(dirname(realpath(__file__))) + '/lib') + +from Communication.EndPoint import CommunicationEndPoint + +class TestCommunicationEndPoint(unittest.TestCase): + def setUp(self): + self._transport = mock.Mock() + self._protocol = mock.Mock() + self._bufsize = 11773 + self._endpoint = CommunicationEndPoint( + self._transport, self._protocol, self._bufsize) + + def testSetClose(self): + self.assertFalse(self._endpoint.shouldClose()) + self._endpoint.setClose() + self.assertTrue(self._endpoint.shouldClose()) + + def testHasProtocol(self): + self.assertTrue(self._endpoint.hasProtocol(mock.Mock)) + + def testHasPendingData(self): + self.assertFalse(self._endpoint.hasPendingData()) + + def testGetTransport(self): + self.assertEqual(self._endpoint.getTransport(), self._transport) + + def testGetProtocol(self): + self.assertEqual(self._endpoint.getProtocol(), self._protocol) + + def testGetHandle(self): + self._transport.getHandle.return_value = 10 + self.assertEqual(self._endpoint.getHandle(), 10) + self._transport.getHandle.assert_call_once() + + def testBufferRead(self): + self._transport.recv.return_value = False + self.assertFalse(self._endpoint.bufferRead()) + self._transport.recv.assert_call_once_with(11773) + + self._transport.reset_mock() + self._endpoint.appendReadData = mock.Mock() + + self._transport.recv.side_effect = iter(['111', '2222', '33333', False]) + self.assertTrue(self._endpoint.bufferRead()) + self._transport.recv.assert_call_with(11773) + self.assertEqual(self._transport.recv.call_count, 4) + self.assertEqual(self._endpoint.appendReadData.call_count, 3) + + def testWriteBuffered(self): + self._endpoint.nextWriteData = mock.Mock() + self._endpoint.nextWriteData.return_value = ('', 1212) + self.assertFalse(self._endpoint.writeBuffered()) + self._endpoint.nextWriteData.assert_called_once_with() + + self._endpoint.nextWriteData.reset_mock() + self._endpoint.nextWriteData.return_value = ('12345', 1212) + self._endpoint.appendWriteData = mock.Mock() + self._transport.send.return_value = 0 + self.assertFalse(self._endpoint.writeBuffered()) + self._endpoint.nextWriteData.assert_called_once_with() + self._transport.send.assert_called_once_with('12345', 1212) + + self._transport.reset_mock() + self._endpoint.nextWriteData.reset_mock() + self._endpoint.nextWriteData.side_effect = iter( + [('111222', 1212), ('333', 1313), ('', 1212)]) + self._endpoint.appendWriteData = mock.Mock() + self._transport.send.return_value = 3 + self.assertTrue(self._endpoint.writeBuffered()) + self._endpoint.nextWriteData.assert_called_with() + self._transport.send.assert_any_call('111222', 1212) + self._transport.send.assert_any_call('222', 1212) + self._transport.send.assert_called_with('333', 1313) + + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestCommunicationEndPoint) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/tests/TestCommunicationManager.py b/tests/TestCommunicationManager.py new file mode 100644 index 0000000..5e9cac4 --- /dev/null +++ b/tests/TestCommunicationManager.py @@ -0,0 +1,153 @@ +import unittest +import mock + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(dirname(realpath(__file__))) + '/lib') + +from Communication.Manager import CommunicationManager + +class TestCommunicationManager(unittest.TestCase): + def setUp(self): + self._manager = CommunicationManager() + self._endpoint = mock.Mock() + self._transport = mock.Mock() + self._event = mock.Mock() + + self._endpoint.getHandle.return_value = 123 + self._endpoint.getTransport.return_value = self._transport + self._event.subject = self._endpoint + self._manager.issueEvent = mock.Mock() + + def testEndPointAlreadyHandled(self): + """ + there really should be a test for this to ensure the if + is working correctly. + """ + pass + + def testAddEndPoint(self): + self._transport.isListen.return_value = False + self._manager._rcons = mock.Mock() + self._manager.addEndPoint(self._endpoint) + self.assertIn(123, self._manager._cons) + self.assertEqual(self._endpoint, self._manager._cons[123]) + self._manager._rcons.append.assert_called_with(123) + + def testAddListenEndPoint(self): + self._transport.isListen.return_value = True + self._manager._rcons = mock.Mock() + self._manager.addEndPoint(self._endpoint) + self.assertIn(123, self._manager._listen) + self.assertEqual(self._endpoint, self._manager._listen[123]) + self._manager._rcons.append.assert_called_with(123) + + def test_addCon(self): + self._manager.addEndPoint = mock.Mock() + self.assertTrue(self._manager._addCon(self._event)) + self._manager.addEndPoint.assert_called_once_with(self._endpoint) + + def test_enableWriteOnWriteFinTransport(self): + self._transport._fin_state = 2 + self.assertTrue(self._manager._enableWrite(self._event)) + self.assertNotIn(123, self._manager._wcons) + + def test_enableWrite(self): + self._transport._fin_state = 0 + self.assertTrue(self._manager._enableWrite(self._event)) + self.assertIn(123, self._manager._wcons) + + def test_disableWriteNoShutdownRead(self): + self._transport._fin_state = 0 + self.assertTrue(self._manager._disableWrite(self._event)) + self.assertNotIn(123, self._manager._wcons) + self.test_enableWrite() + self.assertTrue(self._manager._disableWrite(self._event)) + self.assertNotIn(123, self._manager._wcons) + + def test_disableWriteNoShutdownRead(self): + self._transport._fin_state = 1 + self.assertTrue(self._manager._disableWrite(self._event)) + self.assertNotIn(123, self._manager._wcons) + self.test_enableWrite() + self.assertTrue(self._manager._disableWrite(self._event)) + self.assertNotIn(123, self._manager._wcons) + self._manager.issueEvent.assert_called_with( + self._endpoint, 'shutdown_write') + + def test_shutdown(self): + self._transport.isListen.return_value = True + endpoint2 = mock.Mock() + transport2 = mock.Mock() + endpoint2.getTransport.return_value = transport2 + endpoint2.getHandle.return_value = 321 + transport2.isListen.return_value = False + self._manager.addEndPoint(self._endpoint) + self._manager.addEndPoint(endpoint2) + self.assertFalse(self._manager._shutdown(None)) + self._manager.issueEvent.assert_any_call(self._endpoint, 'close') + self._manager.issueEvent.assert_any_call(endpoint2, 'close') + self.assertEqual(self._manager._rcons, []) + self.assertEqual(self._manager._wcons, []) + + def test_shutdownReadReadyToClose(self): + self._manager._rcons.append(123) + self._transport.shutdownRead.return_value = 3 + self._endpoint.hasPendingData.return_value = False + self.assertFalse(self._manager._shutdownRead(self._event)) + self._manager.issueEvent.assert_called_with(self._endpoint, 'close') + + def test_shutdownReadReadyToShutdownWrite(self): + self._manager._rcons.append(123) + self._transport.shutdownRead.return_value = 0 + self._endpoint.hasPendingData.return_value = False + self.assertFalse(self._manager._shutdownRead(self._event)) + self._manager.issueEvent.assert_called_with(self._endpoint, 'shutdown_write') + + def test_shutdownReadMarkAsClose(self): + self._manager._rcons.append(123) + self._transport.shutdownRead.return_value = 0 + self._endpoint.hasPendingData.return_value = True + self.assertFalse(self._manager._shutdownRead(self._event)) + self._endpoint.setClose.assert_called_once_with() + + def test_shutdownWriteReadyToClose(self): + self._manager._wcons.append(123) + self._transport.shutdownWrite.return_value = 3 + self.assertFalse(self._manager._shutdownWrite(self._event)) + self._manager.issueEvent.assert_called_once_with(self._endpoint, 'close') + + def test_shutdownWrite(self): + self._manager._wcons.append(123) + self._transport.shutdownWrite.return_value = 0 + self.assertFalse(self._manager._shutdownWrite(self._event)) + + def test_closeCon(self): + self._manager._wcons.append(123) + self._manager._rcons.append(123) + self._manager._cons[123] = self._endpoint + self.assertFalse(self._manager._close(self._event)) + self._transport.shutdown.assert_called_with() + self._transport.close.assert_called_with() + self.assertNotIn(123, self._manager._wcons) + self.assertNotIn(123, self._manager._rcons) + self.assertNotIn(123, self._manager._cons) + + def test_closeListen(self): + self._manager._wcons.append(123) + self._manager._rcons.append(123) + self._manager._listen[123] = self._endpoint + self.assertFalse(self._manager._close(self._event)) + self._transport.shutdown.assert_called_with() + self._transport.close.assert_called_with() + self.assertNotIn(123, self._manager._wcons) + self.assertNotIn(123, self._manager._rcons) + self.assertNotIn(123, self._manager._listen) + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestCommunicationManager) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/tests/TestConnectEntryPoint.py b/tests/TestConnectEntryPoint.py new file mode 100644 index 0000000..1d905b7 --- /dev/null +++ b/tests/TestConnectEntryPoint.py @@ -0,0 +1,44 @@ +import unittest +import mock + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(dirname(realpath(__file__))) + '/lib') + +from Communication.ConnectEntryPoint import ConnectEntryPoint +from Transport import Transport + +class TestConnectEntryPoint(unittest.TestCase): + def setUp(self): + self._transport = mock.Mock() + self._protocol = mock.Mock() + self._newcon = mock.Mock() + self._entrypoint = ConnectEntryPoint(self._transport, self._protocol) + self._transport.bind.assert_called_once_with() + + def testAccept(self): + self._transport.accept.return_value = None + self.assertFalse(self._entrypoint.accept()) + + self._transport.accept.side_effect = iter( + [self._newcon, self._newcon, None]) + self.assertTrue(self._entrypoint.accept()) + + self._transport.accept.side_effect = iter( + [self._newcon, Transport.Error(1)]) + self.assertTrue(self._entrypoint.accept()) + + def testPop(self): + self.testAccept() + self.assertEqual(self._entrypoint.pop(), self._newcon) + self.assertEqual(self._entrypoint.pop(), self._newcon) + self.assertEqual(self._entrypoint.pop(), self._newcon) + self.assertEqual(self._entrypoint.pop(), None) + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestConnectEntryPoint) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/tests/TestConnection.py b/tests/TestConnection.py new file mode 100644 index 0000000..5b9dd28 --- /dev/null +++ b/tests/TestConnection.py @@ -0,0 +1,104 @@ +import unittest +import mock + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(dirname(realpath(__file__))) + '/lib') + +from Communication.Connection import Connection + +class TestConnection(unittest.TestCase): + def setUp(self): + self._message = mock.Mock() + self._transport = mock.Mock() + self._protocol = mock.Mock() + self._parser = mock.Mock() + self._composer = mock.Mock() + self._bufsize = 11773 + self._connection = Connection( + self._transport, self._protocol, self._bufsize) + + def testHasPendingData(self): + self.assertFalse(self._connection.hasPendingData()) + self._connection._write_buffer = '1234' + self.assertTrue(self._connection.hasPendingData()) + + def testIterInit(self): + self.assertEqual(self._connection.__iter__(), self._connection) + + def testMessageIterator(self): + self._transport.remote = 1212 + self._protocol.createMessage.return_value = self._message + self._protocol.getParser.return_value = self._parser + self._parser.parse.return_value = 0 + self.assertRaises(StopIteration, self._connection.next) + self._protocol.createMessage.assert_called_once_with(1212) + self._protocol.getParser.assert_called_once_with() + self._parser.parse.assert_called_once_with(self._message, '') + + self._transport.reset_mock() + self._protocol.reset_mock() + self._parser.reset_mock() + + self._connection.appendReadData(('111222333', 1212)) + self._transport.remote = 1212 + self._protocol.getParser.return_value = self._parser + self._parser.parse.return_value = 3 + self._message.ready.return_value = False + self.assertRaises(StopIteration, self._connection.next) + self._protocol.getParser.assert_called_once_with() + self._parser.parse.assert_called_once_with(self._message, '111222333') + self.assertEqual(self._message.ready.call_count, 2) + + self._transport.reset_mock() + self._protocol.reset_mock() + self._parser.reset_mock() + + self._transport.remote = 1212 + self._protocol.getParser.return_value = self._parser + self._parser.parse.return_value = 3 + self._message.ready.return_value = True + self.assertEqual(self._connection.next(), self._message) + self._protocol.createMessage.assert_called_once_with(1212) + self._protocol.getParser.assert_called_once_with() + self._parser.parse.assert_called_once_with(self._message, '222333') + self.assertEqual(self._message.ready.call_count, 2) + + def testCompose(self): + self._protocol.getComposer.return_value = self._composer + self._composer.compose.return_value = '111222333' + self.assertTrue(self._connection.compose(self._message)) + self.assertEqual(self._connection._write_buffer, '111222333') + + self._composer.compose.side_effect = Exception('BOOM!') + self.assertFalse(self._connection.compose(self._message)) + self.assertEqual(self._connection._write_buffer, '111222333') + + def testAppendReadData(self): + self._connection.appendReadData(('111', 1212)) + self.assertEqual(self._connection._read_buffer, '111') + self._connection.appendReadData(('222', 1212)) + self.assertEqual(self._connection._read_buffer, '111222') + self._connection.appendReadData(('333', 1212)) + self.assertEqual(self._connection._read_buffer, '111222333') + + def testNextWriteData(self): + self._connection._write_buffer = '111222333' + self.assertEqual(self._connection.nextWriteData(), ('111222333', None)) + self.assertEqual(self._connection._write_buffer, '') + + def testAppendWriteData(self): + self._connection.appendWriteData(('111', 1212)) + self.assertEqual(self._connection._write_buffer, '111') + self._connection.appendWriteData(('222', 1212)) + self.assertEqual(self._connection._write_buffer, '111222') + self._connection.appendWriteData(('333', 1212)) + self.assertEqual(self._connection._write_buffer, '111222333') + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestConnection) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/tests/TestConnector.py b/tests/TestConnector.py new file mode 100644 index 0000000..23d6554 --- /dev/null +++ b/tests/TestConnector.py @@ -0,0 +1,59 @@ +import unittest +import mock + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(dirname(realpath(__file__))) + '/lib') + +from Communication.Connector import Connector +from Communication.Connection import Connection +from Transport import Transport + +class TestConnector(unittest.TestCase): + def setUp(self): + self._connector = Connector() + self._connector.issueEvent = mock.Mock() + + self._event = mock.Mock() + self._endpoint = mock.Mock() + self._protocol = mock.Mock() + self._dispatcher = mock.Mock() + self._new_transp = mock.Mock() + + self._event.subject = self._endpoint + self._endpoint.getProtocol.return_value = self._protocol + + def testTransportFail(self): + self._endpoint.accept.side_effect = Transport.Error(1) + self.assertTrue(self._connector._accept(self._event)) + self._endpoint.getProtocol.assert_called_once_with() + self._endpoint.accept.called_once_with() + self._connector.issueEvent.assert_called_once_with( + self._endpoint, 'close') + + def testNoNewTransports(self): + self._endpoint.accept.return_value = False + self.assertTrue(self._connector._accept(self._event)) + self._endpoint.getProtocol.assert_called_once_with() + self._endpoint.accept.called_once_with() + self.assertFalse(self._connector.issueEvent.called) + + def testNewTransports(self): + self._endpoint.accept.return_value = True + self._endpoint.pop.side_effect = iter([self._new_transp, False]) + self.assertTrue(self._connector._accept(self._event)) + self._endpoint.getProtocol.assert_called_once_with() + self._endpoint.accept.called_once_with() + issueEvent_args = self._connector.issueEvent.call_args + self.assertNotEqual(issueEvent_args, None) + if issueEvent_args: + self.assertIsInstance(issueEvent_args[0][0], Connection) + self.assertEqual(issueEvent_args[0][1], 'new_con') + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestConnector) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/tests/TestDatagramEntryPoint.py b/tests/TestDatagramEntryPoint.py new file mode 100644 index 0000000..22ec766 --- /dev/null +++ b/tests/TestDatagramEntryPoint.py @@ -0,0 +1,25 @@ +import unittest +import mock + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(dirname(realpath(__file__))) + '/lib') + +from Communication.DatagramEntryPoint import DatagramEntryPoint + +class TestDatagramEntryPoint(unittest.TestCase): + def setUp(self): + self._transport = mock.Mock() + self._protocol = mock.Mock() + self._entrypoint = DatagramEntryPoint(self._transport, self._protocol) + + def testAny(self): + self._transport.bind.assert_called_once_with() + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestDatagramEntryPoint) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/tests/TestDatagramService.py b/tests/TestDatagramService.py new file mode 100644 index 0000000..e16ea31 --- /dev/null +++ b/tests/TestDatagramService.py @@ -0,0 +1,113 @@ +import unittest +import mock + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(dirname(realpath(__file__))) + '/lib') + +from Communication.DatagramService import DatagramService + +class TestDatagramService(unittest.TestCase): + def setUp(self): + self._transport = mock.Mock() + self._protocol = mock.Mock() + self._message = mock.Mock() + self._parser = mock.Mock() + self._composer = mock.Mock() + self._bufsize = 22655 + self._msginfo = ('111222333', 1212) + self._datagram = DatagramService( + self._transport, self._protocol, self._bufsize) + + self._protocol.getParser.return_value = self._parser + self._protocol.getComposer.return_value = self._composer + self._message.getRemote.return_value = self._msginfo[1] + + def testHasPendingData(self): + self.assertFalse(self._datagram.hasPendingData()) + self._datagram._write_buffer = '12345' + self.assertTrue(self._datagram.hasPendingData()) + + def testIterInit(self): + self.assertEqual(self._datagram.__iter__(), self._datagram) + + def testMessageIteratorNoData(self): + self.assertRaises(StopIteration, self._datagram.next) + + def testMessageIteratorCreateMessageFails(self): + self._datagram._read_buffer = mock.Mock() + self._datagram._read_buffer.popleft.return_value = self._msginfo + self._protocol.createMessage.return_value = None + self.assertRaises(StopIteration, self._datagram.next) + self._datagram._read_buffer.popleft.assert_called_once_with() + self._protocol.createMessage.assert_called_once_with(self._msginfo[1]) + + def testMessageIteratorNoDataParsed(self): + self._datagram._read_buffer = mock.Mock() + self._datagram._read_buffer.popleft.return_value = self._msginfo + self._protocol.createMessage.return_value = self._message + self._parser.parse.return_value = 0 + self.assertRaises(StopIteration, self._datagram.next) + self._datagram._read_buffer.popleft.assert_called_once_with() + self._protocol.createMessage.assert_called_once_with(self._msginfo[1]) + self._parser.parse.assert_called_once_with( + self._message, self._msginfo[0]) + + def testMessageIteratorGetMessage(self): + self._datagram._read_buffer = mock.Mock() + self._datagram._read_buffer.popleft.return_value = self._msginfo + self._protocol.createMessage.return_value = self._message + self._parser.parse.return_value = 10 + self.assertEqual(self._datagram.next(), self._message) + self._datagram._read_buffer.popleft.assert_called_once_with() + self._protocol.createMessage.assert_called_once_with(self._msginfo[1]) + self._parser.parse.assert_called_once_with( + self._message, self._msginfo[0]) + + def testComposeSuccess(self): + self._composer.compose.return_value = '111222333' + self.assertTrue(self._datagram.compose(self._message)) + self.assertIn( + ('111222333', self._msginfo[1]), + self._datagram._write_buffer) + + def testComposeFail(self): + self._composer.compose.side_effect = Exception('Boom!') + self.assertFalse(self._datagram.compose(self._message)) + self.assertFalse(self._datagram._write_buffer) + + def testAppendReadData(self): + self._datagram.appendReadData(('111', 1212)) + self.assertIn(('111', 1212), self._datagram._read_buffer) + self._datagram.appendReadData(('222', 1212)) + self.assertIn(('111', 1212), self._datagram._read_buffer) + self.assertIn(('222', 1212), self._datagram._read_buffer) + self._datagram.appendReadData(('333', 1212)) + self.assertIn(('111', 1212), self._datagram._read_buffer) + self.assertIn(('222', 1212), self._datagram._read_buffer) + self.assertIn(('333', 1212), self._datagram._read_buffer) + + def testNextWriteData(self): + self.assertEqual(self._datagram.nextWriteData(), ('', None)) + self._datagram._write_buffer.append(('111222333', 1212)) + self.assertEqual(self._datagram.nextWriteData(), ('111222333', 1212)) + self.assertNotIn(('111222333', 1212), self._datagram._write_buffer) + + def testAppendWriteData(self): + self._datagram.appendWriteData(('111', 1212)) + self.assertIn(('111', 1212), self._datagram._write_buffer) + self._datagram.appendWriteData(('222', 1212)) + self.assertIn(('111', 1212), self._datagram._write_buffer) + self.assertIn(('222', 1212), self._datagram._write_buffer) + self._datagram.appendWriteData(('333', 1212)) + self.assertIn(('111', 1212), self._datagram._write_buffer) + self.assertIn(('222', 1212), self._datagram._write_buffer) + self.assertIn(('333', 1212), self._datagram._write_buffer) + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestDatagramService) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/tests/TestDnsClient.py b/tests/TestDnsClient.py new file mode 100644 index 0000000..35d934d --- /dev/null +++ b/tests/TestDnsClient.py @@ -0,0 +1,34 @@ +import struct +import unittest +import mock + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(dirname(realpath(__file__))) + '/lib') + +from DnsClient import DnsClient + +class TestDnsClient(unittest.TestCase): + def setUp(self): + self._remote_addr = ('10.1.0.10', 1212) + + self._client = DnsClient(self._remote_addr[0], self._remote_addr[1]) + self._client._client = mock.Mock() + self._client._proto = mock.Mock() + + def testGetIp(self): + request = mock.Mock() + response = mock.Mock() + response._answers = [('foo', 1, 1, 15, '\x01\x02\x03\x04')] + self._client._proto.createRequest.return_value = request + self._client._client.getRemoteAddr.return_value = self._remote_addr + self._client._client.issue.return_value = response + self.assertEqual(self._client.getIp('foo'), '1.2.3.4') + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestDnsClient) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/tests/TestEventDispatcher.py b/tests/TestEventDispatcher.py new file mode 100644 index 0000000..04ad781 --- /dev/null +++ b/tests/TestEventDispatcher.py @@ -0,0 +1,54 @@ +import unittest +import mock + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(dirname(realpath(__file__))) + '/lib') + +from Event.Event import Event +from Event.EventDispatcher import EventDispatcher + +class TestEventDisptcher(unittest.TestCase): + def setUp(self): + self._dispatcher = EventDispatcher() + self._handler_mock1 = mock.Mock() + self._handler_mock2 = mock.Mock() + + self._handler_mock1.getHandledIds.return_value = [1, 2] + self._handler_mock2.getHandledIds.return_value = [1, 3] + + def testRegisterHandler(self): + self._dispatcher.registerHandler(self._handler_mock1) + self._dispatcher.registerHandler(self._handler_mock2) + + self._handler_mock1.getHandledIds.called_once() + self._handler_mock2.getHandledIds.called_once() + + self._handler_mock1.setDispatcher.called_once() + self._handler_mock2.setDispatcher.called_once() + + self.assertIn(1, self._dispatcher._handler) + self.assertIn(2, self._dispatcher._handler) + self.assertIn(3, self._dispatcher._handler) + self.assertNotIn(4, self._dispatcher._handler) + self.assertIn(self._handler_mock1, self._dispatcher._handler[1]) + self.assertIn(self._handler_mock2, self._dispatcher._handler[1]) + self.assertIn(self._handler_mock1, self._dispatcher._handler[2]) + self.assertNotIn(self._handler_mock2, self._dispatcher._handler[2]) + self.assertIn(self._handler_mock2, self._dispatcher._handler[3]) + + def testSetHeartbeat(self): + self._dispatcher.setHeartbeat(None) + self.assertEqual(self._dispatcher._heartbeat, None) + self.assertEqual(self._dispatcher._nextbeat, 0.0) + self._dispatcher.setHeartbeat(1.0) + self.assertEqual(self._dispatcher._heartbeat, 1.0) + self.assertNotEqual(self._dispatcher._nextbeat, 0.0) + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestEventDisptcher) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/tests/TestEventHandler.py b/tests/TestEventHandler.py new file mode 100644 index 0000000..95ee1c6 --- /dev/null +++ b/tests/TestEventHandler.py @@ -0,0 +1,82 @@ +import unittest +import mock + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(dirname(realpath(__file__))) + '/lib') + +from Event.EventHandler import EventHandler + +class HandlerOne(EventHandler): + def __init__(self): + super(HandlerOne, self).__init__() + + self._event_methods = { + 1 : self._handleOne, + 2 : self._handleTwo } + + def _handleOne(self, event): + return 'one' + + def _handleTwo(self, event): + return 'two' + +class TestEventHandler(unittest.TestCase): + def setUp(self): + self._handler = EventHandler() + self._handler_one = HandlerOne() + + self._event_mock1 = mock.Mock() + self._event_mock2 = mock.Mock() + self._event_source_mock = mock.Mock() + self._dispatcher_mock1 = mock.Mock() + self._dispatcher_mock2 = mock.Mock() + + self._event_mock1.name = 'a' + self._event_mock1.type = 1 + self._event_mock1.subject = self._event_source_mock + self._event_mock1.data = None + + self._event_mock2.name = 'b' + self._event_mock2.type = 2 + self._event_mock2.subject = self._event_source_mock + self._event_mock2.data = 'arbitrary data' + + self._event_source_mock.emit.return_value = self._event_mock1 + + def testEmptyHandlerSetDispatcher(self): + self._handler.setDispatcher(self._dispatcher_mock1) + self._handler.setDispatcher(self._dispatcher_mock2) + self.assertIn(self._dispatcher_mock1, self._handler._dispatcher) + self.assertIn(self._dispatcher_mock2, self._handler._dispatcher) + + def testEmptyHandlerGetHandledIds(self): + self.assertEqual(self._handler.getHandledIds(), []) + + def testEmptyHandlerNoDispatcherIssueEvent(self): + self._handler.issueEvent(self._event_source_mock, 'a', None) + self._event_source_mock.emit.assert_called_once_with('a', None) + + def testEmptyHandlerIssueEvent(self): + self._handler.setDispatcher(self._dispatcher_mock1) + self._handler.setDispatcher(self._dispatcher_mock2) + self._handler.issueEvent(self._event_source_mock, 'a', None) + self._event_source_mock.emit.assert_called_once_with('a', None) + self._dispatcher_mock1.queueEvent.called_once_with(self._event_mock1) + self._dispatcher_mock2.queueEvent.called_once_with(self._event_mock1) + + def testEmptyHandlerHandleEvent(self): + self.assertFalse(self._handler.handleEvent(self._event_mock1)) + self.assertFalse(self._handler.handleEvent(self._event_mock2)) + + def testHandlerOneHandleEvent(self): + self.assertEqual(self._handler_one.handleEvent(self._event_mock1), 'one') + self.assertEqual(self._handler_one.handleEvent(self._event_mock2), 'two') + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestEventHandler) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/tests/TestEventSubject.py b/tests/TestEventSubject.py new file mode 100644 index 0000000..a8970c0 --- /dev/null +++ b/tests/TestEventSubject.py @@ -0,0 +1,76 @@ +import unittest +import mock + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(dirname(realpath(__file__))) + '/lib') + +from Event.Event import Event +from Event.EventSubject import EventSubject + +class EventSubject1(EventSubject): + _EVENTS = { + 'a': 0x01 + } + +class EventSubject2(EventSubject1): + _EVENTS = { + 'b': 0x01 + } + +class EventSubject3(EventSubject): + _EVENTS = { + 'a': 0x01 + } + +class TestEventSubject(unittest.TestCase): + def setUp(self): + self._subject = EventSubject() + self._subject1 = EventSubject1() + self._subject2 = EventSubject2() + self._subject3 = EventSubject3() + + def testEventId(self): + self.assertEqual(EventSubject().eventId('a'), None) + self.assertNotEqual(EventSubject1().eventId('a'), None) + self.assertNotEqual(EventSubject2().eventId('a'), None) + self.assertNotEqual(EventSubject2().eventId('b'), None) + self.assertNotEqual(EventSubject2.eventId('a'), EventSubject2.eventId('b')) + self.assertNotEqual(EventSubject1.eventId('a'), EventSubject3.eventId('a')) + + def testEmit(self): + event = self._subject1.emit('a', None) + self.assertEqual(event.name, 'a') + self.assertNotEqual(event.type, None) + self.assertEqual(event.subject, self._subject1) + self.assertEqual(event.data, None) + self.assertEqual(event.sno, 0) + + event = self._subject1.emit('b', None) + self.assertEqual(event.name, 'b') + self.assertEqual(event.type, None) + self.assertEqual(event.subject, self._subject1) + self.assertEqual(event.data, None) + self.assertEqual(event.sno, 1) + + event = self._subject2.emit('a', None) + self.assertEqual(event.name, 'a') + self.assertNotEqual(event.type, None) + self.assertEqual(event.subject, self._subject2) + self.assertEqual(event.data, None) + self.assertEqual(event.sno, 2) + + event = self._subject2.emit('b', 'data') + self.assertEqual(event.name, 'b') + self.assertNotEqual(event.type, None) + self.assertEqual(event.subject, self._subject2) + self.assertEqual(event.data, 'data') + self.assertEqual(event.sno, 3) + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestEventSubject) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/tests/TestProtocolHandler.py b/tests/TestProtocolHandler.py new file mode 100644 index 0000000..2c38b9e --- /dev/null +++ b/tests/TestProtocolHandler.py @@ -0,0 +1,102 @@ +import unittest +import mock + +from os.path import dirname, realpath +from sys import argv, path +path.append(dirname(dirname(realpath(__file__))) + '/lib') + +from Communication.ProtocolHandler import ProtocolHandler + +class TestProtocolHandler(unittest.TestCase): + def setUp(self): + self._protocol_handler = ProtocolHandler() + + self._endpoint = mock.Mock() + self._message = mock.Mock() + self._event = mock.Mock() + self._protocol = mock.Mock() + self._protocol_handler.issueEvent = mock.Mock() + + self._event.subject = self._endpoint + self._endpoint.__iter__ = mock.Mock(return_value=iter([self._message])) + self._endpoint.getProtocol.return_value = self._protocol + + def test_parseCloseResponse(self): + self._message.isCLoseMessage.return_value = True + self._message.isResponse.return_value = True + self._protocol_handler._parse(self._event) + self._protocol_handler.issueEvent.assert_called_with( + self._endpoint, 'new_msg', self._message) + self._endpoint.setClose.assert_called_with() + + def test_parseCloseRequest(self): + self._message.isCLoseMessage.return_value = True + self._message.isResponse.return_value = False + self._protocol_handler._parse(self._event) + self._protocol_handler.issueEvent.assert_called_with( + self._endpoint, 'new_msg', self._message) + + def test_parseUpgradeResponse(self): + self._message.isCloseMessage.return_value = False + self._message.isUpgradeMessage.return_value = True + self._message.isRequest.return_value = False + self._protocol_handler._parse(self._event) + self._protocol_handler.issueEvent.assert_called_with( + self._endpoint, 'upgrade', self._message) + + def test_parseUpgradeRequest(self): + response = mock.Mock() + self._protocol.createUpgradeResponse.return_value = response + + self._message.isCloseMessage.return_value = False + self._message.isUpgradeMessage.return_value = True + self._message.isRequest.return_value = True + self._protocol_handler._parse(self._event) + self._protocol_handler.issueEvent.assert_called_with( + self._endpoint, 'send_msg', response) + + def test_parseNormalMessage(self): + self._message.isCloseMessage.return_value = False + self._message.isUpgradeMessage.return_value = False + self._protocol_handler._parse(self._event) + self._protocol_handler.issueEvent.assert_called_with( + self._endpoint, 'new_msg', self._message) + + def test_composeRequest(self): + self._event.data = self._message + self._endpoint.compose.return_value = True + self._message.isResponse.return_value = False + self._protocol_handler._compose(self._event) + self._protocol_handler.issueEvent.assert_called_with( + self._endpoint, 'write_ready') + + def test_composeUpgradeResponse(self): + self._event.data = self._message + self._endpoint.compose.return_value = True + self._message.isResponse.return_value = True + self._message.isUpgradeMessage.return_value = True + self._message.isCloseMessage.return_value = False + self._protocol_handler._compose(self._event) + self._protocol_handler.issueEvent.assert_any_call( + self._endpoint, 'write_ready') + self._protocol_handler.issueEvent.assert_any_call( + self._endpoint, 'upgrade', self._message) + + def test_composeCloseResponse(self): + self._event.data = self._message + self._endpoint.compose.return_value = True + self._message.isResponse.return_value = True + self._message.isUpgradeMessage.return_value = False + self._message.isCloseMessage.return_value = True + self._protocol_handler._compose(self._event) + self._protocol_handler.issueEvent.assert_called_with( + self._endpoint, 'write_ready') + self._endpoint.setClose.assert_called_once_with() + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(TestProtocolHandler) + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite()) + +# vim: set ft=python et ts=8 sw=4 sts=4: diff --git a/websocket.html b/websocket.html new file mode 100644 index 0000000..019d3ff --- /dev/null +++ b/websocket.html @@ -0,0 +1,120 @@ + + + Websocket test + + + +

Websocket Test

+
Websockets are not supported
+
Server time
+ + + + + +