diff options
33 files changed, 2589 insertions, 603 deletions
@@ -1,3 +1,7 @@ -*.pyc +*.pyc +*.egg *.egg-info +*.swp +*.swo dist/ +build/ diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..5f9cb69 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,21 @@ +0.2.2 Jun 28: + o Bugfix: use the provider's default language as default + string. Also take care (and note) a possible case with a + problematic provider misconfiguration. Closes #3029. + o Add data files to setup and manifest (certificates for tests) + o Allow absolute paths in baseconfig.load + o Fix deprecation warnings + o Fix attempt to fetch private keys from server. + o Fix missing imports + o Add possibility of unregistering callbacks for a signal. + o Add a mechanism for events signaling between components. + o Prioritize the path_extension in the which method so it finds our + bundled app before the system one, if any. + o Move the Key Manager to leap client repository. + o Move symmetric encryption code to leap.soledad. + o Refactor opengpg utility functions implementation so it uses a + context manager. + o Add OpenPGP sign/verify + o Add RAISE_WINDOW event + o Add AES-256 (CTR mode) encrypting/decrypting functions using + PyCrypto. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..84a01ef --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include src/leap/common/testing/*.pem @@ -1,3 +1,22 @@ leap.common =========== + +.. image:: https://pypip.in/v/leap.common/badge.png + :target: https://crate.io/packages/leap.common + A collection of shared utils used by the several python LEAP subprojects. + +* leap.common.cert +* leap.common.checks +* leap.common.config +* leap.common.events +* leap.common.files +* leap.common.testing + +Library dependencies +-------------------- +* ``protobuf-compiler`` + +Python dependencies +------------------- +* See ``pkg/requirements.pip`` diff --git a/changes/feature_improve_which b/changes/feature_improve_which deleted file mode 100644 index d1d1fb5..0000000 --- a/changes/feature_improve_which +++ /dev/null @@ -1,2 +0,0 @@ - o Prioritize the path_extension in the which method so it finds our bundled - app before the system one, if any. diff --git a/pkg/requirements-testing.pip b/pkg/requirements-testing.pip new file mode 100644 index 0000000..932a895 --- /dev/null +++ b/pkg/requirements-testing.pip @@ -0,0 +1 @@ +mock diff --git a/pkg/requirements.pip b/pkg/requirements.pip new file mode 100644 index 0000000..141c325 --- /dev/null +++ b/pkg/requirements.pip @@ -0,0 +1,10 @@ +jsonschema<=0.8 +pyxdg +protobuf +pyopenssl +python-dateutil +autopep8 +python-gnupg +PyCrypto + +https://protobuf-socket-rpc.googlecode.com/files/protobuf.socketrpc-1.3.2.tar.gz @@ -19,25 +19,62 @@ setup file for leap.common """ from setuptools import setup, find_packages +# XXX parse pkg/requirements.pip requirements = [ + "jsonschema", + "pyxdg", + 'protobuf', + 'protobuf.socketrpc', + "PyOpenSSL", + "python-dateutil", + "PyCrypto", ] -# XXX add classifiers, docs +#dependency_links = [ + #"https://protobuf-socket-rpc.googlecode.com/files/protobuf.socketrpc-1.3.2.tar.gz#egg=protobuf.socketrpc" +#] + +tests_requirements = [ + 'mock', +] + +trove_classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + ("License :: OSI Approved :: GNU General " + "Public License v3 or later (GPLv3+)"), + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Topic :: Communications", + "Topic :: Security", + "Topic :: Utilities" +] setup( name='leap.common', - version='0.2.0-dev', + # If you change version, do it also in + # src/leap/common/__init__.py + version='0.2.5', url='https://leap.se/', license='GPLv3+', author='The LEAP Encryption Access Project', author_email='info@leap.se', - description='Common files used by the LEAP Client project.', + description='Common files used by the LEAP project.', long_description=( "Common files used by the LEAP Client project." ), + classifiers=trove_classifiers, namespace_packages=["leap"], package_dir={'': 'src'}, + # For now, we do not exclude tests because of the circular dependency + # between leap.common and leap.soledad. + #packages=find_packages('src', exclude=['leap.common.tests']), packages=find_packages('src'), - #test_suite='leap.common.tests', - #install_requires=requirements, + test_suite='leap.common.tests', + install_requires=requirements, + #dependency_links=dependency_links, + tests_require=tests_requirements, + include_package_data=True ) diff --git a/src/leap/common/__init__.py b/src/leap/common/__init__.py index 9467c46..2b30715 100644 --- a/src/leap/common/__init__.py +++ b/src/leap/common/__init__.py @@ -3,6 +3,7 @@ import logging from leap.common import certs from leap.common import check from leap.common import files +from leap.common import events logger = logging.getLogger(__name__) @@ -10,9 +11,9 @@ try: import pygeoip HAS_GEOIP = True except ImportError: - logger.debug('PyGeoIP not found. Disabled Geo support.') + #logger.debug('PyGeoIP not found. Disabled Geo support.') HAS_GEOIP = False -__all__ = ["certs", "check", "files"] +__all__ = ["certs", "check", "files", "events"] -__version__ = "0.2.0-dev" +__version__ = "0.2.5" diff --git a/src/leap/common/certs.py b/src/leap/common/certs.py index 4cb70dd..4fe563b 100644 --- a/src/leap/common/certs.py +++ b/src/leap/common/certs.py @@ -35,10 +35,10 @@ def get_cert_from_string(string): """ Returns the x509 from the contents of this string - @param string: certificate contents as downloaded - @type string: str + :param string: certificate contents as downloaded + :type string: str - @return: x509 or None + :return: x509 or None """ leap_assert(string, "We need something to load") @@ -55,10 +55,10 @@ def get_privatekey_from_string(string): """ Returns the private key from the contents of this string - @param string: private key contents as downloaded - @type string: str + :param string: private key contents as downloaded + :type string: str - @return: private key or None + :return: private key or None """ leap_assert(string, "We need something to load") @@ -75,12 +75,12 @@ def get_digest(cert_data, method): """ Returns the digest for the cert_data using the method specified - @param cert_data: certificate data in string form - @type cert_data: str - @param method: method to be used for digest - @type method: str + :param cert_data: certificate data in string form + :type cert_data: str + :param method: method to be used for digest + :type method: str - @rtype: str + :rtype: str """ x509 = get_cert_from_string(cert_data) digest = x509.digest(method).replace(":", "").lower() @@ -93,10 +93,10 @@ def can_load_cert_and_pkey(string): Loads certificate and private key from a buffer, returns True if everything went well, False otherwise - @param string: buffer containing the cert and private key - @type string: str or any kind of buffer + :param string: buffer containing the cert and private key + :type string: str or any kind of buffer - @rtype: bool + :rtype: bool """ can_load = True @@ -118,10 +118,10 @@ def is_valid_pemfile(cert): """ Checks that the passed string is a valid pem certificate - @param cert: String containing pem content - @type cert: str + :param cert: String containing pem content + :type cert: str - @rtype: bool + :rtype: bool """ leap_assert(cert, "We need a cert to load") @@ -132,10 +132,10 @@ def get_cert_time_boundaries(certfile): """ Returns the time boundaries for the certificate saved in certfile - @param certfile: path to certificate - @type certfile: str + :param certfile: path to certificate + :type certfile: str - @rtype: tuple (from, to) + :rtype: tuple (from, to) """ cert = get_cert_from_string(certfile) leap_assert(cert, 'There was a problem loading the certificate') @@ -151,11 +151,11 @@ def should_redownload(certfile, now=time.gmtime): """ Returns True if any of the checks don't pass, False otherwise - @param certfile: path to certificate - @type certfile: str - @param now: current date function, ONLY USED FOR TESTING + :param certfile: path to certificate + :type certfile: str + :param now: current date function, ONLY USED FOR TESTING - @rtype: bool + :rtype: bool """ exists = os.path.isfile(certfile) diff --git a/src/leap/common/check.py b/src/leap/common/check.py index 359673b..a2d39a6 100644 --- a/src/leap/common/check.py +++ b/src/leap/common/check.py @@ -31,10 +31,10 @@ def leap_assert(condition, message=""): Asserts the condition and displays the message if that's not met. It also logs the error and its backtrace. - @param condition: condition to check - @type condition: bool - @param message: message to display if the condition isn't met - @type message: str + :param condition: condition to check + :type condition: bool + :param message: message to display if the condition isn't met + :type message: str """ if not condition: logger.error("Bug: %s" % (message,)) @@ -51,10 +51,10 @@ def leap_assert_type(var, expectedType): """ Helper assert check for a variable's expected type - @param var: variable to check - @type var: any - @param expectedType: type to check agains - @type expectedType: type + :param var: variable to check + :type var: any + :param expectedType: type to check agains + :type expectedType: type """ leap_assert(isinstance(var, expectedType), "Expected type %r instead of %r" % diff --git a/src/leap/common/config/__init__.py b/src/leap/common/config/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/leap/common/config/__init__.py diff --git a/src/leap/common/config/baseconfig.py b/src/leap/common/config/baseconfig.py new file mode 100644 index 0000000..699d734 --- /dev/null +++ b/src/leap/common/config/baseconfig.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +# baseconfig.py +# Copyright (C) 2013 LEAP +# +# This program 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. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +""" +Implements the abstract base class for configuration +""" + +import copy +import logging +import functools +import os + +from abc import ABCMeta, abstractmethod + +from leap.common.check import leap_assert +from leap.common.files import mkdir_p +from leap.common.config.pluggableconfig import PluggableConfig +from leap.common.config.prefixers import get_platform_prefixer + +logger = logging.getLogger(__name__) + + +class BaseConfig: + """ + Abstract base class for any JSON based configuration. + """ + + __metaclass__ = ABCMeta + + """ + Standalone is a class wide parameter. + + :param standalone: if True it will return the prefix for a + standalone application. Otherwise, it will + return the system + default for configuration storage. + :type standalone: bool + """ + standalone = False + + def __init__(self): + self._data = {} + self._config_checker = None + + @abstractmethod + def _get_spec(self): + """ + Returns the spec object for the specific configuration. + """ + return None + + def _safe_get_value(self, key): + """ + Tries to return a value only if the config has already been loaded. + + :rtype: depends on the config structure, dict, str, array, int + :return: returns the value for the specified key in the config + """ + leap_assert(self._config_checker, "Load the config first") + return self._config_checker.config.get(key, None) + + def get_path_prefix(self): + """ + Returns the platform dependant path prefixer + """ + return get_platform_prefixer().get_path_prefix( + standalone=self.standalone) + + def loaded(self): + """ + Returns True if the configuration has been already + loaded. False otherwise + """ + return self._config_checker is not None + + def save(self, path_list): + """ + Saves the current configuration to disk. + + :param path_list: list of components that form the relative + path to configuration. The absolute path + will be calculated depending on the platform. + :type path_list: list + + :return: True if saved to disk correctly, False otherwise + """ + config_path = os.path.join(self.get_path_prefix(), *(path_list[:-1])) + mkdir_p(config_path) + + try: + self._config_checker.serialize(os.path.join(config_path, + path_list[-1])) + except Exception as e: + logger.warning("%s" % (e,)) + raise + return True + + def load(self, path="", data=None, mtime=None, relative=True): + """ + Loads the configuration from disk. + + :param path: if relative=True, this is a relative path + to configuration. The absolute path + will be calculated depending on the platform + :type path: str + + :param relative: if True, path is relative. If False, it's absolute. + :type relative: bool + + :return: True if loaded from disk correctly, False otherwise + :rtype: bool + """ + + if relative is True: + config_path = os.path.join( + self.get_path_prefix(), path) + else: + config_path = path + + self._config_checker = PluggableConfig(format="json") + self._config_checker.options = copy.deepcopy(self._get_spec()) + + try: + if data is None: + self._config_checker.load(fromfile=config_path, mtime=mtime) + else: + self._config_checker.load(data, mtime=mtime) + except Exception as e: + logger.error("Something went wrong while loading " + + "the config from %s\n%s" % (config_path, e)) + self._config_checker = None + return False + return True + + +class LocalizedKey(object): + """ + Decorator used for keys that are localized in a configuration. + """ + + def __init__(self, func, **kwargs): + self._func = func + + def __call__(self, instance, lang=None): + """ + Tries to return the string for the specified language, otherwise + returns the default language string. + + :param lang: language code + :type lang: str + + :return: localized value from the possible values returned by + self._func + It returns None in case that the provider does not provides + a matching pair of default_language and string for + that language. + e.g.: + 'default_language': 'es', + 'description': {'en': 'test description'} + Note that the json schema can't check that. + """ + descriptions = self._func(instance) + config_lang = instance.get_default_language() + if lang is None: + lang = config_lang + + for key in descriptions.keys(): + if lang.startswith(key): + config_lang = key + break + + description_lang = descriptions.get(config_lang) + if description_lang is None: + logger.error("There is a misconfiguration in the " + "provider's language strings.") + + return description_lang + + def __get__(self, instance, instancetype): + """ + Implement the descriptor protocol to make decorating instance + method possible. + """ + # Return a partial function with the first argument is the instance + # of the class decorated. + return functools.partial(self.__call__, instance) + +if __name__ == "__main__": + try: + config = BaseConfig() # should throw TypeError for _get_spec + except Exception as e: + assert isinstance(e, TypeError), "Something went wrong" + print "Abstract BaseConfig class is working as expected" diff --git a/src/leap/common/config/pluggableconfig.py b/src/leap/common/config/pluggableconfig.py new file mode 100644 index 0000000..8535fa6 --- /dev/null +++ b/src/leap/common/config/pluggableconfig.py @@ -0,0 +1,475 @@ +# -*- coding: utf-8 -*- +# pluggableconfig.py +# Copyright (C) 2013 LEAP +# +# This program 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. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +""" +generic configuration handlers +""" +import copy +import json +import logging +import os +import time +import urlparse + +import jsonschema + +#from leap.base.util.translations import LEAPTranslatable +from leap.common.check import leap_assert + + +logger = logging.getLogger(__name__) + + +__all__ = ['PluggableConfig', + 'adaptors', + 'types', + 'UnknownOptionException', + 'MissingValueException', + 'ConfigurationProviderException', + 'TypeCastException'] + +# exceptions + + +class UnknownOptionException(Exception): + """exception raised when a non-configuration + value is present in the configuration""" + + +class MissingValueException(Exception): + """exception raised when a required value is missing""" + + +class ConfigurationProviderException(Exception): + """exception raised when a configuration provider is missing, etc""" + + +class TypeCastException(Exception): + """exception raised when a + configuration item cannot be coerced to a type""" + + +class ConfigAdaptor(object): + """ + abstract base class for config adaotors for + serialization/deserialization and custom validation + and type casting. + """ + def read(self, filename): + raise NotImplementedError("abstract base class") + + def write(self, config, filename): + with open(filename, 'w') as f: + self._write(f, config) + + def _write(self, fp, config): + raise NotImplementedError("abstract base class") + + def validate(self, config, schema): + raise NotImplementedError("abstract base class") + + +adaptors = {} + + +class JSONSchemaEncoder(json.JSONEncoder): + """ + custom default encoder that + casts python objects to json objects for + the schema validation + """ + def default(self, obj): + if obj is str: + return 'string' + if obj is unicode: + return 'string' + if obj is int: + return 'integer' + if obj is list: + return 'array' + if obj is dict: + return 'object' + if obj is bool: + return 'boolean' + + +class JSONAdaptor(ConfigAdaptor): + indent = 2 + extensions = ['json'] + + def read(self, _from): + if isinstance(_from, file): + _from_string = _from.read() + if isinstance(_from, str): + _from_string = _from + return json.loads(_from_string) + + def _write(self, fp, config): + fp.write(json.dumps(config, + indent=self.indent, + sort_keys=True)) + + def validate(self, config, schema_obj): + schema_json = JSONSchemaEncoder().encode(schema_obj) + schema = json.loads(schema_json) + jsonschema.validate(config, schema) + + +adaptors['json'] = JSONAdaptor() + +# +# Adaptors +# +# Allow to apply a predefined set of types to the +# specs, so it checks the validity of formats and cast it +# to proper python types. + +# TODO: +# - HTTPS uri + + +class DateType(object): + fmt = '%Y-%m-%d' + + def to_python(self, data): + return time.strptime(data, self.fmt) + + def get_prep_value(self, data): + return time.strftime(self.fmt, data) + + +class TranslatableType(object): + """ + a type that casts to LEAPTranslatable objects. + Used for labels we get from providers and stuff. + """ + + def to_python(self, data): + # TODO: add translatable + return data # LEAPTranslatable(data) + + # needed? we already have an extended dict... + #def get_prep_value(self, data): + #return dict(data) + + +class URIType(object): + + def to_python(self, data): + parsed = urlparse.urlparse(data) + if not parsed.scheme: + raise TypeCastException("uri %s has no schema" % data) + return parsed.geturl() + + def get_prep_value(self, data): + return data + + +class HTTPSURIType(object): + + def to_python(self, data): + parsed = urlparse.urlparse(data) + if not parsed.scheme: + raise TypeCastException("uri %s has no schema" % data) + if parsed.scheme != "https": + raise TypeCastException( + "uri %s does not has " + "https schema" % data) + return parsed.geturl() + + def get_prep_value(self, data): + return data + + +types = { + 'date': DateType(), + 'uri': URIType(), + 'https-uri': HTTPSURIType(), + 'translatable': TranslatableType(), +} + + +class PluggableConfig(object): + + options = {} + + def __init__(self, + adaptors=adaptors, + types=types, + format=None): + + self.config = {} + self.adaptors = adaptors + self.types = types + self._format = format + self.mtime = None + self.dirty = False + + @property + def option_dict(self): + if hasattr(self, 'options') and isinstance(self.options, dict): + return self.options.get('properties', None) + + def items(self): + """ + act like an iterator + """ + if isinstance(self.option_dict, dict): + return self.option_dict.items() + return self.options + + def validate(self, config, format=None): + """ + validate config + """ + schema = self.options + if format is None: + format = self._format + + if format: + adaptor = self.get_adaptor(self._format) + adaptor.validate(config, schema) + else: + # we really should make format mandatory... + logger.error('no format passed to validate') + + # first round of validation is ok. + # now we proceed to cast types if any specified. + self.to_python(config) + + def to_python(self, config): + """ + cast types following first type and then format indications. + """ + unseen_options = [i for i in config if i not in self.option_dict] + if unseen_options: + raise UnknownOptionException( + "Unknown options: %s" % ', '.join(unseen_options)) + + for key, value in config.items(): + _type = self.option_dict[key].get('type') + if _type is None and 'default' in self.option_dict[key]: + _type = type(self.option_dict[key]['default']) + if _type is not None: + tocast = True + if not callable(_type) and isinstance(value, _type): + tocast = False + if tocast: + try: + config[key] = _type(value) + except BaseException, e: + raise TypeCastException( + "Could not coerce %s, %s, " + "to type %s: %s" % (key, value, _type.__name__, e)) + _format = self.option_dict[key].get('format', None) + _ftype = self.types.get(_format, None) + if _ftype: + try: + config[key] = _ftype.to_python(value) + except BaseException, e: + raise TypeCastException( + "Could not coerce %s, %s, " + "to format %s: %s" % (key, value, + _ftype.__class__.__name__, + e)) + + return config + + def prep_value(self, config): + """ + the inverse of to_python method, + called just before serialization + """ + for key, value in config.items(): + _format = self.option_dict[key].get('format', None) + _ftype = self.types.get(_format, None) + if _ftype and hasattr(_ftype, 'get_prep_value'): + try: + config[key] = _ftype.get_prep_value(value) + except BaseException, e: + raise TypeCastException( + "Could not serialize %s, %s, " + "by format %s: %s" % (key, value, + _ftype.__class__.__name__, + e)) + else: + config[key] = value + return config + + # methods for adding configuration + + def get_default_values(self): + """ + return a config options from configuration defaults + """ + defaults = {} + for key, value in self.items(): + if 'default' in value: + defaults[key] = value['default'] + return copy.deepcopy(defaults) + + def get_adaptor(self, format): + """ + get specified format adaptor or + guess for a given filename + """ + adaptor = self.adaptors.get(format, None) + if adaptor: + return adaptor + + # not registered in adaptors dict, let's try all + for adaptor in self.adaptors.values(): + if format in adaptor.extensions: + return adaptor + + def filename2format(self, filename): + extension = os.path.splitext(filename)[-1] + return extension.lstrip('.') or None + + def serialize(self, filename, format=None, full=False): + if not format: + format = self._format + if not format: + format = self.filename2format(filename) + if not format: + raise Exception('Please specify a format') + # TODO: more specific exception type + + adaptor = self.get_adaptor(format) + if not adaptor: + raise Exception("Adaptor not found for format: %s" % format) + + config = copy.deepcopy(self.config) + serializable = self.prep_value(config) + adaptor.write(serializable, filename) + + if self.mtime: + self.touch_mtime(filename) + + def touch_mtime(self, filename): + mtime = self.mtime + os.utime(filename, (mtime, mtime)) + + def deserialize(self, string=None, fromfile=None, format=None): + """ + load configuration from a file or string + """ + + def _try_deserialize(): + if fromfile: + with open(fromfile, 'r') as f: + content = adaptor.read(f) + elif string: + content = adaptor.read(string) + return content + + # XXX cleanup this! + + if fromfile: + leap_assert(os.path.exists(fromfile)) + if not format: + format = self.filename2format(fromfile) + + if not format: + format = self._format + if format: + adaptor = self.get_adaptor(format) + else: + adaptor = None + + if adaptor: + content = _try_deserialize() + return content + + # no adaptor, let's try rest of adaptors + + adaptors = self.adaptors[:] + + if format: + adaptors.sort( + key=lambda x: int( + format in x.extensions), + reverse=True) + + for adaptor in adaptors: + content = _try_deserialize() + return content + + def set_dirty(self): + self.dirty = True + + def is_dirty(self): + return self.dirty + + def load(self, *args, **kwargs): + """ + load from string or file + if no string of fromfile option is given, + it will attempt to load from defaults + defined in the schema. + """ + string = args[0] if args else None + fromfile = kwargs.get("fromfile", None) + mtime = kwargs.pop("mtime", None) + self.mtime = mtime + content = None + + # start with defaults, so we can + # have partial values applied. + content = self.get_default_values() + if string and isinstance(string, str): + content = self.deserialize(string) + + if not string and fromfile is not None: + #import ipdb;ipdb.set_trace() + content = self.deserialize(fromfile=fromfile) + + if not content: + logger.error('no content could be loaded') + # XXX raise! + return + + # lazy evaluation until first level of nesting + # to allow lambdas with context-dependant info + # like os.path.expanduser + for k, v in content.iteritems(): + if callable(v): + content[k] = v() + + self.validate(content) + self.config = content + return True + + +def testmain(): # pragma: no cover + + from tests import test_validation as t + import pprint + + config = PluggableConfig(_format="json") + properties = copy.deepcopy(t.sample_spec) + + config.options = properties + config.load(fromfile='data.json') + + print 'config' + pprint.pprint(config.config) + + config.serialize('/tmp/testserial.json') + +if __name__ == "__main__": + testmain() diff --git a/src/leap/common/config/prefixers.py b/src/leap/common/config/prefixers.py new file mode 100644 index 0000000..050d4cd --- /dev/null +++ b/src/leap/common/config/prefixers.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# prefixers.py +# Copyright (C) 2013 LEAP +# +# This program 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. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +""" +Platform dependant configuration path prefixers +""" +import os +import platform + +from abc import ABCMeta, abstractmethod +from xdg import BaseDirectory + +from leap.common.check import leap_assert + + +class Prefixer: + """ + Abstract prefixer class + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def get_path_prefix(self, standalone=False): + """ + Returns the platform dependant path prefixer + + :param standalone: if True it will return the prefix for a + standalone application. Otherwise, it will return the system + default for configuration storage. + :type standalone: bool + """ + return "" + + +def get_platform_prefixer(): + prefixer = globals()[platform.system() + "Prefixer"] + leap_assert(prefixer, "Unimplemented platform prefixer: %s" % + (platform.system(),)) + return prefixer() + + +class LinuxPrefixer(Prefixer): + """ + Config prefixer for the Linux platform + """ + + def get_path_prefix(self, standalone=False): + """ + Returns the platform dependant path prefixer. + This method expects an env variable named LEAP_CLIENT_PATH if + standalone is used. + + :param standalone: if True it will return the prefix for a + standalone application. Otherwise, it will return the system + default for configuration storage. + :type standalone: bool + """ + config_dir = BaseDirectory.xdg_config_home + if not standalone: + return config_dir + return os.path.join(os.getcwd(), "config") + + +class DarwinPrefixer(Prefixer): + """ + Config prefixer for the Darwin platform + """ + + def get_path_prefix(self, standalone=False): + """ + Returns the platform dependant path prefixer. + This method expects an env variable named LEAP_CLIENT_PATH if + standalone is used. + + :param standalone: if True it will return the prefix for a + standalone application. Otherwise, it will return the system + default for configuration storage. + :type standalone: bool + """ + config_dir = BaseDirectory.xdg_config_home + if not standalone: + return config_dir + return os.getenv(os.getcwd(), "config") + + +class WindowsPrefixer(Prefixer): + """ + Config prefixer for the Windows platform + """ + + def get_path_prefix(self, standalone=False): + """ + Returns the platform dependant path prefixer. + This method expects an env variable named LEAP_CLIENT_PATH if + standalone is used. + + :param standalone: if True it will return the prefix for a + standalone application. Otherwise, it will return the system + default for configuration storage. + :type standalone: bool + """ + config_dir = BaseDirectory.xdg_config_home + + if not standalone: + return config_dir + return os.path.join(os.getcwd(), "config") + +if __name__ == "__main__": + try: + abs_prefixer = Prefixer() + except Exception as e: + assert isinstance(e, TypeError), "Something went wrong" + print "Abstract Prefixer class is working as expected" + + linux_prefixer = LinuxPrefixer() + print linux_prefixer.get_path_prefix(standalone=True) + print linux_prefixer.get_path_prefix() diff --git a/src/leap/common/events/Makefile b/src/leap/common/events/Makefile index 4f99f35..4f73dea 100644 --- a/src/leap/common/events/Makefile +++ b/src/leap/common/events/Makefile @@ -21,10 +21,11 @@ PROTOC = protoc -all: signal_pb2.py +all: events_pb2.py %_pb2.py: %.proto $(PROTOC) --python_out=./ $< + autopep8 --in-place --aggressive $@ clean: rm -f *_pb2.py diff --git a/src/leap/common/events/README.rst b/src/leap/common/events/README.rst new file mode 100644 index 0000000..813be8b --- /dev/null +++ b/src/leap/common/events/README.rst @@ -0,0 +1,53 @@ +Events mechanism +================ + +The events mechanism allows for "components" to send signal events to each +other by means of a centralized server. Components can register with the +server to receive signals of certain types, and they can also send signals to +the server that will then redistribute these signals to registered components. + + +Listening daemons +----------------- + +Both components and the server listen for incoming messages by using a +listening daemon that runs in its own thread. The server daemon has to be +started explicitly, while components daemon will be started whenever a +component registers with the server to receive messages. + + +How to use it +------------- + +To start the events server: + +>>> from leap.common.events import server +>>> server.ensure_server(port=8090) + +To register a callback to be called when a given signal is raised: + +>>> from leap.common.events import ( +>>> register, +>>> events_pb2 as proto, +>>> ) +>>> +>>> def mycallback(sigreq): +>>> print str(sigreq) +>>> +>>> events.register(signal=proto.CLIENT_UID, callback=mycallback) + +To signal an event: + +>>> from leap.common.events import ( +>>> signal, +>>> events_pb2 as proto, +>>> ) +>>> signal(proto.CLIENT_UID) + +Adding events +------------- + +* Add the new event under enum ``Event`` in ``events.proto`` +* Compile the new protocolbuffers file:: + + make diff --git a/src/leap/common/events/__init__.py b/src/leap/common/events/__init__.py index 27542a9..12416e4 100644 --- a/src/leap/common/events/__init__.py +++ b/src/leap/common/events/__init__.py @@ -15,37 +15,111 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +""" +An events mechanism that allows for signaling of events between components. +""" + +import logging +import socket + + from leap.common.events import ( - signal_pb2, + events_pb2 as proto, + server, + component, + daemon, ) -# the `registered_callbacks` dictionary below should have the following -# format: -# -# { component: [ (uid, callback), ... ], ... } -# -registered_callbacks = {} +logger = logging.getLogger(__name__) + + +def register(signal, callback, uid=None, replace=False, reqcbk=None, + timeout=1000): + """ + Register a callback to be called when the given signal is received. + + Will timeout after timeout ms if response has not been received. The + timeout arg is only used for asynch requests. If a reqcbk callback has + been supplied the timeout arg is not used. The response value will be + returned for a synch request but nothing will be returned for an asynch + request. + + :param signal: the signal that causes the callback to be launched + :type signal: int (see the `events.proto` file) + :param callback: the callback to be called when the signal is received + :type callback: function + :param uid: a unique id for the callback + :type uid: int + :param replace: should an existent callback with same uid be replaced? + :type replace: bool + :param reqcbk: a callback to be called when a response from server is + received + :type reqcbk: function + callback(leap.common.events.events_pb2.EventResponse) + :param timeout: the timeout for synch calls + :type timeout: int + + :return: the response from server for synch calls or nothing for asynch + calls. + :rtype: leap.common.events.events_pb2.EventsResponse or None + """ + return component.register(signal, callback, uid, replace, reqcbk, timeout) + + +def unregister(signal, uid=None, reqcbk=None, timeout=1000): + """ + Unregister a callback. + + If C{uid} is specified, unregisters only the callback identified by that + unique id. Otherwise, unregisters all callbacks registered for C{signal}. + :param signal: the signal that causes the callback to be launched + :type signal: int (see the `events.proto` file) + :param uid: a unique id for the callback + :type uid: int + :param reqcbk: a callback to be called when a response from server is + received + :type reqcbk: function + callback(leap.common.events.events_pb2.EventResponse) + :param timeout: the timeout for synch calls + :type timeout: int -def register(signal, callback, uid=None, replace=False): + :return: the response from server for synch calls or nothing for asynch + calls. + :rtype: leap.common.events.events_pb2.EventsResponse or None """ - Registers `callback` to be called when `signal` is signaled. + return component.unregister(signal, uid, reqcbk, timeout) + + +def signal(signal, content="", mac_method="", mac="", reqcbk=None, + timeout=1000): + """ + Send `signal` event to events server. + + Will timeout after timeout ms if response has not been received. The + timeout arg is only used for asynch requests. If a reqcbk callback has + been supplied the timeout arg is not used. The response value will be + returned for a synch request but nothing will be returned for an asynch + request. + + :param signal: the signal that causes the callback to be launched + :type signal: int (see the `events.proto` file) + :param content: the contents of the event signal + :type content: str + :param mac_method: the method used to auth mac + :type mac_method: str + :param mac: the content of the auth mac + :type mac: str + :param reqcbk: a callback to be called when a response from server is + received + :type reqcbk: function + callback(leap.common.events.events_pb2.EventResponse) + :param timeout: the timeout for synch calls + :type timeout: int + + :return: the response from server for synch calls or nothing for asynch + calls. + :rtype: leap.common.events.events_pb2.EventsResponse or None """ - if not registered_callbacks.has_key(signal): - registered_callbacks[signal] = [] - cbklist = registered_callbacks[signal] - if uid and filter(lambda (x,y): x == uid, cbklist): - # TODO: create appropriate exception - if not replace: - raise Exception("Callback already registered.") - else: - registered_callbacks[signal] = filter(lambda(x,y): x != uid, - cbklist) - registered_callbacks[signal].append((uid, callback)) - return uid - -#def get_registered_callbacks(): -# return registered_callbacks - -#__all__ = ['signal_pb2', 'service', 'register', 'registered_callbacks'] + return component.signal(signal, content, mac_method, mac, reqcbk, timeout) diff --git a/src/leap/common/events/component.py b/src/leap/common/events/component.py new file mode 100644 index 0000000..029d1ac --- /dev/null +++ b/src/leap/common/events/component.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +# component.py +# Copyright (C) 2013 LEAP +# +# This program 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. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +""" +The component end point of the events mechanism. + +Components are the communicating parties of the events mechanism. They +communicate by sending messages to a server, which in turn redistributes +messages to other components. + +When a component registers a callback for a given signal, it also tells the +server that it wants to be notified whenever signals of that type are sent by +some other component. +""" + + +import logging +import threading + + +from protobuf.socketrpc import RpcService +from leap.common.events import ( + events_pb2 as proto, + server, + daemon, + mac_auth, +) + + +logger = logging.getLogger(__name__) + + +# the `registered_callbacks` dictionary below should have the following +# format: +# +# { event_signal: [ (uid, callback), ... ], ... } +# +registered_callbacks = {} + + +class CallbackAlreadyRegistered(Exception): + """ + Raised when trying to register an already registered callback. + """ + pass + + +def ensure_component_daemon(): + """ + Ensure the component daemon is running and listening for incoming + messages. + + :return: the daemon instance + :rtype: EventsComponentDaemon + """ + import time + daemon = EventsComponentDaemon.ensure(0) + logger.debug('ensure component daemon') + + # Because we use a random port we want to wait until a port is assigned to + # local component daemon. + + while not (EventsComponentDaemon.get_instance() and + EventsComponentDaemon.get_instance().get_port()): + time.sleep(0.1) + return daemon + + +def register(signal, callback, uid=None, replace=False, reqcbk=None, + timeout=1000): + """ + Registers a callback to be called when a specific signal event is + received. + + Will timeout after timeout ms if response has not been received. The + timeout arg is only used for asynch requests. If a reqcbk callback has + been supplied the timeout arg is not used. The response value will be + returned for a synch request but nothing will be returned for an asynch + request. + + :param signal: the signal that causes the callback to be launched + :type signal: int (see the `events.proto` file) + :param callback: the callback to be called when the signal is received + :type callback: function + callback(leap.common.events.events_pb2.SignalRequest) + :param uid: a unique id for the callback + :type uid: int + :param replace: should an existent callback with same uid be replaced? + :type replace: bool + :param reqcbk: a callback to be called when a response from server is + received + :type reqcbk: function + callback(leap.common.events.events_pb2.EventResponse) + :param timeout: the timeout for synch calls + :type timeout: int + + Might raise a CallbackAlreadyRegistered exception if there's already a + callback identified by the given uid and replace is False. + + :return: the response from server for synch calls or nothing for asynch + calls. + :rtype: leap.common.events.events_pb2.EventsResponse or None + """ + ensure_component_daemon() # so we can receive registered signals + # register callback locally + if signal not in registered_callbacks: + registered_callbacks[signal] = [] + cbklist = registered_callbacks[signal] + if uid and filter(lambda (x, y): x == uid, cbklist): + if not replace: + raise CallbackAlreadyRegisteredException() + else: + registered_callbacks[signal] = filter(lambda(x, y): x != uid, + cbklist) + registered_callbacks[signal].append((uid, callback)) + # register callback on server + request = proto.RegisterRequest() + request.event = signal + request.port = EventsComponentDaemon.get_instance().get_port() + request.mac_method = mac_auth.MacMethod.MAC_NONE + request.mac = "" + service = RpcService(proto.EventsServerService_Stub, + server.SERVER_PORT, 'localhost') + logger.info( + "Sending registration request to server on port %s: %s", + server.SERVER_PORT, + str(request)[:40]) + return service.register(request, callback=reqcbk, timeout=timeout) + +def unregister(signal, uid=None, reqcbk=None, timeout=1000): + """ + Unregister a callback. + + If C{uid} is specified, unregisters only the callback identified by that + unique id. Otherwise, unregisters all callbacks + + :param signal: the signal that causes the callback to be launched + :type signal: int (see the `events.proto` file) + :param uid: a unique id for the callback + :type uid: int + :param reqcbk: a callback to be called when a response from server is + received + :type reqcbk: function + callback(leap.common.events.events_pb2.EventResponse) + :param timeout: the timeout for synch calls + :type timeout: int + + :return: the response from server for synch calls or nothing for asynch + calls or None if no callback is registered for that signal or uid. + :rtype: leap.common.events.events_pb2.EventsResponse or None + """ + if signal not in registered_callbacks or not registered_callbacks[signal]: + logger.warning("No callback registered for signal %d." % signal) + return None + # unregister callback locally + cbklist = registered_callbacks[signal] + if uid is not None: + if filter(lambda (cbkuid, _): cbkuid == uid, cbklist) == []: + logger.warning("No callback registered for uid %d." % st) + return None + registered_callbacks[signal] = filter(lambda(x, y): x != uid, cbklist) + else: + # exclude all callbacks for given signal + registered_callbacks[signal] = [] + # unregister port in server if there are no more callbacks for this signal + if not registered_callbacks[signal]: + request = proto.UnregisterRequest() + request.event = signal + request.port = EventsComponentDaemon.get_instance().get_port() + request.mac_method = mac_auth.MacMethod.MAC_NONE + request.mac = "" + service = RpcService(proto.EventsServerService_Stub, + server.SERVER_PORT, 'localhost') + logger.info( + "Sending unregistration request to server on port %s: %s", + server.SERVER_PORT, + str(request)[:40]) + return service.unregister(request, callback=reqcbk, timeout=timeout) + + +def signal(signal, content="", mac_method="", mac="", reqcbk=None, + timeout=1000): + """ + Send `signal` event to events server. + + Will timeout after timeout ms if response has not been received. The + timeout arg is only used for asynch requests. If a reqcbk callback has + been supplied the timeout arg is not used. The response value will be + returned for a synch request but nothing will be returned for an asynch + request. + + :param signal: the signal that causes the callback to be launched + :type signal: int (see the `events.proto` file) + :param content: the contents of the event signal + :type content: str + :param mac_method: the method used for auth mac + :type mac_method: str + :param mac: the content of the auth mac + :type mac: str + :param reqcbk: a callback to be called when a response from server is + received + :type reqcbk: function + callback(leap.common.events.events_pb2.EventResponse) + :param timeout: the timeout for synch calls + :type timeout: int + + :return: the response from server for synch calls or nothing for asynch + calls. + :rtype: leap.common.events.events_pb2.EventsResponse or None + """ + request = proto.SignalRequest() + request.event = signal + request.content = content + request.mac_method = mac_method + request.mac = mac + service = RpcService(proto.EventsServerService_Stub, server.SERVER_PORT, + 'localhost') + logger.info("Sending signal to server: %s", str(request)[:40]) + return service.signal(request, callback=reqcbk, timeout=timeout) + + +class EventsComponentService(proto.EventsComponentService): + """ + Service for receiving signal events in components. + """ + + def __init__(self): + proto.EventsComponentService.__init__(self) + + def signal(self, controller, request, done): + """ + Receive a signal and run callbacks registered for that signal. + + This method is called whenever a signal request is received from + server. + + :param controller: used to mediate a single method call + :type controller: protobuf.socketrpc.controller.SocketRpcController + :param request: the request received from the component + :type request: leap.common.events.events_pb2.SignalRequest + :param done: callback to be called when done + :type done: protobuf.socketrpc.server.Callback + """ + logger.info('Received signal from server: %s...' % str(request)[:40]) + + # run registered callbacks + # TODO: verify authentication using mac in incoming message + if request.event in registered_callbacks: + for (_, cbk) in registered_callbacks[request.event]: + # callbacks should be prepared to receive a + # events_pb2.SignalRequest. + cbk(request) + + # send response back to server + response = proto.EventResponse() + response.status = proto.EventResponse.OK + done.run(response) + + +class EventsComponentDaemon(daemon.EventsSingletonDaemon): + """ + A daemon that listens for incoming events from server. + """ + + @classmethod + def ensure(cls, port): + """ + Make sure the daemon is running on the given port. + + :param port: the port in which the daemon should listen + :type port: int + + :return: a daemon instance + :rtype: EventsComponentDaemon + """ + return cls.ensure_service(port, EventsComponentService()) diff --git a/src/leap/common/events/daemon.py b/src/leap/common/events/daemon.py new file mode 100644 index 0000000..c253948 --- /dev/null +++ b/src/leap/common/events/daemon.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# daemon.py +# Copyright (C) 2013 LEAP +# +# This program 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. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +""" +A singleton daemon for running RPC services using protobuf.socketrpc. +""" + + +import logging +import threading + + +from protobuf.socketrpc.server import ( + SocketRpcServer, + ThreadedTCPServer, + SocketHandler, +) + + +logger = logging.getLogger(__name__) + + +class ServiceAlreadyRunningException(Exception): + """ + Raised whenever a service is already running in this process but someone + attemped to start it in a different port. + """ + + +class EventsRpcServer(SocketRpcServer): + """ + RPC server used in server and component interfaces to receive messages. + """ + + def __init__(self, port, host='localhost'): + """ + Initialize a RPC server. + + :param port: the port in which to listen for incoming messages + :type port: int + :param host: the address to bind to + :type host: str + """ + SocketRpcServer.__init__(self, port, host) + self._server = None + + def run(self): + """ + Run the server. + """ + logger.info('Running server on port %d.' % self.port) + # parent implementation does not hold the server instance, so we do it + # here. + self._server = ThreadedTCPServer((self.host, self.port), + SocketHandler, self) + # if we chose to use a random port, fetch the port number info. + if self.port is 0: + self.port = self._server.socket.getsockname()[1] + self._server.serve_forever() + + def stop(self): + """ + Stop the server. + """ + self._server.shutdown() + + +class EventsSingletonDaemon(threading.Thread): + """ + Singleton class for for launching and terminating a daemon. + + This class is used so every part of the mechanism that needs to listen for + messages can launch its own daemon (thread) to do the job. + """ + + # Singleton instance + __instance = None + + def __new__(cls, *args, **kwargs): + """ + Return a singleton instance if it exists or create and initialize one. + """ + if len(args) is not 2: + raise TypeError("__init__() takes exactly 2 arguments (%d given)" + % len(args)) + if cls.__instance is None: + cls.__instance = object.__new__( + EventsSingletonDaemon) + cls.__initialize(cls.__instance, args[0], args[1]) + return cls.__instance + + @staticmethod + def __initialize(self, port, service): + """ + Initialize a singleton daemon. + + This is a static method disguised as instance method that actually + does the initialization of the daemon instance. + + :param port: the port in which to listen for incoming messages + :type port: int + :param service: the service to provide in this daemon + :type service: google.protobuf.service.Service + """ + threading.Thread.__init__(self) + self._port = port + self._service = service + self._server = EventsRpcServer(self._port) + self._server.registerService(self._service) + self.daemon = True + + def __init__(self): + """ + Singleton placeholder initialization method. + + Initialization is made in __new__ so we can always return the same + instance upon object creation. + """ + pass + + @classmethod + def ensure(cls, port): + """ + Make sure the daemon instance is running. + + Each implementation of this method should call `self.ensure_service` + with the appropriate service from the `events.proto` definitions, and + return the daemon instance. + + :param port: the port in which the daemon should be listening + :type port: int + + :return: a daemon instance + :rtype: EventsSingletonDaemon + """ + raise NotImplementedError(self.ensure) + + @classmethod + def ensure_service(cls, port, service): + """ + Start the singleton instance if not already running. + + Might return ServiceAlreadyRunningException + + :param port: the port in which the daemon should be listening + :type port: int + + :return: a daemon instance + :rtype: EventsSingletonDaemon + """ + daemon = cls(port, service) + if not daemon.is_alive(): + daemon.start() + elif port and port != cls.__instance._port: + # service is running in this process but someone is trying to + # start it in another port + raise ServiceAlreadyRunningException( + "Service is already running in this process on port %d." + % self.__instance._port) + return daemon + + @classmethod + def get_instance(cls): + """ + Retrieve singleton instance of this daemon. + + :return: a daemon instance + :rtype: EventsSingletonDaemon + """ + return cls.__instance + + def run(self): + """ + Run the server. + """ + self._server.run() + + def stop(self): + """ + Stop the daemon. + """ + self._server.stop() + + def get_port(self): + """ + Retrieve the value of the port to which the service running in this + daemon is binded to. + + :return: the port to which the daemon is binded to + :rtype: int + """ + if self._port is 0: + self._port = self._server.port + return self._port diff --git a/src/leap/common/events/events.proto b/src/leap/common/events/events.proto new file mode 100644 index 0000000..a813ed1 --- /dev/null +++ b/src/leap/common/events/events.proto @@ -0,0 +1,79 @@ +// signal.proto +// Copyright (C) 2013 LEAP +// +// This program 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. +// +// This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +package leap.common.events; +option py_generic_services = true; + +enum Event { + CLIENT_SESSION_ID = 1; + CLIENT_UID = 2; + SOLEDAD_CREATING_KEYS = 3; + SOLEDAD_DONE_CREATING_KEYS = 4; + SOLEDAD_UPLOADING_KEYS = 5; + SOLEDAD_DONE_UPLOADING_KEYS = 6; + SOLEDAD_DOWNLOADING_KEYS = 7; + SOLEDAD_DONE_DOWNLOADING_KEYS = 8; + SOLEDAD_NEW_DATA_TO_SYNC = 9; + SOLEDAD_DONE_DATA_SYNC = 10; + UPDATER_NEW_UPDATES = 11; + UPDATER_DONE_UPDATING = 12; + RAISE_WINDOW = 13; +} + +message SignalRequest { + required Event event = 1; + required string content = 2; + required string mac_method = 3; + required bytes mac = 4; + optional string enc_method = 5; + optional bool error_occurred = 6; +} + +message RegisterRequest { + required Event event = 1; + required int32 port = 2; + required string mac_method = 3; + required bytes mac = 4; +} + +message UnregisterRequest { + required Event event = 1; + required int32 port = 2; + required string mac_method = 3; + required bytes mac = 4; +} + +message EventResponse { + + enum Status { + OK = 1; + UNAUTH = 2; + ERROR = 3; + } + + required Status status = 1; + optional string result = 2; +} + +service EventsServerService { + rpc register(RegisterRequest) returns (EventResponse); + rpc unregister(UnregisterRequest) returns (EventResponse); + rpc signal(SignalRequest) returns (EventResponse); +} + +service EventsComponentService { + rpc signal(SignalRequest) returns (EventResponse); +} diff --git a/src/leap/common/events/events_pb2.py b/src/leap/common/events/events_pb2.py new file mode 100644 index 0000000..5b1c118 --- /dev/null +++ b/src/leap/common/events/events_pb2.py @@ -0,0 +1,444 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: events.proto + +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import service as _service +from google.protobuf import service_reflection +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='events.proto', + package='leap.common.events', + serialized_pb='\n\x0c\x65vents.proto\x12\x12leap.common.events\"\x97\x01\n\rSignalRequest\x12(\n\x05\x65vent\x18\x01 \x02(\x0e\x32\x19.leap.common.events.Event\x12\x0f\n\x07\x63ontent\x18\x02 \x02(\t\x12\x12\n\nmac_method\x18\x03 \x02(\t\x12\x0b\n\x03mac\x18\x04 \x02(\x0c\x12\x12\n\nenc_method\x18\x05 \x01(\t\x12\x16\n\x0e\x65rror_occurred\x18\x06 \x01(\x08\"j\n\x0fRegisterRequest\x12(\n\x05\x65vent\x18\x01 \x02(\x0e\x32\x19.leap.common.events.Event\x12\x0c\n\x04port\x18\x02 \x02(\x05\x12\x12\n\nmac_method\x18\x03 \x02(\t\x12\x0b\n\x03mac\x18\x04 \x02(\x0c\"l\n\x11UnregisterRequest\x12(\n\x05\x65vent\x18\x01 \x02(\x0e\x32\x19.leap.common.events.Event\x12\x0c\n\x04port\x18\x02 \x02(\x05\x12\x12\n\nmac_method\x18\x03 \x02(\t\x12\x0b\n\x03mac\x18\x04 \x02(\x0c\"\x82\x01\n\rEventResponse\x12\x38\n\x06status\x18\x01 \x02(\x0e\x32(.leap.common.events.EventResponse.Status\x12\x0e\n\x06result\x18\x02 \x01(\t\"\'\n\x06Status\x12\x06\n\x02OK\x10\x01\x12\n\n\x06UNAUTH\x10\x02\x12\t\n\x05\x45RROR\x10\x03*\xe7\x02\n\x05\x45vent\x12\x15\n\x11\x43LIENT_SESSION_ID\x10\x01\x12\x0e\n\nCLIENT_UID\x10\x02\x12\x19\n\x15SOLEDAD_CREATING_KEYS\x10\x03\x12\x1e\n\x1aSOLEDAD_DONE_CREATING_KEYS\x10\x04\x12\x1a\n\x16SOLEDAD_UPLOADING_KEYS\x10\x05\x12\x1f\n\x1bSOLEDAD_DONE_UPLOADING_KEYS\x10\x06\x12\x1c\n\x18SOLEDAD_DOWNLOADING_KEYS\x10\x07\x12!\n\x1dSOLEDAD_DONE_DOWNLOADING_KEYS\x10\x08\x12\x1c\n\x18SOLEDAD_NEW_DATA_TO_SYNC\x10\t\x12\x1a\n\x16SOLEDAD_DONE_DATA_SYNC\x10\n\x12\x17\n\x13UPDATER_NEW_UPDATES\x10\x0b\x12\x19\n\x15UPDATER_DONE_UPDATING\x10\x0c\x12\x10\n\x0cRAISE_WINDOW\x10\r2\x91\x02\n\x13\x45ventsServerService\x12R\n\x08register\x12#.leap.common.events.RegisterRequest\x1a!.leap.common.events.EventResponse\x12V\n\nunregister\x12%.leap.common.events.UnregisterRequest\x1a!.leap.common.events.EventResponse\x12N\n\x06signal\x12!.leap.common.events.SignalRequest\x1a!.leap.common.events.EventResponse2h\n\x16\x45ventsComponentService\x12N\n\x06signal\x12!.leap.common.events.SignalRequest\x1a!.leap.common.events.EventResponseB\x03\x90\x01\x01') + +_EVENT = _descriptor.EnumDescriptor( + name='Event', + full_name='leap.common.events.Event', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='CLIENT_SESSION_ID', index=0, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='CLIENT_UID', index=1, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOLEDAD_CREATING_KEYS', index=2, number=3, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOLEDAD_DONE_CREATING_KEYS', index=3, number=4, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOLEDAD_UPLOADING_KEYS', index=4, number=5, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOLEDAD_DONE_UPLOADING_KEYS', index=5, number=6, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOLEDAD_DOWNLOADING_KEYS', index=6, number=7, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOLEDAD_DONE_DOWNLOADING_KEYS', index=7, number=8, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOLEDAD_NEW_DATA_TO_SYNC', index=8, number=9, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOLEDAD_DONE_DATA_SYNC', index=9, number=10, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='UPDATER_NEW_UPDATES', index=10, number=11, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='UPDATER_DONE_UPDATING', index=11, number=12, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='RAISE_WINDOW', index=12, number=13, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=542, + serialized_end=901, +) + +Event = enum_type_wrapper.EnumTypeWrapper(_EVENT) +CLIENT_SESSION_ID = 1 +CLIENT_UID = 2 +SOLEDAD_CREATING_KEYS = 3 +SOLEDAD_DONE_CREATING_KEYS = 4 +SOLEDAD_UPLOADING_KEYS = 5 +SOLEDAD_DONE_UPLOADING_KEYS = 6 +SOLEDAD_DOWNLOADING_KEYS = 7 +SOLEDAD_DONE_DOWNLOADING_KEYS = 8 +SOLEDAD_NEW_DATA_TO_SYNC = 9 +SOLEDAD_DONE_DATA_SYNC = 10 +UPDATER_NEW_UPDATES = 11 +UPDATER_DONE_UPDATING = 12 +RAISE_WINDOW = 13 + + +_EVENTRESPONSE_STATUS = _descriptor.EnumDescriptor( + name='Status', + full_name='leap.common.events.EventResponse.Status', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='OK', index=0, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='UNAUTH', index=1, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='ERROR', index=2, number=3, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=500, + serialized_end=539, +) + + +_SIGNALREQUEST = _descriptor.Descriptor( + name='SignalRequest', + full_name='leap.common.events.SignalRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='event', full_name='leap.common.events.SignalRequest.event', index=0, + number=1, type=14, cpp_type=8, label=2, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='content', full_name='leap.common.events.SignalRequest.content', index=1, + number=2, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='mac_method', full_name='leap.common.events.SignalRequest.mac_method', index=2, + number=3, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='mac', full_name='leap.common.events.SignalRequest.mac', index=3, + number=4, type=12, cpp_type=9, label=2, + has_default_value=False, default_value="", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='enc_method', full_name='leap.common.events.SignalRequest.enc_method', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='error_occurred', full_name='leap.common.events.SignalRequest.error_occurred', index=5, + number=6, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=37, + serialized_end=188, +) + + +_REGISTERREQUEST = _descriptor.Descriptor( + name='RegisterRequest', + full_name='leap.common.events.RegisterRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='event', full_name='leap.common.events.RegisterRequest.event', index=0, + number=1, type=14, cpp_type=8, label=2, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='port', full_name='leap.common.events.RegisterRequest.port', index=1, + number=2, type=5, cpp_type=1, label=2, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='mac_method', full_name='leap.common.events.RegisterRequest.mac_method', index=2, + number=3, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='mac', full_name='leap.common.events.RegisterRequest.mac', index=3, + number=4, type=12, cpp_type=9, label=2, + has_default_value=False, default_value="", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=190, + serialized_end=296, +) + + +_UNREGISTERREQUEST = _descriptor.Descriptor( + name='UnregisterRequest', + full_name='leap.common.events.UnregisterRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='event', full_name='leap.common.events.UnregisterRequest.event', index=0, + number=1, type=14, cpp_type=8, label=2, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='port', full_name='leap.common.events.UnregisterRequest.port', index=1, + number=2, type=5, cpp_type=1, label=2, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='mac_method', full_name='leap.common.events.UnregisterRequest.mac_method', index=2, + number=3, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='mac', full_name='leap.common.events.UnregisterRequest.mac', index=3, + number=4, type=12, cpp_type=9, label=2, + has_default_value=False, default_value="", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=298, + serialized_end=406, +) + + +_EVENTRESPONSE = _descriptor.Descriptor( + name='EventResponse', + full_name='leap.common.events.EventResponse', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='leap.common.events.EventResponse.status', index=0, + number=1, type=14, cpp_type=8, label=2, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='result', full_name='leap.common.events.EventResponse.result', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _EVENTRESPONSE_STATUS, + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=409, + serialized_end=539, +) + +_SIGNALREQUEST.fields_by_name['event'].enum_type = _EVENT +_REGISTERREQUEST.fields_by_name['event'].enum_type = _EVENT +_UNREGISTERREQUEST.fields_by_name['event'].enum_type = _EVENT +_EVENTRESPONSE.fields_by_name['status'].enum_type = _EVENTRESPONSE_STATUS +_EVENTRESPONSE_STATUS.containing_type = _EVENTRESPONSE +DESCRIPTOR.message_types_by_name['SignalRequest'] = _SIGNALREQUEST +DESCRIPTOR.message_types_by_name['RegisterRequest'] = _REGISTERREQUEST +DESCRIPTOR.message_types_by_name['UnregisterRequest'] = _UNREGISTERREQUEST +DESCRIPTOR.message_types_by_name['EventResponse'] = _EVENTRESPONSE + + +class SignalRequest(_message.Message): + __metaclass__ = _reflection.GeneratedProtocolMessageType + DESCRIPTOR = _SIGNALREQUEST + + # @@protoc_insertion_point(class_scope:leap.common.events.SignalRequest) + + +class RegisterRequest(_message.Message): + __metaclass__ = _reflection.GeneratedProtocolMessageType + DESCRIPTOR = _REGISTERREQUEST + + # @@protoc_insertion_point(class_scope:leap.common.events.RegisterRequest) + + +class UnregisterRequest(_message.Message): + __metaclass__ = _reflection.GeneratedProtocolMessageType + DESCRIPTOR = _UNREGISTERREQUEST + + # @@protoc_insertion_point(class_scope:leap.common.events.UnregisterRequest) + + +class EventResponse(_message.Message): + __metaclass__ = _reflection.GeneratedProtocolMessageType + DESCRIPTOR = _EVENTRESPONSE + + # @@protoc_insertion_point(class_scope:leap.common.events.EventResponse) + + +DESCRIPTOR.has_options = True +DESCRIPTOR._options = _descriptor._ParseOptions( + descriptor_pb2.FileOptions(), '\220\001\001') + +_EVENTSSERVERSERVICE = _descriptor.ServiceDescriptor( + name='EventsServerService', + full_name='leap.common.events.EventsServerService', + file=DESCRIPTOR, + index=0, + options=None, + serialized_start=904, + serialized_end=1177, + methods=[ + _descriptor.MethodDescriptor( + name='register', + full_name='leap.common.events.EventsServerService.register', + index=0, + containing_service=None, + input_type=_REGISTERREQUEST, + output_type=_EVENTRESPONSE, + options=None, + ), + _descriptor.MethodDescriptor( + name='unregister', + full_name='leap.common.events.EventsServerService.unregister', + index=1, + containing_service=None, + input_type=_UNREGISTERREQUEST, + output_type=_EVENTRESPONSE, + options=None, + ), + _descriptor.MethodDescriptor( + name='signal', + full_name='leap.common.events.EventsServerService.signal', + index=2, + containing_service=None, + input_type=_SIGNALREQUEST, + output_type=_EVENTRESPONSE, + options=None, + ), + ]) + + +class EventsServerService(_service.Service): + __metaclass__ = service_reflection.GeneratedServiceType + DESCRIPTOR = _EVENTSSERVERSERVICE + + +class EventsServerService_Stub(EventsServerService): + __metaclass__ = service_reflection.GeneratedServiceStubType + DESCRIPTOR = _EVENTSSERVERSERVICE + + +_EVENTSCOMPONENTSERVICE = _descriptor.ServiceDescriptor( + name='EventsComponentService', + full_name='leap.common.events.EventsComponentService', + file=DESCRIPTOR, + index=1, + options=None, + serialized_start=1179, + serialized_end=1283, + methods=[ + _descriptor.MethodDescriptor( + name='signal', + full_name='leap.common.events.EventsComponentService.signal', + index=0, + containing_service=None, + input_type=_SIGNALREQUEST, + output_type=_EVENTRESPONSE, + options=None, + ), + ]) + + +class EventsComponentService(_service.Service): + __metaclass__ = service_reflection.GeneratedServiceType + DESCRIPTOR = _EVENTSCOMPONENTSERVICE + + +class EventsComponentService_Stub(EventsComponentService): + __metaclass__ = service_reflection.GeneratedServiceStubType + DESCRIPTOR = _EVENTSCOMPONENTSERVICE + +# @@protoc_insertion_point(module_scope) diff --git a/src/leap/common/events/mac_auth.py b/src/leap/common/events/mac_auth.py new file mode 100644 index 0000000..49d48f7 --- /dev/null +++ b/src/leap/common/events/mac_auth.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# mac_auth.py +# Copyright (C) 2013 LEAP +# +# This program 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. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +""" +Authentication system for events. + +This is not implemented yet. +""" + + +class MacMethod(object): + """ + Representation of possible MAC authentication methods. + """ + + MAC_NONE = 'none' + MAC_HMAC = 'hmac' diff --git a/src/leap/common/events/server.py b/src/leap/common/events/server.py new file mode 100644 index 0000000..d53c218 --- /dev/null +++ b/src/leap/common/events/server.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# server.py +# Copyright (C) 2013 LEAP +# +# This program 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. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. +""" +A server for the events mechanism. + +A server can receive different kinds of requests from components: + + 1. Registration request: store component port number to be notified when + a specific signal arrives. + + 2. Signal request: redistribute the signal to registered components. +""" +import logging +import socket + + +from protobuf.socketrpc import RpcService +from leap.common.events import ( + events_pb2 as proto, + daemon, +) + + +logger = logging.getLogger(__name__) + + +SERVER_PORT = 8090 + +# the `registered_components` dictionary below should have the following +# format: +# +# { event_signal: [ port, ... ], ... } +# +registered_components = {} + + +def ensure_server(port=SERVER_PORT): + """ + Make sure the server is running on the given port. + + Attempt to connect to given local port. Upon success, assume that the + events server has already been started. Upon failure, start events server. + + :param port: the port in which server should be listening + :type port: int + + :return: the daemon instance or nothing + :rtype: EventsServerDaemon or None + """ + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(('localhost', port)) + s.close() + logger.info('Server is already running on port %d.', port) + return None + except socket.error: + logger.info('Launching server on port %d.', port) + return EventsServerDaemon.ensure(port) + + +class EventsServerService(proto.EventsServerService): + """ + Service for receiving events in components. + """ + + def register(self, controller, request, done): + """ + Register a component port to be signaled when specific events come in. + + :param controller: used to mediate a single method call + :type controller: protobuf.socketrpc.controller.SocketRpcController + :param request: the request received from the component + :type request: leap.common.events.events_pb2.RegisterRequest + :param done: callback to be called when done + :type done: protobuf.socketrpc.server.Callback + """ + logger.info("Received registration request: %s..." % str(request)[:40]) + # add component port to signal list + if request.event not in registered_components: + registered_components[request.event] = set([]) + registered_components[request.event].add(request.port) + # send response back to component + + logger.debug('sending response back') + response = proto.EventResponse() + response.status = proto.EventResponse.OK + done.run(response) + + def unregister(self, controller, request, done): + """ + Unregister a component port so it will not be signaled when specific + events come in. + + :param controller: used to mediate a single method call + :type controller: protobuf.socketrpc.controller.SocketRpcController + :param request: the request received from the component + :type request: leap.common.events.events_pb2.RegisterRequest + :param done: callback to be called when done + :type done: protobuf.socketrpc.server.Callback + """ + logger.info( + "Received unregistration request: %s..." % str(request)[:40]) + # remove component port from signal list + response = proto.EventResponse() + if request.event in registered_components: + try: + registered_components[request.event].remove(request.port) + response.status = proto.EventResponse.OK + except KeyError: + response.status = proto.EventsResponse.ERROR + response.result = 'Port %d not registered.' % request.port + # send response back to component + logger.debug('sending response back') + done.run(response) + + def signal(self, controller, request, done): + """ + Perform an RPC call to signal all components registered to receive a + specific signal. + + :param controller: used to mediate a single method call + :type controller: protobuf.socketrpc.controller.SocketRpcController + :param request: the request received from the component + :type request: leap.common.events.events_pb2.SignalRequest + :param done: callback to be called when done + :type done: protobuf.socketrpc.server.Callback + """ + logger.info('Received signal from component: %s...', str(request)[:40]) + # send signal to all registered components + # TODO: verify signal auth + if request.event in registered_components: + for port in registered_components[request.event]: + + def callback(req, resp): + logger.info("Signal received by " + str(port)) + + service = RpcService(proto.EventsComponentService_Stub, + port, 'localhost') + service.signal(request, callback=callback) + # send response back to component + response = proto.EventResponse() + response.status = proto.EventResponse.OK + done.run(response) + + +class EventsServerDaemon(daemon.EventsSingletonDaemon): + """ + Singleton class for starting an events server daemon. + """ + + @classmethod + def ensure(cls, port): + """ + Make sure the daemon is running on the given port. + + :param port: the port in which the daemon should listen + :type port: int + + :return: a daemon instance + :rtype: EventsServerDaemon + """ + return cls.ensure_service(port, EventsServerService()) diff --git a/src/leap/common/events/service.py b/src/leap/common/events/service.py deleted file mode 100644 index fda45b2..0000000 --- a/src/leap/common/events/service.py +++ /dev/null @@ -1,114 +0,0 @@ -# -*- coding: utf-8 -*- -# service.py -# Copyright (C) 2013 LEAP -# -# This program 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. -# -# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -import logging -import threading -from protobuf.socketrpc.server import ( - SocketRpcServer, - ThreadedTCPServer, - SocketHandler, -) -from leap.common.events import ( - signal_pb2 as proto, - registered_callbacks, -) - - -logger = logging.getLogger(__name__) - - -class SignalRpcServer(SocketRpcServer): - - def __init__(self, port, host='localhost'): - '''port - Port this server is started on''' - self.port = port - self.host = host - self.serviceMap = {} - self.server = None - - def run(self): - '''Activate the server.''' - logger.info('Running server on port %d' % self.port) - self.server = ThreadedTCPServer((self.host, self.port), - SocketHandler, self) - self.server.serve_forever() - - def stop(self): - self.server.shutdown() - - -class SignalService(proto.SignalService): - ''' - Handles signaling for LEAP components. - ''' - - def signal(self, controller, request, done): - logger.info('Received signal.') - - # Run registered callbacks - if registered_callbacks.has_key(request.signal): - for (_, cbk) in registered_callbacks[request.signal]: - cbk(request) - - # Create response message - response = proto.SignalResponse() - # TODO: change id for something meaningful - response.id = 1 - response.status = proto.SignalResponse.OK - - # Call provided callback with response message - done.run(response) - - -class SignalServiceThread(threading.Thread): - """ - Singleton class for starting a server thread - """ - - # Singleton instance - _instance = None - - def __init__(self, port): - super(SignalServiceThread, self).__init__() - self._service = SignalService() - self._port = port - self._server = SignalRpcServer(self._port) - self._server.registerService(self._service) - self.setDaemon(True) - - @staticmethod - def start_service(port): - """ - Start the singleton instance if not already running - Will not exit until the process ends - """ - if SignalServiceThread._instance == None: - SignalServiceThread._instance = SignalServiceThread(port) - SignalServiceThread._instance.start() - elif port != SignalServiceThread._instance._port: - # TODO: make this exception more self-explanatory - raise Exception() - return SignalServiceThread._instance - - def get_instance(self): - return self._instance - - def run(self): - self._server.run() - - def stop(self): - self._server.stop() diff --git a/src/leap/common/events/signal.proto b/src/leap/common/events/signal.proto deleted file mode 100644 index 336471c..0000000 --- a/src/leap/common/events/signal.proto +++ /dev/null @@ -1,57 +0,0 @@ -// signal.proto -// Copyright (C) 2013 LEA -// -// This program 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. -// -// This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - -package leap.common.events; - -message SignalRequest { - - enum Signal { - CLIENT_SESSION_ID = 1; - CLIENT_UID = 2; - SOLEDAD_CREATING_KEYS = 3; - SOLEDAD_DONE_CREATING_KEYS = 4; - SOLEDAD_UPLOADING_KEYS = 5; - SOLEDAD_DONE_UPLOADING_KEYS = 6; - SOLEDAD_DOWNLOADING_KEYS = 7; - SOLEDAD_DONE_DOWNLOADING_KEYS = 8; - SOLEDAD_NEW_DATA_TO_SYNC = 9; - SOLEDAD_DONE_DATA_SYNC = 10; - } - - required int32 id = 1; - required Signal signal = 2; - required string content = 3; - required string mac_method = 4; - required bytes mac = 5; - optional string enc_method = 6; - optional bool error_occurred = 7; -} - -message SignalResponse { - - enum Status { - OK = 1; - UNAUTH = 2; - ERROR = 3; - } - - required int32 id = 1; - required Status status = 2; -} - -service SignalService { - rpc signal(SignalRequest) returns (SignalResponse); -} diff --git a/src/leap/common/events/signal_pb2.py b/src/leap/common/events/signal_pb2.py deleted file mode 100644 index b21676f..0000000 --- a/src/leap/common/events/signal_pb2.py +++ /dev/null @@ -1,250 +0,0 @@ -# Generated by the protocol buffer compiler. DO NOT EDIT! - -from google.protobuf import descriptor -from google.protobuf import message -from google.protobuf import reflection -from google.protobuf import service -from google.protobuf import service_reflection -from google.protobuf import descriptor_pb2 -# @@protoc_insertion_point(imports) - - -DESCRIPTOR = descriptor.FileDescriptor( - name='signal.proto', - package='leap.common.events', - serialized_pb='\n\x0csignal.proto\x12\x12leap.common.events\"\xd8\x03\n\rSignalRequest\x12\n\n\x02id\x18\x01 \x02(\x05\x12\x38\n\x06signal\x18\x02 \x02(\x0e\x32(.leap.common.events.SignalRequest.Signal\x12\x0f\n\x07\x63ontent\x18\x03 \x02(\t\x12\x12\n\nmac_method\x18\x04 \x02(\t\x12\x0b\n\x03mac\x18\x05 \x02(\x0c\x12\x12\n\nenc_method\x18\x06 \x01(\t\x12\x16\n\x0e\x65rror_occurred\x18\x07 \x01(\x08\"\xa2\x02\n\x06Signal\x12\x15\n\x11\x43LIENT_SESSION_ID\x10\x01\x12\x0e\n\nCLIENT_UID\x10\x02\x12\x19\n\x15SOLEDAD_CREATING_KEYS\x10\x03\x12\x1e\n\x1aSOLEDAD_DONE_CREATING_KEYS\x10\x04\x12\x1a\n\x16SOLEDAD_UPLOADING_KEYS\x10\x05\x12\x1f\n\x1bSOLEDAD_DONE_UPLOADING_KEYS\x10\x06\x12\x1c\n\x18SOLEDAD_DOWNLOADING_KEYS\x10\x07\x12!\n\x1dSOLEDAD_DONE_DOWNLOADING_KEYS\x10\x08\x12\x1c\n\x18SOLEDAD_NEW_DATA_TO_SYNC\x10\t\x12\x1a\n\x16SOLEDAD_DONE_DATA_SYNC\x10\n\"\x80\x01\n\x0eSignalResponse\x12\n\n\x02id\x18\x01 \x02(\x05\x12\x39\n\x06status\x18\x02 \x02(\x0e\x32).leap.common.events.SignalResponse.Status\"\'\n\x06Status\x12\x06\n\x02OK\x10\x01\x12\n\n\x06UNAUTH\x10\x02\x12\t\n\x05\x45RROR\x10\x03\x32`\n\rSignalService\x12O\n\x06signal\x12!.leap.common.events.SignalRequest\x1a\".leap.common.events.SignalResponse') - - - -_SIGNALREQUEST_SIGNAL = descriptor.EnumDescriptor( - name='Signal', - full_name='leap.common.events.SignalRequest.Signal', - filename=None, - file=DESCRIPTOR, - values=[ - descriptor.EnumValueDescriptor( - name='CLIENT_SESSION_ID', index=0, number=1, - options=None, - type=None), - descriptor.EnumValueDescriptor( - name='CLIENT_UID', index=1, number=2, - options=None, - type=None), - descriptor.EnumValueDescriptor( - name='SOLEDAD_CREATING_KEYS', index=2, number=3, - options=None, - type=None), - descriptor.EnumValueDescriptor( - name='SOLEDAD_DONE_CREATING_KEYS', index=3, number=4, - options=None, - type=None), - descriptor.EnumValueDescriptor( - name='SOLEDAD_UPLOADING_KEYS', index=4, number=5, - options=None, - type=None), - descriptor.EnumValueDescriptor( - name='SOLEDAD_DONE_UPLOADING_KEYS', index=5, number=6, - options=None, - type=None), - descriptor.EnumValueDescriptor( - name='SOLEDAD_DOWNLOADING_KEYS', index=6, number=7, - options=None, - type=None), - descriptor.EnumValueDescriptor( - name='SOLEDAD_DONE_DOWNLOADING_KEYS', index=7, number=8, - options=None, - type=None), - descriptor.EnumValueDescriptor( - name='SOLEDAD_NEW_DATA_TO_SYNC', index=8, number=9, - options=None, - type=None), - descriptor.EnumValueDescriptor( - name='SOLEDAD_DONE_DATA_SYNC', index=9, number=10, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=219, - serialized_end=509, -) - -_SIGNALRESPONSE_STATUS = descriptor.EnumDescriptor( - name='Status', - full_name='leap.common.events.SignalResponse.Status', - filename=None, - file=DESCRIPTOR, - values=[ - descriptor.EnumValueDescriptor( - name='OK', index=0, number=1, - options=None, - type=None), - descriptor.EnumValueDescriptor( - name='UNAUTH', index=1, number=2, - options=None, - type=None), - descriptor.EnumValueDescriptor( - name='ERROR', index=2, number=3, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=601, - serialized_end=640, -) - - -_SIGNALREQUEST = descriptor.Descriptor( - name='SignalRequest', - full_name='leap.common.events.SignalRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - descriptor.FieldDescriptor( - name='id', full_name='leap.common.events.SignalRequest.id', index=0, - number=1, type=5, cpp_type=1, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - descriptor.FieldDescriptor( - name='signal', full_name='leap.common.events.SignalRequest.signal', index=1, - number=2, type=14, cpp_type=8, label=2, - has_default_value=False, default_value=1, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - descriptor.FieldDescriptor( - name='content', full_name='leap.common.events.SignalRequest.content', index=2, - number=3, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=unicode("", "utf-8"), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - descriptor.FieldDescriptor( - name='mac_method', full_name='leap.common.events.SignalRequest.mac_method', index=3, - number=4, type=9, cpp_type=9, label=2, - has_default_value=False, default_value=unicode("", "utf-8"), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - descriptor.FieldDescriptor( - name='mac', full_name='leap.common.events.SignalRequest.mac', index=4, - number=5, type=12, cpp_type=9, label=2, - has_default_value=False, default_value="", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - descriptor.FieldDescriptor( - name='enc_method', full_name='leap.common.events.SignalRequest.enc_method', index=5, - number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=unicode("", "utf-8"), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - descriptor.FieldDescriptor( - name='error_occurred', full_name='leap.common.events.SignalRequest.error_occurred', index=6, - number=7, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _SIGNALREQUEST_SIGNAL, - ], - options=None, - is_extendable=False, - extension_ranges=[], - serialized_start=37, - serialized_end=509, -) - - -_SIGNALRESPONSE = descriptor.Descriptor( - name='SignalResponse', - full_name='leap.common.events.SignalResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - descriptor.FieldDescriptor( - name='id', full_name='leap.common.events.SignalResponse.id', index=0, - number=1, type=5, cpp_type=1, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - descriptor.FieldDescriptor( - name='status', full_name='leap.common.events.SignalResponse.status', index=1, - number=2, type=14, cpp_type=8, label=2, - has_default_value=False, default_value=1, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _SIGNALRESPONSE_STATUS, - ], - options=None, - is_extendable=False, - extension_ranges=[], - serialized_start=512, - serialized_end=640, -) - - -_SIGNALREQUEST.fields_by_name['signal'].enum_type = _SIGNALREQUEST_SIGNAL -_SIGNALREQUEST_SIGNAL.containing_type = _SIGNALREQUEST; -_SIGNALRESPONSE.fields_by_name['status'].enum_type = _SIGNALRESPONSE_STATUS -_SIGNALRESPONSE_STATUS.containing_type = _SIGNALRESPONSE; - -class SignalRequest(message.Message): - __metaclass__ = reflection.GeneratedProtocolMessageType - DESCRIPTOR = _SIGNALREQUEST - - # @@protoc_insertion_point(class_scope:leap.common.events.SignalRequest) - -class SignalResponse(message.Message): - __metaclass__ = reflection.GeneratedProtocolMessageType - DESCRIPTOR = _SIGNALRESPONSE - - # @@protoc_insertion_point(class_scope:leap.common.events.SignalResponse) - - -_SIGNALSERVICE = descriptor.ServiceDescriptor( - name='SignalService', - full_name='leap.common.events.SignalService', - file=DESCRIPTOR, - index=0, - options=None, - serialized_start=642, - serialized_end=738, - methods=[ - descriptor.MethodDescriptor( - name='signal', - full_name='leap.common.events.SignalService.signal', - index=0, - containing_service=None, - input_type=_SIGNALREQUEST, - output_type=_SIGNALRESPONSE, - options=None, - ), -]) - -class SignalService(service.Service): - __metaclass__ = service_reflection.GeneratedServiceType - DESCRIPTOR = _SIGNALSERVICE -class SignalService_Stub(SignalService): - __metaclass__ = service_reflection.GeneratedServiceStubType - DESCRIPTOR = _SIGNALSERVICE - -# @@protoc_insertion_point(module_scope) diff --git a/src/leap/common/events/test_events.py b/src/leap/common/events/test_events.py deleted file mode 100644 index ae55319..0000000 --- a/src/leap/common/events/test_events.py +++ /dev/null @@ -1,88 +0,0 @@ -import unittest -from protobuf.socketrpc import RpcService -from leap.common import events -from leap.common.events import service -from leap.common.events.signal_pb2 import ( - SignalRequest, - SignalService, - SignalService_Stub, -) - - -port = 8090 - -class EventsTestCase(unittest.TestCase): - - def _start_service(self): - return service.SignalServiceThread.start_service(port) - - def setUp(self): - super(EventsTestCase, self).setUp() - self._service = self._start_service() - - def tearDown(self): - events.registered_callbacks = {} - super(EventsTestCase, self).tearDown() - - def test_service_singleton(self): - self.assertTrue(self._service.get_instance() == self._service, - "Can't get singleton class for service.") - - def test_register_signal(self): - key = SignalRequest.SOLEDAD_CREATING_KEYS - self.assertEqual({}, events.registered_callbacks, - 'There should be no registered_callbacks events when ' - 'service has just started.') - events.register(key, lambda x: True) - self.assertEqual(1, len(events.registered_callbacks), - 'Wrong number of registered callbacks.') - self.assertEqual(events.registered_callbacks.keys(), [key], - 'Couldn\'t locate registered signal.') - events.register(key, lambda x: True) - self.assertEqual(1, len(events.registered_callbacks), - 'Wrong number of registered callbacks.') - self.assertEqual(events.registered_callbacks.keys(), [key], - 'Couldn\'t locate registered signal.') - self.assertEqual( - 2, - len(events.registered_callbacks[SignalRequest.SOLEDAD_CREATING_KEYS]), - 'Wrong number of registered callbacks.') - key2 = SignalRequest.CLIENT_UID - events.register(key2, lambda x: True) - self.assertEqual(2, len(events.registered_callbacks), - 'Wrong number of registered callbacks.') - self.assertEqual( - sorted(events.registered_callbacks.keys()), - sorted([key2, key]), - 'Wrong keys in `registered_keys`.') - - def test_register_signal_replace(self): - key = SignalRequest.SOLEDAD_CREATING_KEYS - cbk = lambda x: True - self.assertEqual({}, events.registered_callbacks, - 'There should be no registered_callbacks events when ' - 'service has just started.') - events.register(key, cbk, uid=1) - self.assertRaises(Exception, events.register, key, lambda x: True, uid=1) - self.assertEquals(1, - events.register(key, lambda x: True, uid=1, replace=True), - "Could not replace callback.") - self.assertEqual(1, len(events.registered_callbacks), - 'Wrong number of registered callbacks.') - self.assertEqual(events.registered_callbacks.keys(), [key], - 'Couldn\'t locate registered signal.') - - def test_signal_response_status(self): - sig = SignalRequest.SOLEDAD_CREATING_KEYS - cbk = lambda x: True - events.register(sig, cbk) - request = SignalRequest() - request.id = 1 - request.signal = sig - request.content = 'my signal contents' - request.mac_method = 'nomac' - request.mac = "" - service = RpcService(SignalService_Stub, port, 'localhost') - response = service.signal(request, timeout=1000) - self.assertEqual(response.OK, response.status, - 'Wrong response status.') diff --git a/src/leap/common/files.py b/src/leap/common/files.py index 4c443dd..bba281b 100644 --- a/src/leap/common/files.py +++ b/src/leap/common/files.py @@ -33,8 +33,8 @@ def check_and_fix_urw_only(cert): Might raise OSError - @param cert: Certificate path - @type cert: str + :param cert: Certificate path + :type cert: str """ mode = stat.S_IMODE(os.stat(cert).st_mode) @@ -53,10 +53,10 @@ def get_mtime(filename): """ Returns the modified time or None if the file doesn't exist - @param filename: path to check - @type filename: str + :param filename: path to check + :type filename: str - @rtype: str + :rtype: str """ try: mtime = time.ctime(os.path.getmtime(filename)) + " GMT" @@ -72,8 +72,8 @@ def mkdir_p(path): Might raise OSError - @param path: path to create - @type path: str + :param path: path to create + :type path: str """ try: os.makedirs(path) @@ -97,14 +97,14 @@ def which(name, flags=os.X_OK, path_extension="/usr/sbin:/sbin"): On MS-Windows the only flag that has any meaning is os.F_OK. Any other flags will be ignored. - @type name: C{str} - @param name: The name for which to search. + :type name: C{str} + :param name: The name for which to search. - @type flags: C{int} - @param flags: Arguments to L{os.access}. + :type flags: C{int} + :param flags: Arguments to L{os.access}. - @rtype: C{list} - @param: A list of the full paths to files found, in the + :rtype: C{list} + :param: A list of the full paths to files found, in the order in which they were found. """ diff --git a/src/leap/common/testing/basetest.py b/src/leap/common/testing/basetest.py index 65e23a9..8890bf9 100644 --- a/src/leap/common/testing/basetest.py +++ b/src/leap/common/testing/basetest.py @@ -94,8 +94,8 @@ class BaseLeapTest(unittest.TestCase): Raises NotImplementedError for this platform if do_raise is True - @param do_raise: flag to actually raise exception - @type do_raise: bool + :param do_raise: flag to actually raise exception + :type do_raise: bool """ if do_raise: raise NotImplementedError( @@ -109,8 +109,8 @@ class BaseLeapTest(unittest.TestCase): prepending the temporal dir associated with this TestCase - @param filename: the filename - @type filename: str + :param filename: the filename + :type filename: str """ return os.path.join(self.tempdir, filename) @@ -119,8 +119,8 @@ class BaseLeapTest(unittest.TestCase): Touches a filepath, creating folders along the way if needed. - @param filepath: path to be touched - @type filepath: str + :param filepath: path to be touched + :type filepath: str """ folder, filename = os.path.split(filepath) if not os.path.isdir(folder): @@ -134,7 +134,7 @@ class BaseLeapTest(unittest.TestCase): """ Chmods 600 a file - @param filepath: filepath to be chmodded - @type filepath: str + :param filepath: filepath to be chmodded + :type filepath: str """ check_and_fix_urw_only(filepath) diff --git a/src/leap/common/testing/test_basetest.py b/src/leap/common/testing/test_basetest.py index c4636df..cf0962d 100644 --- a/src/leap/common/testing/test_basetest.py +++ b/src/leap/common/testing/test_basetest.py @@ -25,7 +25,7 @@ except ImportError: import os import StringIO -from leap.testing.basetest import BaseLeapTest +from leap.common.testing.basetest import BaseLeapTest _tempdir = None # global for tempdir checking @@ -38,8 +38,8 @@ class _TestCaseRunner(object): """ Runs a given TestCase - @param testcase: the testcase - @type testcase: unittest.TestCase + :param testcase: the testcase + :type testcase: unittest.TestCase """ if not testcase: return None diff --git a/src/leap/common/tests/__init__.py b/src/leap/common/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/leap/common/tests/__init__.py diff --git a/src/leap/common/tests/test_events.py b/src/leap/common/tests/test_events.py new file mode 100644 index 0000000..7286bdc --- /dev/null +++ b/src/leap/common/tests/test_events.py @@ -0,0 +1,233 @@ +## -*- coding: utf-8 -*- +# test_events.py +# Copyright (C) 2013 LEAP +# +# This program 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. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest +import sets +import time +from protobuf.socketrpc import RpcService +from leap.common import events +from leap.common.events import ( + server, + component, + mac_auth, +) +from leap.common.events.events_pb2 import ( + EventsServerService, + EventsServerService_Stub, + EventResponse, + SignalRequest, + RegisterRequest, + SOLEDAD_CREATING_KEYS, + CLIENT_UID, +) + + +port = 8090 + +received = False +local_callback_executed = False + + +def callback(request, reponse): + return True + + +class EventsTestCase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + server.EventsServerDaemon.ensure(8090) + cls.callbacks = events.component.registered_callbacks + + @classmethod + def tearDownClass(cls): + # give some time for requests to be processed. + time.sleep(1) + + def setUp(self): + super(EventsTestCase, self).setUp() + + def tearDown(self): + #events.component.registered_callbacks = {} + server.registered_components = {} + super(EventsTestCase, self).tearDown() + + def test_service_singleton(self): + """ + Ensure that there's always just one instance of the server daemon + running. + """ + service1 = server.EventsServerDaemon.ensure(8090) + service2 = server.EventsServerDaemon.ensure(8090) + self.assertEqual(service1, service2, + "Can't get singleton class for service.") + + def test_component_register(self): + """ + Ensure components can register callbacks. + """ + self.assertTrue(1 not in self.callbacks, + 'There should should be no callback for this signal.') + events.register(1, lambda x: True) + self.assertTrue(1 in self.callbacks, + 'Could not register signal in local component.') + events.register(2, lambda x: True) + self.assertTrue(1 in self.callbacks, + 'Could not register signal in local component.') + self.assertTrue(2 in self.callbacks, + 'Could not register signal in local component.') + + def test_register_signal_replace(self): + """ + Make sure components can replace already registered callbacks. + """ + sig = 3 + cbk = lambda x: True + events.register(sig, cbk, uid=1) + self.assertRaises(Exception, events.register, sig, lambda x: True, + uid=1) + events.register(sig, lambda x: True, uid=1, replace=True) + self.assertTrue(sig in self.callbacks, 'Could not register signal.') + self.assertEqual(1, len(self.callbacks[sig]), + 'Wrong number of registered callbacks.') + + def test_signal_response_status(self): + """ + Ensure there's an appropriate response from server when signaling. + """ + sig = 4 + request = SignalRequest() + request.event = sig + request.content = 'my signal contents' + request.mac_method = mac_auth.MacMethod.MAC_NONE + request.mac = "" + service = RpcService(EventsServerService_Stub, port, 'localhost') + # test synch + response = service.signal(request, timeout=1000) + self.assertEqual(EventResponse.OK, response.status, + 'Wrong response status.') + # test asynch + + def local_callback(request, response): + global local_callback_executed + local_callback_executed = True + + events.register(sig, local_callback) + service.signal(request, callback=local_callback) + time.sleep(0.1) + self.assertTrue(local_callback_executed, + 'Local callback did not execute.') + + def test_events_server_service_register(self): + """ + Ensure the server can register components to be signaled. + """ + sig = 5 + request = RegisterRequest() + request.event = sig + request.port = 8091 + request.mac_method = mac_auth.MacMethod.MAC_NONE + request.mac = "" + service = RpcService(EventsServerService_Stub, port, 'localhost') + complist = server.registered_components + self.assertEqual({}, complist, + 'There should be no registered_ports when ' + 'server has just been created.') + response = service.register(request, timeout=1000) + self.assertTrue(sig in complist, "Signal not registered succesfully.") + self.assertTrue(8091 in complist[sig], + 'Failed registering component port.') + + def test_component_request_register(self): + """ + Ensure components can register themselves with server. + """ + sig = 6 + complist = server.registered_components + self.assertTrue(sig not in complist, + 'There should be no registered components for this ' + 'signal.') + events.register(sig, lambda x: True) + time.sleep(0.1) + port = component.EventsComponentDaemon.get_instance().get_port() + self.assertTrue(sig in complist, 'Failed registering component.') + self.assertTrue(port in complist[sig], + 'Failed registering component port.') + + def test_component_receives_signal(self): + """ + Ensure components can receive signals. + """ + sig = 7 + + def getsig(param=None): + global received + received = True + + events.register(sig, getsig) + request = SignalRequest() + request.event = sig + request.content = "" + request.mac_method = mac_auth.MacMethod.MAC_NONE + request.mac = "" + service = RpcService(EventsServerService_Stub, port, 'localhost') + response = service.signal(request, timeout=1000) + self.assertTrue(response is not None, 'Did not receive response.') + time.sleep(0.5) + self.assertTrue(received, 'Did not receive signal back.') + + def test_component_send_signal(self): + """ + Ensure components can send signals. + """ + sig = 8 + response = events.signal(sig) + self.assertTrue(response.status == response.OK, + 'Received wrong response status when signaling.') + + def test_component_unregister_all(self): + """ + Test that the component can unregister all events for one signal. + """ + sig = CLIENT_UID + complist = server.registered_components + events.register(sig, lambda x: True) + events.register(sig, lambda x: True) + time.sleep(0.1) + events.unregister(sig) + time.sleep(0.1) + port = component.EventsComponentDaemon.get_instance().get_port() + self.assertFalse(bool(complist[sig])) + self.assertTrue(port not in complist[sig]) + + def test_component_unregister_by_uid(self): + """ + Test that the component can unregister an event by uid. + """ + sig = CLIENT_UID + complist = server.registered_components + events.register(sig, lambda x: True, uid='cbkuid') + events.register(sig, lambda x: True, uid='cbkuid2') + time.sleep(0.1) + events.unregister(sig, uid='cbkuid') + time.sleep(0.1) + port = component.EventsComponentDaemon.get_instance().get_port() + self.assertTrue(sig in complist) + self.assertTrue(len(complist[sig]) == 1) + self.assertTrue( + component.registered_callbacks[sig].pop()[0] == 'cbkuid2') + self.assertTrue(port in complist[sig]) |