diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..94487b951 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/.project b/.project new file mode 100644 index 000000000..1b1ea2cbe --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + netium + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 000000000..c8b3d370e --- /dev/null +++ b/.pydevproject @@ -0,0 +1,8 @@ + + + +/${PROJECT_DIR_NAME}/src + +python 2.6 +Default + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 000000000..998922fa3 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=utf-8 diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..7d19923cb --- /dev/null +++ b/setup.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Pingu System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Pingu System. +# +# Hive Pingu System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Pingu System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Pingu System. If not, see . + +__author__ = "João Magalhães " +""" The author(s) of the module """ + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import os +import glob +import setuptools + +setuptools.setup( + name = "netium", + version = "0.1.0", + author = "Hive Solutions Lda.", + author_email = "development@hive.pt", + description = "Netium System", + license = "GNU General Public License (GPL), Version 3", + keywords = "netium net infrastructure", + url = "http://netium.com", + packages = [ + "netium" + ], + classifiers = [ + "Development Status :: 3 - Alpha", + "Topic :: Utilities", + "License :: OSI Approved :: GNU General Public License (GPL)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7" + ] +) diff --git a/src/netium/__init__.py b/src/netium/__init__.py new file mode 100644 index 000000000..bfc73c930 --- /dev/null +++ b/src/netium/__init__.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import base + +from base import * diff --git a/src/netium/base/__init__.py b/src/netium/base/__init__.py new file mode 100644 index 000000000..1171a8da0 --- /dev/null +++ b/src/netium/base/__init__.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import client +import common +import conn +import observer +import server + +from client import * +from common import * +from conn import * +from observer import * +from server import * diff --git a/src/netium/base/client.py b/src/netium/base/client.py new file mode 100644 index 000000000..b867523a6 --- /dev/null +++ b/src/netium/base/client.py @@ -0,0 +1,241 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__author__ = "João Magalhães joamag@hive.pt>" +""" The author(s) of the module """ + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import socket + +from common import * #@UnusedWildImport + +class Client(Base): + + def __init__(self, name = None, handler = None, *args, **kwargs): + Base.__init__(self, name = name, hadler = handler, *args, **kwargs) + self.pendings = [] + self._pending_lock = threading.RLock() + + def ticks(self): + self.set_state(STATE_TICK) + if self.pendings: self._connects() + + def reads(self, reads): + self.set_state(STATE_READ) + for read in reads: + self.on_read(read) + + def writes(self, writes): + self.set_state(STATE_WRITE) + for write in writes: + self.on_write(write) + + def errors(self, errors): + self.set_state(STATE_ERRROR) + for error in errors: + self.on_error(error) + + def connect(self, host, port, ssl = False, key_file = None, cer_file = None): + key_file = key_file or SSL_KEY_PATH + cer_file = cer_file or SSL_CER_PATH + + _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + _socket.setblocking(0) + + if ssl: _socket = self._ssl_wrap( + _socket, + key_file = key_file, + cer_file = cer_file, + server = False + ) + + _socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + _socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + _socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + hasattr(socket, "SO_REUSEPORT") and\ + _socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) #@UndefinedVariable + + address = (host, port) + + connection = self.new_connection(_socket, address, ssl = ssl) + self.pendings.append(connection) + + return connection + + def on_read(self, _socket): + connection = self.connections_m.get(_socket, None) + if not connection: return + if not connection.status == OPEN: return + + try: + # verifies if there's any pending operations in the + # socket (eg: ssl handshaking) and performs them trying + # to finish them, in they are still pending at the current + # state returns immediately (waits for next loop) + if self._pending(_socket): return + + # iterates continuously trying to read as much data as possible + # when there's a failure to read more data it should raise an + # exception that should be handled properly + while True: + data = _socket.recv(CHUNK_SIZE) + if data: self.on_data(connection, data) + else: self.on_connection_d(connection); break + except ssl.SSLError, error: + error_v = error.args[0] + if not error_v in SSL_VALID_ERRORS: + self.info(error) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + self.on_connection_d(connection) + except socket.error, error: + error_v = error.args[0] + if not error_v in VALID_ERRORS: + self.info(error) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + self.on_connection_d(connection) + except BaseException, exception: + self.info(exception) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + self.on_connection_d(connection) + + def on_write(self, socket): + connection = self.connections_m.get(socket, None) + if not connection: return + if not connection.status == OPEN: return + + if connection.connecting: + if connection.ssl: self._ssl_handshake(connection.socket) + else: self.on_connect(connection) + + try: + connection._send() + except ssl.SSLError, error: + error_v = error.args[0] + if not error_v in SSL_VALID_ERRORS: + self.info(error) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + self.on_connection_d(connection) + except socket.error, error: + error_v = error.args[0] + if not error_v in VALID_ERRORS: + self.info(error) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + self.on_connection_d(connection) + except BaseException, exception: + self.info(exception) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + self.on_connection_d(connection) + + def on_error(self, socket): + connection = self.connections_m.get(socket, None) + if not connection: return + if not connection.status == OPEN: return + + self.on_connection_d(connection) + + def on_connect(self, connection): + connection.set_connected() + + def on_data(self, connection, data): + pass + + def on_connection_c(self, connection): + connection.open(connect = True) + + def on_connection_d(self, connection): + connection.close() + + def _connects(self): + self._pending_lock.acquire() + try: + while self.pendings: + connection = self.pendings.pop() + self._connect(connection) + finally: + self._pending_lock.release() + + def _connect(self, connection): + # retrieves the socket associated with the connection + # and call the on connection created handler to set the + # connection ready for the connect operation + _socket = connection.socket + self.on_connection_c(connection) + + # tries to run the non blocking connection it should + # fail and the connection should only be considered as + # open when a write event is raised for the connection + try: _socket.connect(connection.address) + except ssl.SSLError, error: + error_v = error.args[0] + if not error_v in SSL_VALID_ERRORS: + self.info(error) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + except socket.error, error: + error_v = error.args[0] + if not error_v in VALID_ERRORS: + self.info(error) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + + # in case the connection is not of type ssl the method + # may returns as there's nothing left to be done, as the + # rest of the method is dedicated to ssl tricks + if not connection.ssl: return + + # creates the ssl object for the socket as it may have been + # destroyed by the underlying ssl library (as an error) because + # the socket is of type non blocking and raises an error + _socket._sslobj = _socket._sslobj or ssl._ssl.sslwrap( + _socket._sock, + False, + _socket.keyfile, + _socket.certfile, + _socket.cert_reqs, + _socket.ssl_version, + _socket.ca_certs + ) + + def _ssl_handshake(self, _socket): + Base._ssl_handshake(self, _socket) + if _socket._pending: return + connection = self.connections_m.get(_socket, None) + connection and self.on_connect(connection) diff --git a/src/netium/base/common.py b/src/netium/base/common.py new file mode 100644 index 000000000..3eede48cb --- /dev/null +++ b/src/netium/base/common.py @@ -0,0 +1,392 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__author__ = "João Magalhães joamag@hive.pt>" +""" The author(s) of the module """ + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import os +import ssl +import time +import types +import errno +import select +import logging +import traceback + +import observer + +from conn import * #@UnusedWildImport + +WSAEWOULDBLOCK = 10035 +""" The wsa would block error code meant to be used on +windows environments as a replacement for the would block +error code that indicates the failure to operate on a non +blocking connection """ + +VALID_ERRORS = ( + errno.EWOULDBLOCK, + errno.EAGAIN, + errno.EPERM, + errno.ENOENT, + WSAEWOULDBLOCK +) +""" List containing the complete set of error that represent +non ready operations in a non blocking socket """ + +SSL_VALID_ERRORS = ( + ssl.SSL_ERROR_WANT_READ, + ssl.SSL_ERROR_WANT_WRITE +) +""" The list containing the valid error in the handshake +operation of the ssl connection establishment """ + +STATE_STOP = 1 +""" The stop state value, this value is set when the service +is either in the constructed stage or when the service has been +stop normally or with an error """ + +STATE_START = 2 +""" The start state set when the service is in the starting +stage and running, normal state """ + +STATE_CONFIG = 3 +""" The configuration state that is set when the service is +preparing to become started and the configuration attributes +are being set according to pre-determined indications """ + +STATE_SELECT = 4 +""" State to be used when the service is in the select part +of the loop, this is the most frequent state in an idle service +as the service "spends" most of its time in it """ + +STATE_TICK = 5 +""" Tick state representative of the situation where the loop +tick operation is being started and all the pre tick handlers +are going to be called for pre-operations """ + +STATE_READ = 6 +""" Read state that is set when the connection are being read +and the on data handlers are being called, this is the part +where all the logic driven by incoming data is being called """ + +STATE_WRITE = 7 +""" The write state that is set on the writing of data to the +connections, this is a pretty "fast" state as no logic is +associated with it """ + +STATE_ERRROR = 8 +""" The error state to be used when the connection is processing +any error state coming from its main select operation and associated +with a certain connection (very rare) """ + +STATE_STRINGS = ( + "STOP", + "START", + "CONFIG", + "SELECT", + "TICK", + "READ", + "WRITE", + "ERROR" +) +""" Sequence that contains the various strings associated with +the various states for the base service, this may be used to +create an integer to string resolution mechanism """ + +# initializes the various paths that are going to be used for +# the base files configuration in the complete service infra +# structure, these should include the ssl based files +BASE_PATH = os.path.dirname(__file__) +EXTRAS_PATH = os.path.join(BASE_PATH, "extras") +SSL_KEY_PATH = os.path.join(EXTRAS_PATH, "net.key") +SSL_CER_PATH = os.path.join(EXTRAS_PATH, "net.cer") + +class Base(observer.Observable): + """ + Base network structure to be used by all the network + capable infra-structures (eg: servers and clients). + + Should handle all the nonblocking event loop so that + the read and write operations are easy to handle. + """ + + def __init__(self, name = None, handler = None, *args, **kwargs): + observer.Observable.__init__(self, *args, **kwargs) + self.name = name or self.__class__.__name__ + self.handler = handler + self.logger = None + self.read_l = [] + self.write_l = [] + self.error_l = [] + self.connections = [] + self.connections_m = {} + self._running = False + self._loaded = False + self.set_state(STATE_STOP); + + def load(self): + if self._loaded: return + + self.load_logging(); + self._loaded = True + + def load_logging(self, level = logging.DEBUG): + logging.basicConfig(format = "%(asctime)s [%(levelname)s] %(message)s") + self.logger = logging.getLogger(self.__class__.__name__) + self.logger.setLevel(level) + self.handler and self.logger.addHandler(self.handler) + + def start(self): + # triggers the loading of the internal structures of + # the base structure in case the loading has already + # been done nothing is done (avoids duplicated load) + self.load() + + # sets the running flag that controls the running of the + # main loop and then changes the current state to start + # as the main loop is going to start + self._running = True + self.set_state(STATE_START) + + # enters the main loop operation printing a message + # to the logger indicating this start, this stage + # should block the thread until a stop call is made + self.info("Starting the service's \"loop\" stage") + try: self.loop() + except BaseException, exception: + self.error(exception) + lines = traceback.format_exc().splitlines() + for line in lines: self.warning(line) + except: + self.critical("Critical level loop exception raised") + lines = traceback.format_exc().splitlines() + for line in lines: self.error(line) + finally: + self.info("Stopping the service's \"loop\" stage") + self.set_state(STATE_STOP) + + def stop(self): + self._running = False + + def is_empty(self): + return not self.read_l and not self.write_l and not self.error_l + + def loop(self): + # iterates continuously while the running flag + # is set, once it becomes unset the loop breaks + # at the next execution cycle + while self._running: + # calls the base tick int handler indicating that a new + # tick loop iteration is going to be started, all the + # "in between loop" operation should be performed in this + # callback as this is the "space" they have for execution + self.ticks() + + # updates the current state to select to indicate + # that the base service is selecting the connections + self.set_state(STATE_SELECT) + + # verifies if the current selection list is empty + # in case it's sleeps for a while and then continues + # the loop (this avoids error in empty selection) + is_empty = self.is_empty() + if is_empty: time.sleep(0.25); continue + + # runs the main selection operation on the current set + # of connection for each of the three operations returning + # the resulting active sets for the callbacks + reads, writes, errors = select.select( + self.read_l, + self.write_l, + self.error_l, + 0.25 + ) + + # calls the various callbacks with the selections lists, + # these are the main entry points for the logic to be executed + # each of this methods should be implemented in the underlying + # class instances as no behavior is defined at this inheritance + # level (abstract class) + self.reads(reads) + self.writes(writes) + self.errors(errors) + + def ticks(self): + self.set_state(STATE_TICK) + + def reads(self, reads): + self.set_state(STATE_READ) + + def writes(self, writes): + self.set_state(STATE_WRITE) + + def errors(self, errors): + self.set_state(STATE_ERRROR) + + def info_dict(self): + info = dict() + info["loaded"] = self._loaded + info["connections"] = len(self.connections) + info["state"] = self.get_state_s() + return info + + def new_connection(self, socket, address, ssl = False): + """ + Creates a new connection for the provided socket + object and string based address, the returned + value should be a workable object. + + @type socket: Socket + @param socket: The socket object to be encapsulated + by the object to be created (connection). + @type address: String + @param address: The address as a string to be used to + describe the connection object to be created. + @type ssl: bool + @param ssl: If the connection to be created is meant to + be secured using the ssl framework for encryption. + @rtype: Connection + @return: The connection object that encapsulates the + provided socket and address values. + """ + + return Connection(self, socket, address, ssl = ssl) + + def debug(self, object): + self.log(object, level = logging.DEBUG) + + def info(self, object): + self.log(object, level = logging.INFO) + + def warning(self, object): + self.log(object, level = logging.WARNING) + + def error(self, object): + self.log(object, level = logging.ERROR) + + def critical(self, object): + self.log(object, level = logging.CRITICAL) + + def log(self, object, level = logging.INFO): + object_t = type(object) + message = unicode(object) if not object_t in types.StringTypes else object + self.logger.log(level, message) + + def set_state(self, state): + self._state = state + + def get_state_s(self, lower = True): + """ + Retrieves a string describing the current state + of the system, this string should be as descriptive + as possible. + + An optional parameter controls if the string should + be lower cased or not. + + @type lower: bool + @param lower: If the returned string should be converted + into a lower cased version. + @rtype: String + @return: A string describing the current sate of the loop + system, should be as descriptive as possible. + """ + + state_s = STATE_STRINGS[self._state - 1] + state_s = state_s.lower() if lower else state_s + return state_s + + def _pending(self, _socket): + """ + Tries to perform the pending operations in the socket + and, these operations are set in the pending variable + of the socket structure. + + The method returns if there are still pending operations + after this method tick. + + @type _socket: Socket + @param _socket: The socket object to be checked for + pending operations and that is going to be used in the + performing of these operations. + @rtype: bool + @return: If there are still pending operations to be + performed in the provided socket. + """ + + # verifies if the pending attribute exists in the socket + # and that the value is valid, in case it's not there's + # no pending operation (method call) to be performed, and + # as such must return immediately with no pending value + if not hasattr(_socket, "_pending") or\ + not _socket._pending: return False + + # calls the pending callback method and verifies if the + # pending value still persists in the socket if that the + # case returns the is pending value to the caller method + _socket._pending(_socket) + is_pending = not _socket._pending == None + return is_pending + + def _ssl_wrap(self, _socket, key_file = None, cer_file = None, server = True): + dir_path = os.path.dirname(__file__) + base_path = os.path.join(dir_path, "../../") + base_path = os.path.normpath(base_path) + extras_path = os.path.join(base_path, "extras") + ssl_path = os.path.join(extras_path, "ssl") + + key_file = key_file or os.path.join(ssl_path, "server.key") + cer_file = cer_file or os.path.join(ssl_path, "server.cer") + + socket_ssl = ssl.wrap_socket( + _socket, + keyfile = key_file, + certfile = cer_file, + server_side = server, + ssl_version = ssl.PROTOCOL_TLSv1, + do_handshake_on_connect = False + ) + return socket_ssl + + def _ssl_handshake(self, _socket): + try: + _socket.do_handshake() + _socket._pending = None + except ssl.SSLError, error: + error_v = error.args[0] + if error_v in SSL_VALID_ERRORS: + _socket._pending = self._ssl_handshake + else: raise diff --git a/src/netium/base/conn.py b/src/netium/base/conn.py new file mode 100644 index 000000000..7c226e012 --- /dev/null +++ b/src/netium/base/conn.py @@ -0,0 +1,200 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__author__ = "João Magalhães joamag@hive.pt>" +""" The author(s) of the module """ + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import threading + +OPEN = 1 +""" The open status value, meant to be used in situations +where the status of the entity is open (opposite of closed) """ + +CLOSED = 2 +""" Closed status value to be used in entities which have +no pending structured opened and operations are limited """ + +CHUNK_SIZE = 4096 +""" The size of the chunk to be used while received +data from the service socket """ + +class Connection(object): + """ + Abstract connection object that should encapsulate + a socket object enabling it to be accessed in much + more "protected" way avoiding possible sync problems. + + It should also abstract the developer from all the + select associated complexities adding and removing the + underlying socket from the selecting mechanism for the + appropriate operations. + """ + + def __init__(self, owner, socket, address, ssl = False): + self.status = CLOSED + self.connecting = False + self.owner = owner + self.socket = socket + self.address = address + self.ssl = ssl + self.pending = [] + self.pending_lock = threading.RLock() + + def open(self, connect = False): + # in case the current status of the connection is not + # closed does not make sense to open it as it should + # already be open anyway (returns immediately) + if not self.status == CLOSED: return + + # retrieves the reference to the owner object from the + # current instance to be used to add the socket to the + # proper pooling mechanisms (at least for reading) + owner = self.owner + + # registers the socket for the proper reading mechanisms + # in the polling infra-structure of the owner + owner.read_l.append(self.socket) + owner.error_l.append(self.socket) + + # adds the current connection object to the list of + # connections in the owner and the registers it in + # the map that associates the socket with the connection + owner.connections.append(self) + owner.connections_m[self.socket] = self + + # sets the status of the current connection as open + # as all the internal structures have been correctly + # updated and not it's safe to perform operations + self.status = OPEN + + # in case the connect flag is set must set the current + # connection as connecting indicating that some extra + # steps are still required to complete the connection + if connect: self.set_connecting() + + def close(self): + # in case the current status of the connection is not open + # doen't make sense to close as it's already closed + if not self.status == OPEN: return + + # immediately sets the status of the connection as closed + # so that no one else changed the current connection status + # this is relevant to avoid any erroneous situation + self.status = CLOSED + + # retrieves the reference to the owner object from the + # current instance to be used to removed the socket from the + # proper pooling mechanisms (at least for reading) + owner = self.owner + + # removes the socket from all the polling mechanisms so that + # interaction with it is no longer part of the selecting mechanism + if self.socket in owner.read_l: owner.read_l.remove(self.socket) + if self.socket in owner.write_l: owner.write_l.remove(self.socket) + if self.socket in owner.error_l: owner.error_l.remove(self.socket) + + # removes the current connection from the list of connection in the + # owner and also from the map that associates the socket with the + # proper connection (also in the owner) + if self in owner.connections: owner.connections.remove(self) + if self.socket in owner.connections_m: del owner.connections_m[self.socket] + + # closes the socket, using the proper gracefully way so that + # operations are no longer allowed in the socket, in case there's + # an error in the operation fails silently (un purpose) + try: self.socket.close() + except: pass + + def set_connecting(self): + self.connecting = True + self.ensure_write() + + def set_connected(self): + self.remove_write() + self.connecting = False + + def ensure_write(self): + if not self.status == OPEN: return + if self.socket in self.owner.write_l: return + self.owner.write_l.append(self.socket) + + def remove_write(self): + if not self.status == OPEN: return + if not self.socket in self.owner.write_l: return + self.owner.write_l.remove(self.socket) + + def send(self, data): + """ + The main send call to be used by a proxy connection and + from different threads. + + Calling this method should be done with care as this can + create dead lock or socket corruption situations. + + @type data: String + @param data: The buffer containing the data to be sent + through this connection to the other endpoint. + """ + + self.ensure_write() + self.pending_lock.acquire() + try: self.pending.insert(0, data) + finally: self.pending_lock.release() + + def recv(self, size = CHUNK_SIZE): + return self._recv(size = size) + + def is_open(self): + return self.status == OPEN + + def is_closed(self): + return self.status == CLOSED + + def _send(self): + self.pending_lock.acquire() + try: + while True: + if not self.pending: break + data = self.pending.pop() + try: self.socket.send(data) + except: self.pending.append(data) + finally: + self.pending_lock.release() + + self.remove_write() + + def _recv(self, size): + return self.socket.recv(size) diff --git a/src/netium/base/observer.py b/src/netium/base/observer.py new file mode 100644 index 000000000..e831ee9ad --- /dev/null +++ b/src/netium/base/observer.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__author__ = "João Magalhães joamag@hive.pt>" +""" The author(s) of the module """ + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +class Observable(object): + + def __init__(self, *args, **kwargs): + self.events = {} + + def bind(self, name, method): + methods = self.events.get(name, []) + methods.append(method) + self.events[name] = methods + + def unbind(self, name, method = None): + methods = self.events.get(name, []) + if method: methods.remove(method) + else: methods[:] = [] + + def trigger(self, name, *args, **kwargs): + methods = self.events.get(name, []) + for method in methods: method(*args, **kwargs) diff --git a/src/netium/base/server.py b/src/netium/base/server.py new file mode 100644 index 000000000..1030efa8a --- /dev/null +++ b/src/netium/base/server.py @@ -0,0 +1,249 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__author__ = "João Magalhães joamag@hive.pt>" +""" The author(s) of the module """ + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import socket + +from common import * #@UnusedWildImport + +class Server(Base): + + def __init__(self, name = None, handler = None, *args, **kwargs): + Base.__init__(self, name = name, hadler = handler, *args, **kwargs) + self.socket = None + self.host = None + self.port = None + self.ssl = False + + def reads(self, reads): + self.set_state(STATE_READ) + for read in reads: + if read == self.socket: self.on_read_s(read) + else: self.on_read(read) + + def writes(self, writes): + self.set_state(STATE_WRITE) + for write in writes: + if write == self.socket: self.on_write_s(write) + else: self.on_write(write) + + def errors(self, errors): + self.set_state(STATE_ERRROR) + for error in errors: + if error == self.socket: self.on_error_s(error) + else: self.on_error(error) + + def info_dict(self): + info = Base.info_dict(self) + info["host"] = self.host + info["port"] = self.port + info["ssl"] = self.ssl + return info + + def serve(self, host = "127.0.0.1", port = 9090, ssl = False, key_file = None, cer_file = None): + # updates the current service status to the configuration + # stage as the next steps is to configure the service socket + self.set_state(STATE_CONFIG) + + # populates the basic information on the currently running + # server like the host the port and the (is) ssl flag to be + # used latter for reference operations + self.host = host + self.port = port + self.ssl = ssl + + # defaults the provided ssl key and certificate paths to the + # ones statically defined (dummy certificates), please beware + # that using these certificates may create validation problems + key_file = key_file or SSL_KEY_PATH + cer_file = cer_file or SSL_CER_PATH + + # creates the socket that it's going to be used for the listening + # of new connections (server socket) and sets it as non blocking + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setblocking(0) + + # in case the server is meant to be used as ssl wraps the socket + # in suck fashion so that it becomes "secured" + if ssl: self.socket = self._ssl_wrap( + self.socket, + key_file = key_file, + cer_file = cer_file, + server = True + ) + + # sets the various options in the service socket so that it becomes + # ready for the operation with the highest possible performance, these + # options include the reuse address to be able to re-bind to the port + # and address and the keep alive that drops connections after some time + # avoiding the leak of connections (operative system managed) + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + hasattr(socket, "SO_REUSEPORT") and\ + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) #@UndefinedVariable + + # adds the socket to all of the pool lists so that it's ready to read + # write and handle error, this is the expected behavior of a service + # socket so that it can handle all of the expected operations + self.read_l.append(self.socket) + self.write_l.append(self.socket) + self.error_l.append(self.socket) + + # binds the socket to the provided host and port and then start the + # listening in the socket with the maximum backlog as possible + self.socket.bind((host, port)) + self.socket.listen(5) + + # starts the base system so that the event loop gets started and the + # the servers gets ready to accept new connections (starts service) + self.start() + + def on_read_s(self, _socket): + try: + socket_c, address = _socket.accept() + try: self.on_socket_c(socket_c, address) + except: socket_c.close(); raise + except BaseException, exception: + self.info(exception) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + + def on_write_s(self, socket): + pass + + def on_error_s(self, socket): + pass + + def on_read(self, _socket): + connection = self.connections_m.get(_socket, None) + if not connection: return + if not connection.status == OPEN: return + + try: + # verifies if there's any pending operations in the + # socket (eg: ssl handshaking) and performs them trying + # to finish them, in they are still pending at the current + # state returns immediately (waits for next loop) + if self._pending(_socket): return + + # iterates continuously trying to read as much data as possible + # when there's a failure to read more data it should raise an + # exception that should be handled properly + while True: + data = _socket.recv(CHUNK_SIZE) + if data: self.on_data(connection, data) + else: self.on_connection_d(connection); break + except ssl.SSLError, error: + error_v = error.args[0] + if not error_v in SSL_VALID_ERRORS: + self.info(error) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + self.on_connection_d(connection) + except socket.error, error: + error_v = error.args[0] + if not error_v in VALID_ERRORS: + self.info(error) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + self.on_connection_d(connection) + except BaseException, exception: + self.info(exception) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + self.on_connection_d(connection) + + def on_write(self, socket): + connection = self.connections_m.get(socket, None) + if not connection: return + if not connection.status == OPEN: return + + try: + connection._send() + except ssl.SSLError, error: + error_v = error.args[0] + if not error_v in SSL_VALID_ERRORS: + self.info(error) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + self.on_connection_d(connection) + except socket.error, error: + error_v = error.args[0] + if not error_v in VALID_ERRORS: + self.info(error) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + self.on_connection_d(connection) + except BaseException, exception: + self.info(exception) + lines = traceback.format_exc().splitlines() + for line in lines: self.debug(line) + self.on_connection_d(connection) + + def on_error(self, socket): + connection = self.connections_m.get(socket, None) + if not connection: return + if not connection.status == OPEN: return + + self.on_connection_d(connection) + + def on_data(self, connection, data): + pass + + def on_socket_c(self, socket_c, address): + if self.ssl: socket_c.pending = None + + socket_c.setblocking(0) + socket_c.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + socket_c.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + + if self.ssl: self._ssl_handshake(socket_c) + + connection = self.new_connection(socket_c, address, ssl = self.ssl) + self.on_connection_c(connection) + + def on_socket_d(self, socket_c): + connection = self.connections_m.get(socket_c, None) + if not connection: return + + def on_connection_c(self, connection): + connection.open() + + def on_connection_d(self, connection): + connection.close() diff --git a/src/netium/clients/__init__.py b/src/netium/clients/__init__.py new file mode 100644 index 000000000..7696cd943 --- /dev/null +++ b/src/netium/clients/__init__.py @@ -0,0 +1,41 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import apn +import http + +from apn import * +from http import * diff --git a/src/netium/clients/apn.py b/src/netium/clients/apn.py new file mode 100644 index 000000000..5279b8a0e --- /dev/null +++ b/src/netium/clients/apn.py @@ -0,0 +1,51 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__author__ = "João Magalhães joamag@hive.pt>" +""" The author(s) of the module """ + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import netium + +TOKEN_STRING = "asdads" + +class ApnClient(netium.Client): + + def send_message(self, token_string = TOKEN_STRING, *args, **kwargs): + message = kwargs.get("message", "Hello World") + sound = kwargs.get("sound", "default") + badge = kwargs.get("badge", 0) + sandbox = kwargs.get("sandbox", True) + wait = kwargs.get("wait", False) diff --git a/src/netium/clients/http.py b/src/netium/clients/http.py new file mode 100644 index 000000000..4b5b55a66 --- /dev/null +++ b/src/netium/clients/http.py @@ -0,0 +1,119 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__author__ = "João Magalhães joamag@hive.pt>" +""" The author(s) of the module """ + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import urlparse + +import netium + +class HttpConnection(netium.Connection): + + def __init__(self, owner, socket, address, ssl = False): + netium.Connection.__init__(self, owner, socket, address, ssl = ssl) + self.version = "HTTP/1.0" + self.method = "GET" + self.url = None + self.ssl = False + self.host = None + self.port = None + self.path = None + + def set_http( + self, + version = "HTTP/1.0", + method = "GET", + url = None, + host = None, + port = None, + path = None, + ssl = False + ): + self.method = method.upper() + self.version = version + self.url = url + self.host = host + self.port = port + self.path = path + self.ssl = ssl + +class HttpClient(netium.Client): + """ + Simple test of an http client + """ + + def get(self, url): + parsed = urlparse.urlparse(url) + ssl = parsed.scheme == "https" + host = parsed.hostname + port = parsed.port or (ssl and 443 or 80) + path = parsed.path + + connection = self.connect(host, port, ssl = ssl) + connection.set_http( + version = "HTTP/1.0", + method = "GET", + url = url, + host = host, + port = port, + path = path, + ssl = ssl + ) + + def on_connect(self, connection): + netium.Client.on_connect(self, connection) + + method = connection.method + path = connection.path + version = connection.version + + connection.send("%s %s %s\r\n\r\n" % (method, path, version)) + + def on_data(self, connection, data): + netium.Client.on_data(self, connection, data) + + headers, message = data.split("\r\n\r\n", 1) + self.on_data_http(headers, message) + + def on_connection_d(self, connection): + netium.Client.on_connection_d(self, connection) + + def new_connection(self, socket, address, ssl = False): + return HttpConnection(self, socket, address, ssl = ssl) + + def on_data_http(self, headers, message): + print message diff --git a/src/netium/servers/__init__.py b/src/netium/servers/__init__.py new file mode 100644 index 000000000..3374cfce7 --- /dev/null +++ b/src/netium/servers/__init__.py @@ -0,0 +1,41 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import echo +import ws + +from echo import * +from ws import * diff --git a/src/netium/servers/echo.py b/src/netium/servers/echo.py new file mode 100644 index 000000000..f343028f6 --- /dev/null +++ b/src/netium/servers/echo.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__author__ = "João Magalhães joamag@hive.pt>" +""" The author(s) of the module """ + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import ws + +class EchoServer(ws.WSServer): + + def on_data_ws(self, connection, data): + ws.WSServer.on_data_ws(self, connection, data) + connection.send_ws(data) + +if __name__ == "__main__": + server = EchoServer() + server.serve() diff --git a/src/netium/servers/ws.py b/src/netium/servers/ws.py new file mode 100644 index 000000000..92b30b330 --- /dev/null +++ b/src/netium/servers/ws.py @@ -0,0 +1,260 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Hive Netium System +# Copyright (C) 2008-2012 Hive Solutions Lda. +# +# This file is part of Hive Netium System. +# +# Hive Netium System is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hive Netium System is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Hive Netium System. If not, see . + +__author__ = "João Magalhães joamag@hive.pt>" +""" The author(s) of the module """ + +__version__ = "1.0.0" +""" The version of the module """ + +__revision__ = "$LastChangedRevision$" +""" The revision number of the module """ + +__date__ = "$LastChangedDate$" +""" The last change date of the module """ + +__copyright__ = "Copyright (c) 2008-2012 Hive Solutions Lda." +""" The copyright for the module """ + +__license__ = "GNU General Public License (GPL), Version 3" +""" The license for the module """ + +import base64 +import hashlib + +import netium + +class WSConnection(netium.Connection): + + def __init__(self, owner, socket, address, ssl = False): + netium.Connection.__init__(self, owner, socket, address, ssl = ssl) + self.handshake = False + self.method = None + self.path = None + self.version = None + self.buffer_l = [] + self.headers = {} + + def send_ws(self, data): + encoded = self._encode(data) + return self.send(encoded) + + def recv_ws(self, size = netium.CHUNK_SIZE): + data = self.recv(size = size) + decoded = self._decode(data) + return decoded + + def add_buffer(self, data): + self.buffer_l.append(data) + + def do_handshake(self): + if self.handshake: + raise RuntimeError("Handshake already done") + + buffer = "".join(self.buffer_l) + if not buffer[-4:] == "\r\n\r\n": + raise RuntimeError("Missing data for handshake") + + lines = buffer.split("\r\n") + for line in lines[1:]: + values = line.split(":") + values_l = len(values) + if not values_l == 2: continue + + key, value = values + key = key.strip() + value = value.strip() + self.headers[key] = value + + first = lines[0] + self.method, self.path, self.version = first.split(" ") + + del self.buffer_l[:] + self.handshake = True + + def accept_key(self): + socket_key = self.headers.get("Sec-WebSocket-Key", None) + if not socket_key: + raise RuntimeError("No socket key found in headers") + + hash = hashlib.sha1(socket_key + WSServer.MAGIC_VALUE) + hash_digest = hash.digest() + accept_key = base64.b64encode(hash_digest) + return accept_key + + def get_buffer(self, delete = True): + if not self.buffer_l: return "" + buffer = "".join(self.buffer_l) + if delete: del self.buffer_l[:] + return buffer + + def _encode(self, data): + data_l = len(data) + encoded_l = list() + + encoded_l.append(chr(129)) + + if data_l <= 125: + encoded_l.append(chr(data_l)) + + elif data_l >= 126 and data_l <= 65535: + encoded_l.append(chr(126)) + encoded_l.append(chr((data_l >> 8) & 255)) + encoded_l.append(chr(data_l & 255)) + + else: + encoded_l.append(chr(127)) + encoded_l.append(chr((data_l >> 56) & 255)) + encoded_l.append(chr((data_l >> 48) & 255)) + encoded_l.append(chr((data_l >> 40) & 255)) + encoded_l.append(chr((data_l >> 32) & 255)) + encoded_l.append(chr((data_l >> 24) & 255)) + encoded_l.append(chr((data_l >> 16) & 255)) + encoded_l.append(chr((data_l >> 8) & 255)) + encoded_l.append(chr(data_l & 255)) + + encoded_l.append(data) + encoded = "".join(encoded_l) + return encoded + + def _decode(self, data): + second_byte = data[1] + + length = ord(second_byte) & 127 + + index_mask_f = 2 + + if length == 126: + length = 0 + length += ord(data[2]) << 8 + length += ord(data[3]) + index_mask_f = 4 + + elif length == 127: + length = 0 + length += ord(data[2]) << 56 + length += ord(data[3]) << 48 + length += ord(data[4]) << 40 + length += ord(data[5]) << 32 + length += ord(data[6]) << 24 + length += ord(data[7]) << 16 + length += ord(data[8]) << 8 + length += ord(data[9]) + index_mask_f = 10 + + # calculates the size of the raw data part of the message and + # in case its smaller than the defined length of the data returns + # immediately indicating that there's not enough data to complete + # the decoding of the data (should be re-trying again latter) + raw_size = len(data) - index_mask_f - 4 + if raw_size < length: + raise RuntimeError("Not enough data available for parsing") + + # retrieves the masks part of the data that are going to be + # used in the decoding part of the process + masks = data[index_mask_f:index_mask_f + 4] + + # allocates the array that is going to be used + # for the decoding of the data with the length + # that was computed as the data length + decoded_a = bytearray(length) + + # starts the initial data index and then iterates over the + # range of decoded length applying the mask to the data + # (decoding it consequently) to the created decoded array + i = index_mask_f + 4 + for j in range(length): + decoded_a[j] = chr(ord(data[i]) ^ ord(masks[j % 4])) + i += 1 + + # converts the decoded array of data into a string and + # and returns the "partial" string containing the data that + # remained pending to be parsed + decoded = str(decoded_a) + return decoded, data[i:] + +class WSServer(netium.Server): + """ + Base class for the creation of websocket server, should + handle both the upgrading/handshaking of the connection + and together with the associated connection class the + encoding and decoding of the frames. + """ + + MAGIC_VALUE = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + """ The magic value used by the websocket protocol as part + of the key generation process in the handshake """ + + def on_connection_c(self, connection): + netium.Server.on_connection_c(self, connection) + + def on_data(self, connection, data): + netium.Server.on_data(self, connection, data) + + if connection.handshake: + while data: + buffer = connection.get_buffer() + try: decoded, data = connection._decode(buffer + data) + except: self.add_buffer(data); break + self.on_data_ws(connection, decoded) + + else: + connection.add_buffer(data) + connection.do_handshake() + accept_key = connection.accept_key() + response = self._handshake_response(accept_key) + connection.send(response) + self.on_handshake(connection) + + def new_connection(self, socket, address, ssl = False): + return WSConnection(self, socket, address, ssl = ssl) + + def send_ws(self, connection, data): + encoded = self._encode(data) + connection.send(encoded) + + def on_data_ws(self, connection, data): + pass + + def on_handshake(self, connection): + pass + + def _handshake_response(self, accept_key): + """ + Returns the response contents of the handshake operation for + the provided accept key. + + The key value should already be calculated according to the + specification. + + @type accept_key: String + @param accept_key: The accept key to be used in the creation + of the response message. + @rtype: String + @return: The response message contents generated according to + the specification and the provided accept key. + """ + + data = "HTTP/1.1 101 Switching Protocols\r\n" +\ + "Upgrade: websocket\r\n" +\ + "Connection: Upgrade\r\n" +\ + "Sec-WebSocket-Accept: %s\r\n\r\n" % accept_key + return data