diff options
Diffstat (limited to 'debian/python-leap.common/usr/share/pyshared/leap')
23 files changed, 4059 insertions, 0 deletions
| diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/__init__.py b/debian/python-leap.common/usr/share/pyshared/leap/common/__init__.py new file mode 100644 index 0000000..5bcbb38 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/__init__.py @@ -0,0 +1,19 @@ +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__) + +try: +    import pygeoip +    HAS_GEOIP = True +except ImportError: +    #logger.debug('PyGeoIP not found. Disabled Geo support.') +    HAS_GEOIP = False + +__all__ = ["certs", "check", "files", "events"] + +__version__ = "0.2.3-dev" diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/certs.py b/debian/python-leap.common/usr/share/pyshared/leap/common/certs.py new file mode 100644 index 0000000..4cb70dd --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/certs.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# certs.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 cert checks and helpers +""" + +import os +import time +import logging + +from OpenSSL import crypto +from dateutil.parser import parse as dateparse + +from leap.common.check import leap_assert + +logger = logging.getLogger(__name__) + + +def get_cert_from_string(string): +    """ +    Returns the x509 from the contents of this string + +    @param string: certificate contents as downloaded +    @type string: str + +    @return: x509 or None +    """ +    leap_assert(string, "We need something to load") + +    x509 = None +    try: +        x509 = crypto.load_certificate(crypto.FILETYPE_PEM, string) +    except Exception as e: +        logger.error("Something went wrong while loading the certificate: %r" +                     % (e,)) +    return x509 + + +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 + +    @return: private key or None +    """ +    leap_assert(string, "We need something to load") + +    pkey = None +    try: +        pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, string) +    except Exception as e: +        logger.error("Something went wrong while loading the certificate: %r" +                     % (e,)) +    return pkey + + +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 + +    @rtype: str +    """ +    x509 = get_cert_from_string(cert_data) +    digest = x509.digest(method).replace(":", "").lower() + +    return digest + + +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 + +    @rtype: bool +    """ +    can_load = True + +    try: +        cert = get_cert_from_string(string) +        key = get_privatekey_from_string(string) + +        leap_assert(cert, 'The certificate could not be loaded') +        leap_assert(key, 'The private key could not be loaded') +    except Exception as e: +        can_load = False +        logger.error("Something went wrong while trying to load " +                     "the certificate: %r" % (e,)) + +    return can_load + + +def is_valid_pemfile(cert): +    """ +    Checks that the passed string is a valid pem certificate + +    @param cert: String containing pem content +    @type cert: str + +    @rtype: bool +    """ +    leap_assert(cert, "We need a cert to load") + +    return can_load_cert_and_pkey(cert) + + +def get_cert_time_boundaries(certfile): +    """ +    Returns the time boundaries for the certificate saved in certfile + +    @param certfile: path to certificate +    @type certfile: str + +    @rtype: tuple (from, to) +    """ +    cert = get_cert_from_string(certfile) +    leap_assert(cert, 'There was a problem loading the certificate') + +    fromts, tots = (cert.get_notBefore(), cert.get_notAfter()) +    from_, to_ = map( +        lambda ts: time.gmtime(time.mktime(dateparse(ts).timetuple())), +        (fromts, tots)) +    return from_, to_ + + +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 + +    @rtype: bool +    """ +    exists = os.path.isfile(certfile) + +    if not exists: +        return True + +    certdata = None +    try: +        with open(certfile, "r") as f: +            certdata = f.read() +            if not is_valid_pemfile(certdata): +                return True +    except: +        return True + +    valid_from, valid_to = get_cert_time_boundaries(certdata) + +    if not (valid_from < now() < valid_to): +        return True + +    return False diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/check.py b/debian/python-leap.common/usr/share/pyshared/leap/common/check.py new file mode 100644 index 0000000..359673b --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/check.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# check.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/>. +""" +Set of functions to help checking situations +""" + +import inspect +import logging +import traceback + + +logger = logging.getLogger(__name__) + + +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 +    """ +    if not condition: +        logger.error("Bug: %s" % (message,)) +        try: +            frame = inspect.currentframe() +            stack_trace = traceback.format_stack(frame) +            logger.error(''.join(stack_trace)) +        except Exception as e: +            logger.error("Bug in leap_assert: %r" % (e,)) +    assert condition, message + + +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 +    """ +    leap_assert(isinstance(var, expectedType), +                "Expected type %r instead of %r" % +                (expectedType, type(var))) diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/config/__init__.py b/debian/python-leap.common/usr/share/pyshared/leap/common/config/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/config/__init__.py diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/config/baseconfig.py b/debian/python-leap.common/usr/share/pyshared/leap/common/config/baseconfig.py new file mode 100644 index 0000000..146f1e4 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/config/baseconfig.py @@ -0,0 +1,185 @@ +# -*- 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): +        """ +        Loads the configuration from disk + +        @type path: str +        @param path: relative path to configuration. The absolute path +        will be calculated depending on the platform + +        @return: True if loaded from disk correctly, False otherwise +        """ + +        config_path = os.path.join(self.get_path_prefix(), +                                   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.warning("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="en"): +        """ +        Tries to return the string for the specified language, otherwise +        informs the problem and returns an empty string + +        @param lang: language code +        @type lang: str + +        @return: localized value from the possible values returned by +        self._func +        """ +        descriptions = self._func(instance) +        description_lang = "" +        config_lang = "en" +        for key in descriptions.keys(): +            if lang.startswith(key): +                config_lang = key +                break + +        description_lang = descriptions[config_lang] +        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/debian/python-leap.common/usr/share/pyshared/leap/common/config/pluggableconfig.py b/debian/python-leap.common/usr/share/pyshared/leap/common/config/pluggableconfig.py new file mode 100644 index 0000000..8535fa6 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/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/debian/python-leap.common/usr/share/pyshared/leap/common/config/prefixers.py b/debian/python-leap.common/usr/share/pyshared/leap/common/config/prefixers.py new file mode 100644 index 0000000..27274bd --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/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/debian/python-leap.common/usr/share/pyshared/leap/common/events/__init__.py b/debian/python-leap.common/usr/share/pyshared/leap/common/events/__init__.py new file mode 100644 index 0000000..c949080 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/events/__init__.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# __init__.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/>. + +""" +An events mechanism that allows for signaling of events between components. +""" + +import logging +import socket + + +from leap.common.events import ( +    events_pb2, +    server, +    component, +    daemon, +) + + +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 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 +    """ +    return component.signal(signal, content, mac_method, mac, reqcbk, timeout) diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/events/component.py b/debian/python-leap.common/usr/share/pyshared/leap/common/events/component.py new file mode 100644 index 0000000..0cf0e38 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/events/component.py @@ -0,0 +1,238 @@ +# -*- 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. +    """ + + +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)) +    return service.register(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)) +    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)) + +        # 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/debian/python-leap.common/usr/share/pyshared/leap/common/events/daemon.py b/debian/python-leap.common/usr/share/pyshared/leap/common/events/daemon.py new file mode 100644 index 0000000..d2c7b9b --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/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/debian/python-leap.common/usr/share/pyshared/leap/common/events/events_pb2.py b/debian/python-leap.common/usr/share/pyshared/leap/common/events/events_pb2.py new file mode 100644 index 0000000..a4f1df4 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/events/events_pb2.py @@ -0,0 +1,371 @@ +# 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='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\"\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\xb9\x01\n\x13\x45ventsServerService\x12R\n\x08register\x12#.leap.common.events.RegisterRequest\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=432, +    serialized_end=791, +) + + +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=390, +    serialized_end=429, +) + + +_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, +) + + +_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=299, +  serialized_end=429, +) + +_SIGNALREQUEST.fields_by_name['event'].enum_type = _EVENT +_REGISTERREQUEST.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['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 EventResponse(message.Message): +    __metaclass__ = reflection.GeneratedProtocolMessageType +    DESCRIPTOR = _EVENTRESPONSE + +    # @@protoc_insertion_point(class_scope:leap.common.events.EventResponse) + + +_EVENTSSERVERSERVICE = descriptor.ServiceDescriptor( +  name='EventsServerService', +  full_name='leap.common.events.EventsServerService', +  file=DESCRIPTOR, +  index=0, +  options=None, +  serialized_start=794, +  serialized_end=979, +  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='signal', +    full_name='leap.common.events.EventsServerService.signal', +    index=1, +    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=981, +  serialized_end=1085, +  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/debian/python-leap.common/usr/share/pyshared/leap/common/events/mac_auth.py b/debian/python-leap.common/usr/share/pyshared/leap/common/events/mac_auth.py new file mode 100644 index 0000000..49d48f7 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/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/debian/python-leap.common/usr/share/pyshared/leap/common/events/server.py b/debian/python-leap.common/usr/share/pyshared/leap/common/events/server.py new file mode 100644 index 0000000..16c6513 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/events/server.py @@ -0,0 +1,149 @@ +# -*- 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)) +        # 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 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)) +        # 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/debian/python-leap.common/usr/share/pyshared/leap/common/files.py b/debian/python-leap.common/usr/share/pyshared/leap/common/files.py new file mode 100644 index 0000000..4c443dd --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/files.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# files.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 file helper methods +""" + +import errno +import logging +import os +import stat +import time + +logger = logging.getLogger(__name__) + + +def check_and_fix_urw_only(cert): +    """ +    Test for 600 mode and try to set it if anything different found + +    Might raise OSError + +    @param cert: Certificate path +    @type cert: str +    """ +    mode = stat.S_IMODE(os.stat(cert).st_mode) + +    if mode != int('600', 8): +        try: +            logger.warning('Bad permission on %s attempting to set 600' % +                           (cert,)) +            os.chmod(cert, stat.S_IRUSR | stat.S_IWUSR) +        except OSError: +            logger.error('Error while trying to chmod 600 %s' % +                         cert) +            raise + + +def get_mtime(filename): +    """ +    Returns the modified time or None if the file doesn't exist + +    @param filename: path to check +    @type filename: str + +    @rtype: str +    """ +    try: +        mtime = time.ctime(os.path.getmtime(filename)) + " GMT" +        return mtime +    except OSError: +        return None + + +def mkdir_p(path): +    """ +    Creates the path and all the intermediate directories that don't +    exist + +    Might raise OSError + +    @param path: path to create +    @type path: str +    """ +    try: +        os.makedirs(path) +    except OSError as exc: +        if exc.errno == errno.EEXIST and os.path.isdir(path): +            pass +        else: +            raise + + +# Twisted implementation of which +def which(name, flags=os.X_OK, path_extension="/usr/sbin:/sbin"): +    """ +    Search PATH for executable files with the given name. + +    On newer versions of MS-Windows, the PATHEXT environment variable will be +    set to the list of file extensions for files considered executable. This +    will normally include things like ".EXE". This fuction will also find files +    with the given name ending with any of these extensions. + +    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 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 +    order in which they were found. +    """ + +    result = [] +    exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep)) +    path = os.environ.get('PATH', None) +    path = path_extension + os.pathsep + path +    if path is None: +        return [] +    parts = path.split(os.pathsep) +    for p in parts: +        p = os.path.join(p, name) +        if os.access(p, flags): +            result.append(p) +        for e in exts: +            pext = p + e +            if os.access(pext, flags): +                result.append(pext) +    return result diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/__init__.py b/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/__init__.py new file mode 100644 index 0000000..d6dbb8a --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/__init__.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +# __init__.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/>. + + +""" +Key Manager is a Nicknym agent for LEAP client. +""" + +import requests + +try: +    import simplejson as json +except ImportError: +    import json  # noqa + +from leap.common.check import leap_assert +from leap.common.keymanager.errors import ( +    KeyNotFound, +    NoPasswordGiven, +) +from leap.common.keymanager.keys import ( +    build_key_from_dict, +) +from leap.common.keymanager.openpgp import ( +    OpenPGPKey, +    OpenPGPScheme, +    encrypt_sym, +) + + +TAGS_INDEX = 'by-tags' +TAGS_AND_PRIVATE_INDEX = 'by-tags-and-private' +INDEXES = { +    TAGS_INDEX: ['tags'], +    TAGS_AND_PRIVATE_INDEX: ['tags', 'bool(private)'], +} + + +class KeyManager(object): + +    def __init__(self, address, nickserver_url, soledad, token=None): +        """ +        Initialize a Key Manager for user's C{address} with provider's +        nickserver reachable in C{url}. + +        @param address: The address of the user of this Key Manager. +        @type address: str +        @param url: The URL of the nickserver. +        @type url: str +        @param soledad: A Soledad instance for local storage of keys. +        @type soledad: leap.soledad.Soledad +        """ +        self._address = address +        self._nickserver_url = nickserver_url +        self._soledad = soledad +        self.token = token +        self._wrapper_map = { +            OpenPGPKey: OpenPGPScheme(soledad), +            # other types of key will be added to this mapper. +        } +        self._init_indexes() +        self._fetcher = requests + +    # +    # utilities +    # + +    def _key_class_from_type(self, ktype): +        """ +        Return key class from string representation of key type. +        """ +        return filter( +            lambda klass: str(klass) == ktype, +            self._wrapper_map).pop() + +    def _init_indexes(self): +        """ +        Initialize the database indexes. +        """ +        # Ask the database for currently existing indexes. +        db_indexes = dict(self._soledad.list_indexes()) +        # Loop through the indexes we expect to find. +        for name, expression in INDEXES.items(): +            if name not in db_indexes: +                # The index does not yet exist. +                self._soledad.create_index(name, *expression) +                continue +            if expression == db_indexes[name]: +                # The index exists and is up to date. +                continue +            # The index exists but the definition is not what expected, so we +            # delete it and add the proper index expression. +            self._soledad.delete_index(name) +            self._soledad.create_index(name, *expression) + +    def _get_dict_from_http_json(self, path): +        """ +        Make a GET HTTP request and return a dictionary containing the +        response. +        """ +        response = self._fetcher.get(self._nickserver_url+path) +        leap_assert(response.status_code == 200, 'Invalid response.') +        leap_assert( +            response.headers['content-type'].startswith('application/json') +                is True, +            'Content-type is not JSON.') +        return response.json() + +    # +    # key management +    # + +    def send_key(self, ktype, send_private=False, password=None): +        """ +        Send user's key of type C{ktype} to provider. + +        Public key bound to user's is sent to provider, which will sign it and +        replace any prior keys for the same address in its database. + +        If C{send_private} is True, then the private key is encrypted with +        C{password} and sent to server in the same request, together with a +        hash string of user's address and password. The encrypted private key +        will be saved in the server in a way it is publicly retrievable +        through the hash string. + +        @param ktype: The type of the key. +        @type ktype: KeyType + +        @raise httplib.HTTPException: +        @raise KeyNotFound: If the key was not found both locally and in +            keyserver. +        """ +        # prepare the public key bound to address +        pubkey = self.get_key( +            self._address, ktype, private=False, fetch_remote=False) +        data = { +            'address': self._address, +            'keys': [ +                json.loads(pubkey.get_json()), +            ] +        } +        # prepare the private key bound to address +        if send_private: +            if password is None or password == '': +                raise NoPasswordGiven('Can\'t send unencrypted private keys!') +            privkey = self.get_key( +                self._address, ktype, private=True, fetch_remote=False) +            privkey = json.loads(privkey.get_json()) +            privkey.key_data = encrypt_sym(privkey.key_data, password) +            data['keys'].append(privkey) +        self._fetcher.put( +            self._nickserver_url + '/key/' + self._address, +            data=data, +            auth=(self._address, self._token)) + +    def get_key(self, address, ktype, private=False, fetch_remote=True): +        """ +        Return a key of type C{ktype} bound to C{address}. + +        First, search for the key in local storage. If it is not available, +        then try to fetch from nickserver. + +        @param address: The address bound to the key. +        @type address: str +        @param ktype: The type of the key. +        @type ktype: KeyType +        @param private: Look for a private key instead of a public one? +        @type private: bool + +        @return: A key of type C{ktype} bound to C{address}. +        @rtype: EncryptionKey +        @raise KeyNotFound: If the key was not found both locally and in +            keyserver. +        """ +        leap_assert( +            ktype in self._wrapper_map, +            'Unkown key type: %s.' % str(ktype)) +        try: +            return self._wrapper_map[ktype].get_key(address, private=private) +        except KeyNotFound: +            if fetch_remote is False: +                raise +            # fetch keys from server and discard unwanted types. +            keys = filter(lambda k: isinstance(k, ktype), +                          self.fetch_keys_from_server(address)) +            if len(keys) is 0: +                raise KeyNotFound() +            leap_assert( +                len(keys) == 1, +                'Got more than one key of type %s for %s.' % +                (str(ktype), address)) +            self._wrapper_map[ktype].put_key(keys[0]) +            return self._wrapper_map[ktype].get_key(address, private=private) + +    def fetch_keys_from_server(self, address): +        """ +        Fetch keys bound to C{address} from nickserver. + +        @param address: The address bound to the keys. +        @type address: str + +        @return: A list of keys bound to C{address}. +        @rtype: list of EncryptionKey +        @raise KeyNotFound: If the key was not found on nickserver. +        @raise httplib.HTTPException: +        """ +        keydata = self._get_dict_from_http_json('/key/%s' % address) +        leap_assert( +            keydata['address'] == address, +            "Fetched key for wrong address.") +        keys = [] +        for key in keydata['keys']: +            keys.append( +                build_key_from_dict( +                    self._key_class_from_type(key['type']), +                    address, +                    key)) +        return keys + +    def get_all_keys_in_local_db(self, private=False): +        """ +        Return all keys stored in local database. + +        @return: A list with all keys in local db. +        @rtype: list +        """ +        return map( +            lambda doc: build_key_from_dict( +                self._key_class_from_type(doc.content['type']), +                doc.content['address'], +                doc.content), +            self._soledad.get_from_index( +                TAGS_AND_PRIVATE_INDEX, +                'keymanager-key', +                '1' if private else '0')) + +    def refresh_keys(self): +        """ +        Fetch keys from nickserver and update them locally. +        """ +        addresses = set(map( +            lambda doc: doc.address, +            self.get_all_keys_in_local_db(private=False))) +        # TODO: maybe we should not attempt to refresh our own public key? +        for address in addresses: +            for key in self.fetch_keys_from_server(address): +                self._wrapper_map[key.__class__].put_key(key) + +    def gen_key(self, ktype): +        """ +        Generate a key of type C{ktype} bound to the user's address. + +        @param ktype: The type of the key. +        @type ktype: KeyType + +        @return: The generated key. +        @rtype: EncryptionKey +        """ +        return self._wrapper_map[ktype].gen_key(self._address) + +    # +    # Token setter/getter +    # + +    def _get_token(self): +        return self._token + +    def _set_token(self, token): +        self._token = token + +    token = property( +        _get_token, _set_token, doc='The auth token.') diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/errors.py b/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/errors.py new file mode 100644 index 0000000..1cf506e --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/errors.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# errors.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/>. + + +""" +Errors and exceptions used by the Key Manager. +""" + + +class KeyNotFound(Exception): +    """ +    Raised when key was no found on keyserver. +    """ + + +class KeyAlreadyExists(Exception): +    """ +    Raised when attempted to create a key that already exists. +    """ + + +class KeyAttributesDiffer(Exception): +    """ +    Raised when trying to delete a key but the stored key differs from the key +    passed to the delete_key() method. +    """ + +class NoPasswordGiven(Exception): +    """ +    Raised when trying to perform some action that needs a password without +    providing one. +    """ diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/gpg.py b/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/gpg.py new file mode 100644 index 0000000..f3e6453 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/gpg.py @@ -0,0 +1,397 @@ +# -*- coding: utf-8 -*- +# gpgwrapper.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 GPG wrapper used to handle OpenPGP keys. + +This is a temporary class that will be superseded by the a revised version of +python-gnupg. +""" + + +import os +import gnupg +import re +from gnupg import ( +    logger, +    _is_sequence, +    _make_binary_stream, +) + + +class ListPackets(): +    """ +    Handle status messages for --list-packets. +    """ + +    def __init__(self, gpg): +        """ +        Initialize the packet listing handling class. + +        @param gpg: GPG object instance. +        @type gpg: gnupg.GPG +        """ +        self.gpg = gpg +        self.nodata = None +        self.key = None +        self.need_passphrase = None +        self.need_passphrase_sym = None +        self.userid_hint = None + +    def handle_status(self, key, value): +        """ +        Handle one line of the --list-packets status message. + +        @param key: The status message key. +        @type key: str +        @param value: The status message value. +        @type value: str +        """ +        # TODO: write tests for handle_status +        if key == 'NODATA': +            self.nodata = True +        if key == 'ENC_TO': +            # This will only capture keys in our keyring. In the future we +            # may want to include multiple unknown keys in this list. +            self.key, _, _ = value.split() +        if key == 'NEED_PASSPHRASE': +            self.need_passphrase = True +        if key == 'NEED_PASSPHRASE_SYM': +            self.need_passphrase_sym = True +        if key == 'USERID_HINT': +            self.userid_hint = value.strip().split() + + +class GPGWrapper(gnupg.GPG): +    """ +    This is a temporary class for handling GPG requests, and should be +    replaced by a more general class used throughout the project. +    """ + +    GNUPG_HOME = os.environ['HOME'] + "/.config/leap/gnupg" +    GNUPG_BINARY = "/usr/bin/gpg"  # this has to be changed based on OS + +    def __init__(self, gpgbinary=GNUPG_BINARY, gnupghome=GNUPG_HOME, +                 verbose=False, use_agent=False, keyring=None, options=None): +        """ +        Initialize a GnuPG process wrapper. + +        @param gpgbinary: Name for GnuPG binary executable. +        @type gpgbinary: C{str} +        @param gpghome: Full pathname to directory containing the public and +            private keyrings. +        @type gpghome: C{str} +        @param keyring: Name of alternative keyring file to use. If specified, +            the default keyring is not used. +        @param verbose: Should some verbose info be output? +        @type verbose: bool +        @param use_agent: Should pass `--use-agent` to GPG binary? +        @type use_agent: bool +        @param keyring: Path for the keyring to use. +        @type keyring: str +        @options: A list of additional options to pass to the GPG binary. +        @type options: list + +        @raise: RuntimeError with explanation message if there is a problem +            invoking gpg. +        """ +        gnupg.GPG.__init__(self, gnupghome=gnupghome, gpgbinary=gpgbinary, +                           verbose=verbose, use_agent=use_agent, +                           keyring=keyring, options=options) +        self.result_map['list-packets'] = ListPackets + +    def find_key_by_email(self, email, secret=False): +        """ +        Find user's key based on their email. + +        @param email: Email address of key being searched for. +        @type email: str +        @param secret: Should we search for a secret key? +        @type secret: bool + +        @return: The fingerprint of the found key. +        @rtype: str +        """ +        for key in self.list_keys(secret=secret): +            for uid in key['uids']: +                if re.search(email, uid): +                    return key +        raise LookupError("GnuPG public key for email %s not found!" % email) + +    def find_key_by_subkey(self, subkey, secret=False): +        """ +        Find user's key based on a subkey fingerprint. + +        @param email: Subkey fingerprint of the key being searched for. +        @type email: str +        @param secret: Should we search for a secret key? +        @type secret: bool + +        @return: The fingerprint of the found key. +        @rtype: str +        """ +        for key in self.list_keys(secret=secret): +            for sub in key['subkeys']: +                if sub[0] == subkey: +                    return key +        raise LookupError( +            "GnuPG public key for subkey %s not found!" % subkey) + +    def find_key_by_keyid(self, keyid, secret=False): +        """ +        Find user's key based on the key ID. + +        @param email: The key ID of the key being searched for. +        @type email: str +        @param secret: Should we search for a secret key? +        @type secret: bool + +        @return: The fingerprint of the found key. +        @rtype: str +        """ +        for key in self.list_keys(secret=secret): +            if keyid == key['keyid']: +                return key +        raise LookupError( +            "GnuPG public key for keyid %s not found!" % keyid) + +    def find_key_by_fingerprint(self, fingerprint, secret=False): +        """ +        Find user's key based on the key fingerprint. + +        @param email: The fingerprint of the key being searched for. +        @type email: str +        @param secret: Should we search for a secret key? +        @type secret: bool + +        @return: The fingerprint of the found key. +        @rtype: str +        """ +        for key in self.list_keys(secret=secret): +            if fingerprint == key['fingerprint']: +                return key +        raise LookupError( +            "GnuPG public key for fingerprint %s not found!" % fingerprint) + +    def encrypt(self, data, recipient, sign=None, always_trust=True, +                passphrase=None, symmetric=False): +        """ +        Encrypt data using GPG. + +        @param data: The data to be encrypted. +        @type data: str +        @param recipient: The address of the public key to be used. +        @type recipient: str +        @param sign: Should the encrypted content be signed? +        @type sign: bool +        @param always_trust: Skip key validation and assume that used keys +            are always fully trusted? +        @type always_trust: bool +        @param passphrase: The passphrase to be used if symmetric encryption +            is desired. +        @type passphrase: str +        @param symmetric: Should we encrypt to a password? +        @type symmetric: bool + +        @return: An object with encrypted result in the `data` field. +        @rtype: gnupg.Crypt +        """ +        # TODO: devise a way so we don't need to "always trust". +        return gnupg.GPG.encrypt(self, data, recipient, sign=sign, +                                 always_trust=always_trust, +                                 passphrase=passphrase, +                                 symmetric=symmetric, +                                 cipher_algo='AES256') + +    def decrypt(self, data, always_trust=True, passphrase=None): +        """ +        Decrypt data using GPG. + +        @param data: The data to be decrypted. +        @type data: str +        @param always_trust: Skip key validation and assume that used keys +            are always fully trusted? +        @type always_trust: bool +        @param passphrase: The passphrase to be used if symmetric encryption +            is desired. +        @type passphrase: str + +        @return: An object with decrypted result in the `data` field. +        @rtype: gnupg.Crypt +        """ +        # TODO: devise a way so we don't need to "always trust". +        return gnupg.GPG.decrypt(self, data, always_trust=always_trust, +                                 passphrase=passphrase) + +    def send_keys(self, keyserver, *keyids): +        """ +        Send keys to a keyserver + +        @param keyserver: The keyserver to send the keys to. +        @type keyserver: str +        @param keyids: The key ids to send. +        @type keyids: list + +        @return: A list of keys sent to server. +        @rtype: gnupg.ListKeys +        """ +        # TODO: write tests for this. +        # TODO: write a SendKeys class to handle status for this. +        result = self.result_map['list'](self) +        gnupg.logger.debug('send_keys: %r', keyids) +        data = gnupg._make_binary_stream("", self.encoding) +        args = ['--keyserver', keyserver, '--send-keys'] +        args.extend(keyids) +        self._handle_io(args, data, result, binary=True) +        gnupg.logger.debug('send_keys result: %r', result.__dict__) +        data.close() +        return result + +    def encrypt_file(self, file, recipients, sign=None, +                     always_trust=False, passphrase=None, +                     armor=True, output=None, symmetric=False, +                     cipher_algo=None): +        """ +        Encrypt the message read from the file-like object 'file'. + +        @param file: The file to be encrypted. +        @type data: file +        @param recipient: The address of the public key to be used. +        @type recipient: str +        @param sign: Should the encrypted content be signed? +        @type sign: bool +        @param always_trust: Skip key validation and assume that used keys +            are always fully trusted? +        @type always_trust: bool +        @param passphrase: The passphrase to be used if symmetric encryption +            is desired. +        @type passphrase: str +        @param armor: Create ASCII armored output? +        @type armor: bool +        @param output: Path of file to write results in. +        @type output: str +        @param symmetric: Should we encrypt to a password? +        @type symmetric: bool +        @param cipher_algo: Algorithm to use. +        @type cipher_algo: str + +        @return: An object with encrypted result in the `data` field. +        @rtype: gnupg.Crypt +        """ +        args = ['--encrypt'] +        if symmetric: +            args = ['--symmetric'] +            if cipher_algo: +                args.append('--cipher-algo %s' % cipher_algo) +        else: +            args = ['--encrypt'] +            if not _is_sequence(recipients): +                recipients = (recipients,) +            for recipient in recipients: +                args.append('--recipient "%s"' % recipient) +        if armor:  # create ascii-armored output - set to False for binary +            args.append('--armor') +        if output:  # write the output to a file with the specified name +            if os.path.exists(output): +                os.remove(output)  # to avoid overwrite confirmation message +            args.append('--output "%s"' % output) +        if sign: +            args.append('--sign --default-key "%s"' % sign) +        if always_trust: +            args.append("--always-trust") +        result = self.result_map['crypt'](self) +        self._handle_io(args, file, result, passphrase=passphrase, binary=True) +        logger.debug('encrypt result: %r', result.data) +        return result + +    def list_packets(self, data): +        """ +        List the sequence of packets. + +        @param data: The data to extract packets from. +        @type data: str + +        @return: An object with packet info. +        @rtype ListPackets +        """ +        args = ["--list-packets"] +        result = self.result_map['list-packets'](self) +        self._handle_io( +            args, +            _make_binary_stream(data, self.encoding), +            result, +        ) +        return result + +    def encrypted_to(self, data): +        """ +        Return the key to which data is encrypted to. + +        @param data: The data to be examined. +        @type data: str + +        @return: The fingerprint of the key to which data is encrypted to. +        @rtype: str +        """ +        # TODO: make this support multiple keys. +        result = self.list_packets(data) +        if not result.key: +            raise LookupError( +                "Content is not encrypted to a GnuPG key!") +        try: +            return self.find_key_by_keyid(result.key) +        except: +            return self.find_key_by_subkey(result.key) + +    def is_encrypted_sym(self, data): +        """ +        Say whether some chunk of data is encrypted to a symmetric key. + +        @param data: The data to be examined. +        @type data: str + +        @return: Whether data is encrypted to a symmetric key. +        @rtype: bool +        """ +        result = self.list_packets(data) +        return bool(result.need_passphrase_sym) + +    def is_encrypted_asym(self, data): +        """ +        Say whether some chunk of data is encrypted to a private key. + +        @param data: The data to be examined. +        @type data: str + +        @return: Whether data is encrypted to a private key. +        @rtype: bool +        """ +        result = self.list_packets(data) +        return bool(result.key) + +    def is_encrypted(self, data): +        """ +        Say whether some chunk of data is encrypted to a key. + +        @param data: The data to be examined. +        @type data: str + +        @return: Whether data is encrypted to a key. +        @rtype: bool +        """ +        return self.is_encrypted_asym(data) or self.is_encrypted_sym(data) diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/keys.py b/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/keys.py new file mode 100644 index 0000000..2e6bfe9 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/keys.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +# keys.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/>. + + +""" +Abstact key type and encryption scheme representations. +""" + + +try: +    import simplejson as json +except ImportError: +    import json  # noqa +import re + + +from hashlib import sha256 +from abc import ABCMeta, abstractmethod +from leap.common.check import leap_assert + + +# +# Key handling utilities +# + +def is_address(address): +    """ +    Return whether the given C{address} is in the form user@provider. + +    @param address: The address to be tested. +    @type address: str +    @return: Whether C{address} is in the form user@provider. +    @rtype: bool +    """ +    return bool(re.match('[\w.-]+@[\w.-]+', address)) + + +def build_key_from_dict(kClass, address, kdict): +    """ +    Build an C{kClass} key bound to C{address} based on info in C{kdict}. + +    @param address: The address bound to the key. +    @type address: str +    @param kdict: Dictionary with key data. +    @type kdict: dict +    @return: An instance of the key. +    @rtype: C{kClass} +    """ +    leap_assert(address == kdict['address'], 'Wrong address in key data.') +    return kClass( +        address, +        key_id=kdict['key_id'], +        fingerprint=kdict['fingerprint'], +        key_data=kdict['key_data'], +        private=kdict['private'], +        length=kdict['length'], +        expiry_date=kdict['expiry_date'], +        first_seen_at=kdict['first_seen_at'], +        last_audited_at=kdict['last_audited_at'], +        validation=kdict['validation'],  # TODO: verify for validation. +    ) + + +def keymanager_doc_id(ktype, address, private=False): +    """ +    Return the document id for the document containing a key for +    C{address}. + +    @param address: The type of the key. +    @type address: KeyType +    @param address: The address bound to the key. +    @type address: str +    @param private: Whether the key is private or not. +    @type private: bool +    @return: The document id for the document that stores a key bound to +        C{address}. +    @rtype: str +    """ +    leap_assert(is_address(address), "Wrong address format: %s" % address) +    ktype = str(ktype) +    visibility = 'private' if private else 'public' +    return sha256('keymanager-'+address+'-'+ktype+'-'+visibility).hexdigest() + + +# +# Abstraction for encryption keys +# + +class EncryptionKey(object): +    """ +    Abstract class for encryption keys. + +    A key is "validated" if the nicknym agent has bound the user address to a +    public key. Nicknym supports three different levels of key validation: + +    * Level 3 - path trusted: A path of cryptographic signatures can be traced +      from a trusted key to the key under evaluation. By default, only the +      provider key from the user's provider is a "trusted key". +    * level 2 - provider signed: The key has been signed by a provider key for +      the same domain, but the provider key is not validated using a trust +      path (i.e. it is only registered) +    * level 1 - registered: The key has been encountered and saved, it has no +      signatures (that are meaningful to the nicknym agent). +    """ + +    __metaclass__ = ABCMeta + +    def __init__(self, address, key_id=None, fingerprint=None, +                 key_data=None, private=None, length=None, expiry_date=None, +                 validation=None, first_seen_at=None, last_audited_at=None): +        self.address = address +        self.key_id = key_id +        self.fingerprint = fingerprint +        self.key_data = key_data +        self.private = private +        self.length = length +        self.expiry_date = expiry_date +        self.validation = validation +        self.first_seen_at = first_seen_at +        self.last_audited_at = last_audited_at + +    def get_json(self): +        """ +        Return a JSON string describing this key. + +        @return: The JSON string describing this key. +        @rtype: str +        """ +        return json.dumps({ +            'address': self.address, +            'type': str(self.__class__), +            'key_id': self.key_id, +            'fingerprint': self.fingerprint, +            'key_data': self.key_data, +            'private': self.private, +            'length': self.length, +            'expiry_date': self.expiry_date, +            'validation': self.validation, +            'first_seen_at': self.first_seen_at, +            'last_audited_at': self.last_audited_at, +            'tags': ['keymanager-key'], +        }) + + +# +# Encryption schemes +# + +class EncryptionScheme(object): +    """ +    Abstract class for Encryption Schemes. + +    A wrapper for a certain encryption schemes should know how to get and put +    keys in local storage using Soledad, how to generate new keys and how to +    find out about possibly encrypted content. +    """ + +    __metaclass__ = ABCMeta + +    def __init__(self, soledad): +        """ +        Initialize this Encryption Scheme. + +        @param soledad: A Soledad instance for local storage of keys. +        @type soledad: leap.soledad.Soledad +        """ +        self._soledad = soledad + +    @abstractmethod +    def get_key(self, address, private=False): +        """ +        Get key from local storage. + +        @param address: The address bound to the key. +        @type address: str +        @param private: Look for a private key instead of a public one? +        @type private: bool + +        @return: The key bound to C{address}. +        @rtype: EncryptionKey +        @raise KeyNotFound: If the key was not found on local storage. +        """ +        pass + +    @abstractmethod +    def put_key(self, key): +        """ +        Put a key in local storage. + +        @param key: The key to be stored. +        @type key: EncryptionKey +        """ +        pass + +    @abstractmethod +    def gen_key(self, address): +        """ +        Generate a new key. + +        @param address: The address bound to the key. +        @type address: str + +        @return: The key bound to C{address}. +        @rtype: EncryptionKey +        """ +        pass + +    @abstractmethod +    def delete_key(self, key): +        """ +        Remove C{key} from storage. + +        @param key: The key to be removed. +        @type key: EncryptionKey +        """ +        pass diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/openpgp.py b/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/openpgp.py new file mode 100644 index 0000000..e2ffe76 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/openpgp.py @@ -0,0 +1,459 @@ +# -*- coding: utf-8 -*- +# openpgp.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/>. + + +""" +Infrastructure for using OpenPGP keys in Key Manager. +""" + + +import re +import tempfile +import shutil + +from leap.common.check import leap_assert +from leap.common.keymanager.errors import ( +    KeyNotFound, +    KeyAlreadyExists, +    KeyAttributesDiffer +) +from leap.common.keymanager.keys import ( +    EncryptionKey, +    EncryptionScheme, +    is_address, +    keymanager_doc_id, +    build_key_from_dict, +) +from leap.common.keymanager.gpg import GPGWrapper + + +# +# Utility functions +# + +def encrypt_sym(data, passphrase): +    """ +    Encrypt C{data} with C{passphrase}. + +    @param data: The data to be encrypted. +    @type data: str +    @param passphrase: The passphrase used to encrypt C{data}. +    @type passphrase: str + +    @return: The encrypted data. +    @rtype: str +    """ + +    def _encrypt_cb(gpg): +        return gpg.encrypt( +                data, None, passphrase=passphrase, symmetric=True).data + +    return _safe_call(_encrypt_cb) + + +def decrypt_sym(data, passphrase): +    """ +    Decrypt C{data} with C{passphrase}. + +    @param data: The data to be decrypted. +    @type data: str +    @param passphrase: The passphrase used to decrypt C{data}. +    @type passphrase: str + +    @return: The decrypted data. +    @rtype: str +    """ + +    def _decrypt_cb(gpg): +        return gpg.decrypt(data, passphrase=passphrase).data + +    return _safe_call(_decrypt_cb) + + +def encrypt_asym(data, key): +    """ +    Encrypt C{data} using public @{key}. + +    @param data: The data to be encrypted. +    @type data: str +    @param key: The key used to encrypt. +    @type key: OpenPGPKey + +    @return: The encrypted data. +    @rtype: str +    """ +    leap_assert(key.private is False, 'Key is not public.') + +    def _encrypt_cb(gpg): +        return gpg.encrypt( +                data, key.fingerprint, symmetric=False).data + +    return _safe_call(_encrypt_cb, key.key_data) + + +def decrypt_asym(data, key): +    """ +    Decrypt C{data} using private @{key}. + +    @param data: The data to be decrypted. +    @type data: str +    @param key: The key used to decrypt. +    @type key: OpenPGPKey + +    @return: The decrypted data. +    @rtype: str +    """ +    leap_assert(key.private is True, 'Key is not private.') + +    def _decrypt_cb(gpg): +        return gpg.decrypt(data).data + +    return _safe_call(_decrypt_cb, key.key_data) + + +def is_encrypted(data): +    """ +    Return whether C{data} was encrypted using OpenPGP. + +    @param data: The data we want to know about. +    @type data: str + +    @return: Whether C{data} was encrypted using this wrapper. +    @rtype: bool +    """ + +    def _is_encrypted_cb(gpg): +        return gpg.is_encrypted(data) + +    return _safe_call(_is_encrypted_cb) + + +def is_encrypted_sym(data): +    """ +    Return whether C{data} was encrypted using a public OpenPGP key. + +    @param data: The data we want to know about. +    @type data: str + +    @return: Whether C{data} was encrypted using this wrapper. +    @rtype: bool +    """ + +    def _is_encrypted_cb(gpg): +        return gpg.is_encrypted_sym(data) + +    return _safe_call(_is_encrypted_cb) + + +def is_encrypted_asym(data): +    """ +    Return whether C{data} was asymmetrically encrypted using OpenPGP. + +    @param data: The data we want to know about. +    @type data: str + +    @return: Whether C{data} was encrypted using this wrapper. +    @rtype: bool +    """ + +    def _is_encrypted_cb(gpg): +        return gpg.is_encrypted_asym(data) + +    return _safe_call(_is_encrypted_cb) + + +def _build_key_from_gpg(address, key, key_data): +    """ +    Build an OpenPGPKey for C{address} based on C{key} from +    local gpg storage. + +    ASCII armored GPG key data has to be queried independently in this +    wrapper, so we receive it in C{key_data}. + +    @param address: The address bound to the key. +    @type address: str +    @param key: Key obtained from GPG storage. +    @type key: dict +    @param key_data: Key data obtained from GPG storage. +    @type key_data: str +    @return: An instance of the key. +    @rtype: OpenPGPKey +    """ +    return OpenPGPKey( +        address, +        key_id=key['keyid'], +        fingerprint=key['fingerprint'], +        key_data=key_data, +        private=True if key['type'] == 'sec' else False, +        length=key['length'], +        expiry_date=key['expires'], +        validation=None,  # TODO: verify for validation. +    ) + + +def _build_unitary_gpgwrapper(key_data=None): +    """ +    Return a temporary GPG wrapper keyring containing exactly zero or one +    keys. + +    Temporary unitary keyrings allow the to use GPG's facilities for exactly +    one key. This function creates an empty temporary keyring and imports +    C{key_data} if it is not None. + +    @param key_data: ASCII armored key data. +    @type key_data: str +    @return: A GPG wrapper with a unitary keyring. +    @rtype: gnupg.GPG +    """ +    tmpdir = tempfile.mkdtemp() +    gpg = GPGWrapper(gnupghome=tmpdir) +    leap_assert(len(gpg.list_keys()) is 0, 'Keyring not empty.') +    if key_data: +        gpg.import_keys(key_data) +        leap_assert( +            len(gpg.list_keys()) is 1, +            'Unitary keyring has wrong number of keys: %d.' +            % len(gpg.list_keys())) +    return gpg + + +def _destroy_unitary_gpgwrapper(gpg): +    """ +    Securely erase a unitary keyring. + +    @param gpg: A GPG wrapper instance. +    @type gpg: gnupg.GPG +    """ +    for secret in [True, False]: +        for key in gpg.list_keys(secret=secret): +            gpg.delete_keys( +                key['fingerprint'], +                secret=secret) +    leap_assert(len(gpg.list_keys()) is 0, 'Keyring not empty!') +    # TODO: implement some kind of wiping of data or a more secure way that +    # does not write to disk. +    shutil.rmtree(gpg.gnupghome) + + +def _safe_call(callback, key_data=None, **kwargs): +    """ +    Run C{callback} in an unitary keyring containing C{key_data}. + +    @param callback: Function whose first argument is the gpg keyring. +    @type callback: function(gnupg.GPG) +    @param key_data: ASCII armored key data. +    @type key_data: str +    @param **kwargs: Other eventual parameters for the callback. +    @type **kwargs: **dict + +    @return: The results of the callback. +    @rtype: str or bool +    """ +    gpg = _build_unitary_gpgwrapper(key_data) +    val = callback(gpg, **kwargs) +    _destroy_unitary_gpgwrapper(gpg) +    return val + + +# +# The OpenPGP wrapper +# + +class OpenPGPKey(EncryptionKey): +    """ +    Base class for OpenPGP keys. +    """ + + +class OpenPGPScheme(EncryptionScheme): +    """ +    A wrapper for OpenPGP keys. +    """ + +    def __init__(self, soledad): +        """ +        Initialize the OpenPGP wrapper. + +        @param soledad: A Soledad instance for key storage. +        @type soledad: leap.soledad.Soledad +        """ +        EncryptionScheme.__init__(self, soledad) + +    def gen_key(self, address): +        """ +        Generate an OpenPGP keypair bound to C{address}. + +        @param address: The address bound to the key. +        @type address: str +        @return: The key bound to C{address}. +        @rtype: OpenPGPKey +        @raise KeyAlreadyExists: If key already exists in local database. +        """ +        # make sure the key does not already exist +        leap_assert(is_address(address), 'Not an user address: %s' % address) +        try: +            self.get_key(address) +            raise KeyAlreadyExists(address) +        except KeyNotFound: +            pass + +        def _gen_key_cb(gpg): +            params = gpg.gen_key_input( +                key_type='RSA', +                key_length=4096, +                name_real=address, +                name_email=address, +                name_comment='Generated by LEAP Key Manager.') +            gpg.gen_key(params) +            pubkeys = gpg.list_keys() +            # assert for new key characteristics +            leap_assert( +                len(pubkeys) is 1,  # a unitary keyring! +                'Keyring has wrong number of keys: %d.' % len(pubkeys)) +            key = gpg.list_keys(secret=True).pop() +            leap_assert( +                len(key['uids']) is 1,  # with just one uid! +                'Wrong number of uids for key: %d.' % len(key['uids'])) +            leap_assert( +                re.match('.*<%s>$' % address, key['uids'][0]) is not None, +                'Key not correctly bound to address.') +            # insert both public and private keys in storage +            for secret in [True, False]: +                key = gpg.list_keys(secret=secret).pop() +                openpgp_key = _build_key_from_gpg( +                    address, key, +                    gpg.export_keys(key['fingerprint'], secret=secret)) +                self.put_key(openpgp_key) + +        _safe_call(_gen_key_cb) +        return self.get_key(address, private=True) + +    def get_key(self, address, private=False): +        """ +        Get key bound to C{address} from local storage. + +        @param address: The address bound to the key. +        @type address: str +        @param private: Look for a private key instead of a public one? +        @type private: bool + +        @return: The key bound to C{address}. +        @rtype: OpenPGPKey +        @raise KeyNotFound: If the key was not found on local storage. +        """ +        leap_assert(is_address(address), 'Not an user address: %s' % address) +        doc = self._get_key_doc(address, private) +        if doc is None: +            raise KeyNotFound(address) +        return build_key_from_dict(OpenPGPKey, address, doc.content) + +    def put_key_raw(self, data): +        """ +        Put key contained in raw C{data} in local storage. + +        @param data: The key data to be stored. +        @type data: str +        """ +        # TODO: add more checks for correct key data. +        leap_assert(data is not None, 'Data does not represent a key.') + +        def _put_key_raw_cb(gpg): + +            privkey = None +            pubkey = None +            try: +                privkey = gpg.list_keys(secret=True).pop() +            except IndexError: +                pass +            pubkey = gpg.list_keys(secret=False).pop()  # unitary keyring +            # extract adress from first uid on key +            match = re.match('.*<([\w.-]+@[\w.-]+)>.*', pubkey['uids'].pop()) +            leap_assert(match is not None, 'No user address in key data.') +            address = match.group(1) +            if privkey is not None: +                match = re.match( +                    '.*<([\w.-]+@[\w.-]+)>.*', privkey['uids'].pop()) +                leap_assert(match is not None, 'No user address in key data.') +                privaddress = match.group(1) +                leap_assert( +                    address == privaddress, +                    'Addresses in pub and priv key differ.') +                leap_assert( +                    pubkey['fingerprint'] == privkey['fingerprint'], +                    'Fingerprints for pub and priv key differ.') +                # insert private key in storage +                openpgp_privkey = _build_key_from_gpg( +                    address, privkey, +                    gpg.export_keys(privkey['fingerprint'], secret=True)) +                self.put_key(openpgp_privkey) +            # insert public key in storage +            openpgp_pubkey = _build_key_from_gpg( +                address, pubkey, +                gpg.export_keys(pubkey['fingerprint'], secret=False)) +            self.put_key(openpgp_pubkey) + +        _safe_call(_put_key_raw_cb, data) + +    def put_key(self, key): +        """ +        Put C{key} in local storage. + +        @param key: The key to be stored. +        @type key: OpenPGPKey +        """ +        doc = self._get_key_doc(key.address, private=key.private) +        if doc is None: +            self._soledad.create_doc_from_json( +                key.get_json(), +                doc_id=keymanager_doc_id( +                    OpenPGPKey, key.address, key.private)) +        else: +            doc.set_json(key.get_json()) +            self._soledad.put_doc(doc) + +    def _get_key_doc(self, address, private=False): +        """ +        Get the document with a key (public, by default) bound to C{address}. + +        If C{private} is True, looks for a private key instead of a public. + +        @param address: The address bound to the key. +        @type address: str +        @param private: Whether to look for a private key. +        @type private: bool +        @return: The document with the key or None if it does not exist. +        @rtype: leap.soledad.backends.leap_backend.LeapDocument +        """ +        return self._soledad.get_doc( +            keymanager_doc_id(OpenPGPKey, address, private)) + +    def delete_key(self, key): +        """ +        Remove C{key} from storage. + +        @param key: The key to be removed. +        @type key: EncryptionKey +        """ +        leap_assert(key.__class__ is OpenPGPKey, 'Wrong key type.') +        stored_key = self.get_key(key.address, private=key.private) +        if stored_key is None: +            raise KeyNotFound(key) +        if stored_key.__dict__ != key.__dict__: +            raise KeyAttributesDiffer(key) +        doc = self._soledad.get_doc( +            keymanager_doc_id(OpenPGPKey, key.address, key.private)) +        self._soledad.delete_doc(doc) diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/testing/__init__.py b/debian/python-leap.common/usr/share/pyshared/leap/common/testing/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/testing/__init__.py diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/testing/basetest.py b/debian/python-leap.common/usr/share/pyshared/leap/common/testing/basetest.py new file mode 100644 index 0000000..65e23a9 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/testing/basetest.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# basetest.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/>. +""" +Common testing facilities +""" +import os +import platform +import shutil +import tempfile + +try: +    import unittest2 as unittest +except ImportError: +    import unittest + +from leap.common.check import leap_assert +from leap.common.files import mkdir_p, check_and_fix_urw_only + + +class BaseLeapTest(unittest.TestCase): +    """ +    Base Leap TestCase +    """ +    __name__ = "leap_test" +    _system = platform.system() + +    @classmethod +    def setUpClass(cls): +        """ +        Sets up common facilities for testing this TestCase: +        - custom PATH and HOME environmental variables +        - creates a temporal folder to which those point. +        It saves the old path and home vars so they can be restored later. +        """ +        cls.old_path = os.environ['PATH'] +        cls.old_home = os.environ['HOME'] +        cls.tempdir = tempfile.mkdtemp(prefix="leap_tests-") +        cls.home = cls.tempdir +        bin_tdir = os.path.join( +            cls.tempdir, +            'bin') +        os.environ["PATH"] = bin_tdir +        os.environ["HOME"] = cls.tempdir + +    @classmethod +    def tearDownClass(cls): +        """ +        Cleanup common facilities used for testing this TestCase: +        - restores the default PATH and HOME variables +        - removes the temporal folder +        """ +        os.environ["PATH"] = cls.old_path +        os.environ["HOME"] = cls.old_home +        # safety check! please do not wipe my home... +        # XXX needs to adapt to non-linuces +        leap_assert( +            cls.tempdir.startswith('/tmp/leap_tests-'), +            "beware! tried to remove a dir which does not " +            "live in temporal folder!") +        shutil.rmtree(cls.tempdir) + +    # you have to override these methods +    # this way we ensure we did not put anything +    # here that you can forget to call. + +    def setUp(self): +        """not implemented""" +        raise NotImplementedError("abstract base class") + +    def tearDown(self): +        """not implemented""" +        raise NotImplementedError("abstract base class") + +    # +    # helper methods +    # + +    def _missing_test_for_plat(self, do_raise=False): +        """ +        Raises NotImplementedError for this platform +        if do_raise is True + +        @param do_raise: flag to actually raise exception +        @type do_raise: bool +        """ +        if do_raise: +            raise NotImplementedError( +                "This test is not implemented " +                "for the running platform: %s" % +                self._system) + +    def get_tempfile(self, filename): +        """ +        Returns the path of a given filename +        prepending the temporal dir associated with this +        TestCase + +        @param filename: the filename +        @type filename: str +        """ +        return os.path.join(self.tempdir, filename) + +    def touch(self, filepath): +        """ +        Touches a filepath, creating folders along +        the way if needed. + +        @param filepath: path to be touched +        @type filepath: str +        """ +        folder, filename = os.path.split(filepath) +        if not os.path.isdir(folder): +            mkdir_p(folder) +        self.assertTrue(os.path.isdir(folder)) +        with open(filepath, 'w') as fp: +            fp.write(' ') +        self.assertTrue(os.path.isfile(filepath)) + +    def chmod600(self, filepath): +        """ +        Chmods 600 a file + +        @param filepath: filepath to be chmodded +        @type filepath: str +        """ +        check_and_fix_urw_only(filepath) diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/testing/https_server.py b/debian/python-leap.common/usr/share/pyshared/leap/common/testing/https_server.py new file mode 100644 index 0000000..08d5089 --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/testing/https_server.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# leap.common.testing.https_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 simple HTTPS server to be used in tests +""" +from BaseHTTPServer import HTTPServer +import os +import ssl +import SocketServer +import threading +import unittest + +_where = os.path.split(__file__)[0] + + +def where(filename): +    return os.path.join(_where, filename) + + +class HTTPSServer(HTTPServer): +    def server_bind(self): +        SocketServer.TCPServer.server_bind(self) +        self.socket = ssl.wrap_socket( +            self.socket, server_side=True, +            certfile=where("leaptestscert.pem"), +            keyfile=where("leaptestskey.pem"), +            ca_certs=where("cacert.pem"), +            ssl_version=ssl.PROTOCOL_SSLv23) + + +class TestServerThread(threading.Thread): +    def __init__(self, test_object, request_handler): +        threading.Thread.__init__(self) +        self.request_handler = request_handler +        self.test_object = test_object + +    def run(self): +        self.server = HTTPSServer(('localhost', 0), self.request_handler) +        host, port = self.server.socket.getsockname() +        self.test_object.HOST, self.test_object.PORT = host, port +        self.test_object.server_started.set() +        self.test_object = None +        try: +            self.server.serve_forever(0.05) +        finally: +            self.server.server_close() + +    def stop(self): +        self.server.shutdown() + + +class BaseHTTPSServerTestCase(unittest.TestCase): +    """ +    derived classes need to implement a request_handler +    """ +    def setUp(self): +        self.server_started = threading.Event() +        self.thread = TestServerThread(self, self.request_handler) +        self.thread.start() +        self.server_started.wait() + +    def tearDown(self): +        self.thread.stop() + +    def get_server(self): +        host, port = self.HOST, self.PORT +        if host == "127.0.0.1": +            host = "localhost" +        return "%s:%s" % (host, port) + + +if __name__ == "__main__": +    unittest.main() diff --git a/debian/python-leap.common/usr/share/pyshared/leap/common/testing/test_basetest.py b/debian/python-leap.common/usr/share/pyshared/leap/common/testing/test_basetest.py new file mode 100644 index 0000000..220e28d --- /dev/null +++ b/debian/python-leap.common/usr/share/pyshared/leap/common/testing/test_basetest.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# test_basetest.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/>. +""" +Unittests for BaseLeapTest ...becase it's oh so meta +""" +try: +    import unittest2 as unittest +except ImportError: +    import unittest + +import os +import StringIO + +from leap.common.testing.basetest import BaseLeapTest + +_tempdir = None  # global for tempdir checking + + +class _TestCaseRunner(object): +    """ +    TestCaseRunner used to run BaseLeapTest +    """ +    def run_testcase(self, testcase=None): +        """ +        Runs a given TestCase + +        @param testcase: the testcase +        @type testcase: unittest.TestCase +        """ +        if not testcase: +            return None +        loader = unittest.TestLoader() +        suite = loader.loadTestsFromTestCase(testcase) + +        # Create runner, and run testcase +        io = StringIO.StringIO() +        runner = unittest.TextTestRunner(stream=io) +        results = runner.run(suite) +        return results + + +class TestAbstractBaseLeapTest(unittest.TestCase, _TestCaseRunner): +    """ +    TestCase for BaseLeapTest abs +    """ +    def test_abstract_base_class(self): +        """ +        Test errors raised when setup/teardown not overloaded +        """ +        class _BaseTest(BaseLeapTest): +            def test_dummy_method(self): +                pass + +            def test_tautology(self): +                assert True + +        results = self.run_testcase(_BaseTest) + +        # should be 2 errors: NotImplemented +        # raised for setUp/tearDown +        self.assertEquals(results.testsRun, 2) +        self.assertEquals(len(results.failures), 0) +        self.assertEquals(len(results.errors), 2) + + +class TestInitBaseLeapTest(BaseLeapTest): +    """ +    TestCase for testing initialization of BaseLeapTest +    """ + +    def setUp(self): +        """nuke it""" +        pass + +    def tearDown(self): +        """nuke it""" +        pass + +    def test_path_is_changed(self): +        """tests whether we have changed the PATH env var""" +        os_path = os.environ['PATH'] +        self.assertTrue(os_path.startswith(self.tempdir)) + +    def test_old_path_is_saved(self): +        """tests whether we restore the PATH env var""" +        self.assertTrue(len(self.old_path) > 1) + + +class TestCleanedBaseLeapTest(unittest.TestCase, _TestCaseRunner): +    """ +    TestCase for testing tempdir creation and cleanup +    """ + +    def test_tempdir_is_cleaned_after_tests(self): +        """ +        test if a TestCase derived from BaseLeapTest creates and cleans the +        temporal dir +        """ +        class _BaseTest(BaseLeapTest): +            def setUp(self): +                """set global _tempdir to this instance tempdir""" +                global _tempdir +                _tempdir = self.tempdir + +            def tearDown(self): +                """nothing""" +                pass + +            def test_tempdir_created(self): +                """test if tempdir was created""" +                self.assertTrue(os.path.isdir(self.tempdir)) + +            def test_tempdir_created_on_setupclass(self): +                """test if tempdir is the one created by setupclass""" +                self.assertEqual(_tempdir, self.tempdir) + +        results = self.run_testcase(_BaseTest) +        self.assertEquals(results.testsRun, 2) +        self.assertEquals(len(results.failures), 0) +        self.assertEquals(len(results.errors), 0) + +        # did we cleaned the tempdir? +        self.assertFalse(os.path.isdir(_tempdir)) + +if __name__ == "__main__": +    unittest.main() | 
