diff options
Diffstat (limited to 'src')
25 files changed, 2490 insertions, 595 deletions
| 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]) | 
