summaryrefslogtreecommitdiff
path: root/debian/python-leap.common/usr/share/pyshared/leap/common
diff options
context:
space:
mode:
authorMicah Anderson <micah@riseup.net>2013-05-30 14:45:06 -0400
committerMicah Anderson <micah@riseup.net>2013-05-30 14:45:06 -0400
commit2f4d6b60f5c5581a0e53ffa77b747485b1e64d7b (patch)
tree67827d0d6747c49046d875316d8868c5d4191644 /debian/python-leap.common/usr/share/pyshared/leap/common
parentdbbf33d27268266088ee1ab15fc04cef33259250 (diff)
initial debian packaging
Diffstat (limited to 'debian/python-leap.common/usr/share/pyshared/leap/common')
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/__init__.py19
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/certs.py179
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/check.py61
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/config/__init__.py0
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/config/baseconfig.py185
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/config/pluggableconfig.py475
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/config/prefixers.py132
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/events/__init__.py100
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/events/component.py238
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/events/daemon.py208
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/events/events_pb2.py371
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/events/mac_auth.py31
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/events/server.py149
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/files.py126
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/__init__.py286
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/errors.py46
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/gpg.py397
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/keys.py230
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/keymanager/openpgp.py459
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/testing/__init__.py0
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/testing/basetest.py140
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/testing/https_server.py87
-rw-r--r--debian/python-leap.common/usr/share/pyshared/leap/common/testing/test_basetest.py140
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()