diff options
Diffstat (limited to 'src/leap/common')
26 files changed, 421 insertions, 1180 deletions
diff --git a/src/leap/common/_version.py b/src/leap/common/_version.py index 2f2cac0..4f57eb5 100644 --- a/src/leap/common/_version.py +++ b/src/leap/common/_version.py @@ -1,13 +1,21 @@ # This file was generated by the `freeze_debianver` command in setup.py -# Using 'versioneer.py' (0.7+) from +# Using 'versioneer.py' (0.16) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. -version_version = '0.5.0' -version_full = 'dd032e7374fa137a8613c2392d744b9b16280fca' +import json +import sys +version_json = ''' +{ + "dirty": false, + "error": null, + "full-revisionid": "73f71ff59fa0787ea661b7d0c11ffa0261609d49", + "version": "0.5.2" +} +''' # END VERSION_JSON -def get_versions(default={}, verbose=False): - return {'version': version_version, 'full': version_full} +def get_versions(): + return json.loads(version_json) diff --git a/src/leap/common/certs.py b/src/leap/common/certs.py index c49015a..95704a6 100644 --- a/src/leap/common/certs.py +++ b/src/leap/common/certs.py @@ -192,8 +192,8 @@ def get_compatible_ssl_context_factory(cert_path=None): class WebClientContextFactory(ssl.ClientContextFactory): """ - A web context factory which ignores the hostname and port and does no - certificate verification. + A web context factory which ignores the hostname and port and does + no certificate verification. """ def getContext(self, hostname, port): return ssl.ClientContextFactory.getContext(self) diff --git a/src/leap/common/config/__init__.py b/src/leap/common/config/__init__.py index 68d92dc..15c6fea 100644 --- a/src/leap/common/config/__init__.py +++ b/src/leap/common/config/__init__.py @@ -18,8 +18,22 @@ Common configs """ import os +import sys -from dirspec.basedir import get_xdg_config_home + +def _get_xdg_config_home(): + if sys.platform == 'win32': + from win32com.shell import shell, shellcon + get_path = lambda name: shell.SHGetFolderPath( + 0, getattr(shellcon, name), None, 0).encode('utf8') + path = get_path('CSIDL_LOCAL_APPDATA') + elif sys.platform == 'darwin': + user_home = os.path.expanduser('~') + path = os.path.join(user_home, 'Library', 'Preferences') + else: + user_home = os.path.expanduser('~') + path = os.path.join(user_home, '.config') + return path def get_path_prefix(standalone=False): @@ -32,8 +46,6 @@ def get_path_prefix(standalone=False): configuration storage. :type standalone: bool """ - config_home = get_xdg_config_home() if standalone: - config_home = os.path.join(os.getcwd(), "config") - - return config_home + return os.path.join(os.getcwd(), "config") + return _get_xdg_config_home() diff --git a/src/leap/common/config/tests/test_baseconfig.py b/src/leap/common/config/tests/test_baseconfig.py deleted file mode 100644 index e17e82d..0000000 --- a/src/leap/common/config/tests/test_baseconfig.py +++ /dev/null @@ -1,271 +0,0 @@ -# -*- coding: utf-8 -*- -# test_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/>. -""" -Tests for baseconfig -""" -import json -import unittest -import copy - -from leap.common.config.baseconfig import BaseConfig, LocalizedKey -from leap.common.testing.basetest import BaseLeapTest - -from mock import Mock - -# reduced eipconfig sample config -sample_config = { - "gateways": [ - { - "capabilities": { - "adblock": False, - "transport": ["openvpn"], - "user_ips": False - }, - "host": "host.dev.example.org", - }, { - "capabilities": { - "adblock": False, - "transport": ["openvpn"], - "user_ips": False - }, - "host": "host2.dev.example.org", - } - ], - "default_language": "en", - "languages": [ - "en", - "es" - ], - "name": { - "en": "Baseconfig testing environment", - "es": "Entorno de pruebas de Baseconfig" - }, - "serial": 1, - "version": 1 -} - -# reduced eipconfig.spec version -sample_spec = { - 'description': 'sample eip service config', - 'type': 'object', - 'properties': { - 'serial': { - 'type': int, - 'default': 1, - 'required': ["True"] - }, - 'version': { - 'type': int, - 'default': 1, - 'required': ["True"] - }, - "default_language": { - 'type': unicode, - 'default': 'en' - }, - 'languages': { - 'type': list, - 'default': ['en'] - }, - 'name': { - 'type': dict, - 'format': 'translatable', - 'default': {u'en': u'Test Provider'} - }, - 'gateways': { - 'type': list, - 'default': [ - {"capabilities": { - "adblock": True, - "transport": ["openvpn"], - "user_ips": False}, - "host": "location.example.org", - }] - }, - } -} - - -class TestConfig(BaseConfig): - """ - BaseConfig implementation for testing purposes only. - """ - def get_gateways(self): - return self._safe_get_value("gateways") - - def get_serial(self): - return self._safe_get_value("serial") - - def get_version(self): - return self._safe_get_value("version") - - def _get_schema(self): - return sample_spec - - def _get_spec(self): - return self._get_schema() - - def get_default_language(self): - return self._safe_get_value("default_language") - - @LocalizedKey - def get_name(self): - return self._safe_get_value("name") - - -class BaseConfigTest(BaseLeapTest): - - def setUp(self): - pass - - def tearDown(self): - pass - - def _write_config(self, data): - """ - Helper to write some data to a temp config file. - - :param data: data to be used to save in the config file. - :data type: dict (valid json) - """ - self.config_file = self.get_tempfile("config.json") - conf = open(self.config_file, "w") - conf.write(json.dumps(data)) - conf.close() - - def _get_config(self, fromfile=False, data=sample_config): - """ - Helper that returns a TestConfig object using the data parameter - or a sample data. - - :param fromfile: sets if we should use a file or a string - :fromfile type: bool - :param data: sets the data to be used to load in the TestConfig object - :data type: dict (valid json) - :rtype: TestConfig - """ - config = TestConfig() - - loaded = False - if fromfile: - self._write_config(data) - loaded = config.load(self.config_file, relative=False) - else: - json_string = json.dumps(data) - loaded = config.load(data=json_string) - - if not loaded: - return None - - return config - - def test_loads_from_file(self): - config = self._get_config(fromfile=True) - self.assertIsNotNone(config) - - def test_loads_from_data(self): - config = self._get_config() - self.assertIsNotNone(config) - - def test_load_valid_config_from_file(self): - config = self._get_config(fromfile=True) - self.assertIsNotNone(config) - - self.assertEqual(config.get_version(), sample_config["version"]) - self.assertEqual(config.get_serial(), sample_config["serial"]) - self.assertEqual(config.get_gateways(), sample_config["gateways"]) - - def test_load_valid_config_from_data(self): - config = self._get_config() - self.assertIsNotNone(config) - - self.assertEqual(config.get_version(), sample_config["version"]) - self.assertEqual(config.get_serial(), sample_config["serial"]) - self.assertEqual(config.get_gateways(), sample_config["gateways"]) - - def test_safe_get_value_no_config(self): - config = TestConfig() - - with self.assertRaises(AssertionError): - config.get_version() - - def test_safe_get_value_non_existent_value(self): - config = self._get_config() - - self.assertIsNone(config._safe_get_value('non-existent-value')) - - def test_loaded(self): - config = self._get_config() - self.assertTrue(config.loaded()) - - def test_not_loaded(self): - config = TestConfig() - self.assertFalse(config.loaded()) - - def test_save_and_load(self): - config = self._get_config() - config.get_path_prefix = Mock(return_value=self.tempdir) - config_file = 'test_config.json' - self.assertTrue(config.save([config_file])) - - config_saved = TestConfig() - config_file_path = self.get_tempfile(config_file) - self.assertTrue(config_saved.load(config_file_path, relative=False)) - - self.assertEqual(config.get_version(), config_saved.get_version()) - self.assertEqual(config.get_serial(), config_saved.get_serial()) - self.assertEqual(config.get_gateways(), config_saved.get_gateways()) - - def test_localizations(self): - conf = self._get_config() - - self.assertEqual(conf.get_name(lang='en'), sample_config['name']['en']) - self.assertEqual(conf.get_name(lang='es'), sample_config['name']['es']) - - def _localized_config(self, lang): - """ - Helper to change default language of the provider config. - """ - conf = copy.deepcopy(sample_config) - conf['default_language'] = lang - json_string = json.dumps(conf) - config = TestConfig() - config.load(data=json_string) - - return config - - def test_default_localization1(self): - default_language = sample_config['languages'][0] - config = self._localized_config(default_language) - - default_name = sample_config['name'][default_language] - - self.assertEqual(config.get_name(lang='xx'), default_name) - self.assertEqual(config.get_name(), default_name) - - def test_default_localization2(self): - default_language = sample_config['languages'][1] - config = self._localized_config(default_language) - - default_name = sample_config['name'][default_language] - - self.assertEqual(config.get_name(lang='xx'), default_name) - self.assertEqual(config.get_name(), default_name) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/src/leap/common/config/tests/test_get_path_prefix.py b/src/leap/common/config/tests/test_get_path_prefix.py deleted file mode 100644 index 27824fc..0000000 --- a/src/leap/common/config/tests/test_get_path_prefix.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -# test_get_path_prefix.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/>. -""" -Tests for get_path_prefix -""" -import os -import mock - -try: - import unittest2 as unittest -except ImportError: - import unittest - -from leap.common.config import get_path_prefix -from leap.common.testing.basetest import BaseLeapTest - - -class GetPathPrefixTest(BaseLeapTest): - """ - Tests for the get_path_prefix helper. - - Note: we only are testing that the path is correctly returned and that if - we are not in a bundle (standalone=False) then the paths are different. - - dirspec calculates the correct path using different methods and dlls - (in case of Windows) so we don't implement tests to check if the paths - are the correct ones. - """ - def setUp(self): - pass - - def tearDown(self): - pass - - def test_standalone_path(self): - expected_path = os.path.join('expected', 'path', 'config') - fake_cwd = os.path.join('expected', 'path') - with mock.patch('os.getcwd', lambda: fake_cwd): - path = get_path_prefix(standalone=True) - self.assertEquals(path, expected_path) - - def test_path_prefix(self): - standalone_path = get_path_prefix(standalone=True) - path = get_path_prefix(standalone=False) - self.assertNotEquals(path, standalone_path) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/src/leap/common/events/auth.py b/src/leap/common/events/auth.py new file mode 100644 index 0000000..db217ca --- /dev/null +++ b/src/leap/common/events/auth.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# auth.py +# Copyright (C) 2016 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/>. +""" +ZAP authentication, twisted style. +""" +from zmq import PAIR +from zmq.auth.base import Authenticator, VERSION +from txzmq.connection import ZmqConnection +from zmq.utils.strtypes import b, u + +from twisted.python import log + +from txzmq.connection import ZmqEndpoint, ZmqEndpointType + + +class TxAuthenticator(ZmqConnection): + + """ + This does not implement the whole ZAP protocol, but the bare minimum that + we need. + """ + + socketType = PAIR + address = 'inproc://zeromq.zap.01' + encoding = 'utf-8' + + def __init__(self, factory, *args, **kw): + super(TxAuthenticator, self).__init__(factory, *args, **kw) + self.authenticator = Authenticator(factory.context) + self.authenticator._send_zap_reply = self._send_zap_reply + + def start(self): + endpoint = ZmqEndpoint(ZmqEndpointType.bind, self.address) + self.addEndpoints([endpoint]) + + def messageReceived(self, msg): + + command = msg[0] + + if command == b'ALLOW': + addresses = [u(m, self.encoding) for m in msg[1:]] + try: + self.authenticator.allow(*addresses) + except Exception as e: + log.err("Failed to allow %s", addresses) + + elif command == b'CURVE': + domain = u(msg[1], self.encoding) + location = u(msg[2], self.encoding) + self.authenticator.configure_curve(domain, location) + + def _send_zap_reply(self, request_id, status_code, status_text, + user_id='user'): + """ + Send a ZAP reply to finish the authentication. + """ + user_id = user_id if status_code == b'200' else b'' + if isinstance(user_id, unicode): + user_id = user_id.encode(self.encoding, 'replace') + metadata = b'' # not currently used + reply = [VERSION, request_id, status_code, status_text, + user_id, metadata] + self.send(reply) + + def shutdown(self): + if self.factory: + super(TxAuthenticator, self).shutdown() + + +class TxAuthenticationRequest(ZmqConnection): + + socketType = PAIR + address = 'inproc://zeromq.zap.01' + encoding = 'utf-8' + + def start(self): + endpoint = ZmqEndpoint(ZmqEndpointType.connect, self.address) + self.addEndpoints([endpoint]) + + def allow(self, *addresses): + self.send([b'ALLOW'] + [b(a, self.encoding) for a in addresses]) + + def configure_curve(self, domain='*', location=''): + domain = b(domain, self.encoding) + location = b(location, self.encoding) + self.send([b'CURVE', domain, location]) diff --git a/src/leap/common/events/catalog.py b/src/leap/common/events/catalog.py index 8bddd2c..9a834b2 100644 --- a/src/leap/common/events/catalog.py +++ b/src/leap/common/events/catalog.py @@ -24,49 +24,54 @@ Events catalog. EVENTS = [ "CLIENT_SESSION_ID", "CLIENT_UID", - "IMAP_CLIENT_LOGIN", - "IMAP_SERVICE_FAILED_TO_START", - "IMAP_SERVICE_STARTED", - "IMAP_UNHANDLED_ERROR", - "KEYMANAGER_DONE_UPLOADING_KEYS", - "KEYMANAGER_FINISHED_KEY_GENERATION", - "KEYMANAGER_KEY_FOUND", - "KEYMANAGER_KEY_NOT_FOUND", - "KEYMANAGER_LOOKING_FOR_KEY", - "KEYMANAGER_STARTED_KEY_GENERATION", - "MAIL_FETCHED_INCOMING", - "MAIL_MSG_DECRYPTED", - "MAIL_MSG_DELETED_INCOMING", - "MAIL_MSG_PROCESSING", - "MAIL_MSG_SAVED_LOCALLY", - "MAIL_UNREAD_MESSAGES", "RAISE_WINDOW", - "SMTP_CONNECTION_LOST", - "SMTP_END_ENCRYPT_AND_SIGN", - "SMTP_END_SIGN", - "SMTP_RECIPIENT_ACCEPTED_ENCRYPTED", - "SMTP_RECIPIENT_ACCEPTED_UNENCRYPTED", - "SMTP_RECIPIENT_REJECTED", - "SMTP_SEND_MESSAGE_ERROR", - "SMTP_SEND_MESSAGE_START", - "SMTP_SEND_MESSAGE_SUCCESS", - "SMTP_SERVICE_FAILED_TO_START", - "SMTP_SERVICE_STARTED", - "SMTP_START_ENCRYPT_AND_SIGN", - "SMTP_START_SIGN", - "SOLEDAD_CREATING_KEYS", - "SOLEDAD_DONE_CREATING_KEYS", - "SOLEDAD_DONE_DATA_SYNC", - "SOLEDAD_DONE_DOWNLOADING_KEYS", - "SOLEDAD_DONE_UPLOADING_KEYS", - "SOLEDAD_DOWNLOADING_KEYS", - "SOLEDAD_INVALID_AUTH_TOKEN", - "SOLEDAD_NEW_DATA_TO_SYNC", - "SOLEDAD_SYNC_RECEIVE_STATUS", - "SOLEDAD_SYNC_SEND_STATUS", - "SOLEDAD_UPLOADING_KEYS", "UPDATER_DONE_UPDATING", "UPDATER_NEW_UPDATES", + + "KEYMANAGER_DONE_UPLOADING_KEYS", # (address) + "KEYMANAGER_FINISHED_KEY_GENERATION", # (address) + "KEYMANAGER_KEY_FOUND", # (address) + "KEYMANAGER_KEY_NOT_FOUND", # (address) + "KEYMANAGER_LOOKING_FOR_KEY", # (address) + "KEYMANAGER_STARTED_KEY_GENERATION", # (address) + + "SOLEDAD_CREATING_KEYS", # {uuid, userid} + "SOLEDAD_DONE_CREATING_KEYS", # {uuid, userid} + "SOLEDAD_DONE_DATA_SYNC", # {uuid, userid} + "SOLEDAD_DONE_DOWNLOADING_KEYS", # {uuid, userid} + "SOLEDAD_DONE_UPLOADING_KEYS", # {uuid, userid} + "SOLEDAD_DOWNLOADING_KEYS", # {uuid, userid} + "SOLEDAD_INVALID_AUTH_TOKEN", # {uuid, userid} + "SOLEDAD_SYNC_RECEIVE_STATUS", # {uuid, userid} + "SOLEDAD_SYNC_SEND_STATUS", # {uuid, userid} + "SOLEDAD_UPLOADING_KEYS", # {uuid, userid} + "SOLEDAD_NEW_DATA_TO_SYNC", + + "MAIL_FETCHED_INCOMING", # (userid) + "MAIL_MSG_DECRYPTED", # (userid) + "MAIL_MSG_DELETED_INCOMING", # (userid) + "MAIL_MSG_PROCESSING", # (userid) + "MAIL_MSG_SAVED_LOCALLY", # (userid) + "MAIL_UNREAD_MESSAGES", # (userid, number) + + "IMAP_SERVICE_STARTED", + "IMAP_SERVICE_FAILED_TO_START", + "IMAP_UNHANDLED_ERROR", + "IMAP_CLIENT_LOGIN", # (username) + + "SMTP_SERVICE_STARTED", + "SMTP_SERVICE_FAILED_TO_START", + "SMTP_START_ENCRYPT_AND_SIGN", # (from_addr) + "SMTP_END_ENCRYPT_AND_SIGN", # (from_addr) + "SMTP_START_SIGN", # (from_addr) + "SMTP_END_SIGN", # (from_addr) + "SMTP_SEND_MESSAGE_START", # (from_addr) + "SMTP_SEND_MESSAGE_SUCCESS", # (from_addr) + "SMTP_RECIPIENT_ACCEPTED_ENCRYPTED", # (userid, dest) + "SMTP_RECIPIENT_ACCEPTED_UNENCRYPTED", # (userid, dest) + "SMTP_CONNECTION_LOST", # (userid, dest) + "SMTP_RECIPIENT_REJECTED", # (userid, dest) + "SMTP_SEND_MESSAGE_ERROR", # (userid, dest) ] diff --git a/src/leap/common/events/client.py b/src/leap/common/events/client.py index 60d24bc..78617de 100644 --- a/src/leap/common/events/client.py +++ b/src/leap/common/events/client.py @@ -63,14 +63,18 @@ logger = logging.getLogger(__name__) _emit_addr = EMIT_ADDR _reg_addr = REG_ADDR +_factory = None +_enable_curve = True -def configure_client(emit_addr, reg_addr): - global _emit_addr, _reg_addr +def configure_client(emit_addr, reg_addr, factory=None, enable_curve=True): + global _emit_addr, _reg_addr, _factory, _enable_curve logger.debug("Configuring client with addresses: (%s, %s)" % (emit_addr, reg_addr)) _emit_addr = emit_addr _reg_addr = reg_addr + _factory = factory + _enable_curve = enable_curve class EventsClient(object): @@ -103,7 +107,9 @@ class EventsClient(object): """ with cls._instance_lock: if cls._instance is None: - cls._instance = cls(_emit_addr, _reg_addr) + cls._instance = cls( + _emit_addr, _reg_addr, factory=_factory, + enable_curve=_enable_curve) return cls._instance def register(self, event, callback, uid=None, replace=False): @@ -270,7 +276,7 @@ class EventsClientThread(threading.Thread, EventsClient): A threaded version of the events client. """ - def __init__(self, emit_addr, reg_addr): + def __init__(self, emit_addr, reg_addr, factory=None, enable_curve=True): """ Initialize the events client. """ @@ -281,15 +287,22 @@ class EventsClientThread(threading.Thread, EventsClient): self._config_prefix = os.path.join( get_path_prefix(flags.STANDALONE), "leap", "events") self._loop = None + self._factory = factory self._context = None self._push = None self._sub = None + if enable_curve: + self.use_curve = zmq_has_curve() + else: + self.use_curve = False + def _init_zmq(self): """ Initialize ZMQ connections. """ self._loop = EventsIOLoop() + # we need a new context for each thread self._context = zmq.Context() # connect SUB first, otherwise we might miss some event sent from this # same client @@ -311,7 +324,7 @@ class EventsClientThread(threading.Thread, EventsClient): logger.debug("Connecting %s to %s." % (socktype, address)) socket = self._context.socket(socktype) # configure curve authentication - if zmq_has_curve(): + if self.use_curve: public, private = maybe_create_and_get_certificates( self._config_prefix, "client") server_public_file = os.path.join( diff --git a/src/leap/common/events/examples/README.txt b/src/leap/common/events/examples/README.txt new file mode 100644 index 0000000..0bb0df6 --- /dev/null +++ b/src/leap/common/events/examples/README.txt @@ -0,0 +1,49 @@ +How to debug +----------------------------------------- +monitor the events socket: + sudo ngrep -W byline -d any port 9000 + +launch the server: + python server.py + +launch the client: + python client.py + +if zmq is available and enabled, you should see encrypted messages passing by +the socket. + +You should see something like the following: + +#### +T 127.0.0.1:9000 -> 127.0.0.1:33122 [AP] +.......... +## +T 127.0.0.1:33122 -> 127.0.0.1:9000 [AP] +........... +## +T 127.0.0.1:9000 -> 127.0.0.1:33122 [AP] +..CURVE............................................... +# +T 127.0.0.1:33122 -> 127.0.0.1:9000 [AP] +.CURVE............................................... +# +T 127.0.0.1:33122 -> 127.0.0.1:9000 [AP] +...HELLO.............................................................................:....^...".....'.S...n......Y...................O.7.+.D.q".*..R...j.....8..qu..~......Ck.G\....:...m....Tg.s..M..x<.. +## +T 127.0.0.1:9000 -> 127.0.0.1:33122 [AP] +...WELCOME..%.'.,Td... I..}...........`..Nm......./_.Je...4.....-.....f<v.|.".jJ...^.D...$lJ..U......g..../w.......\..W.....!........i.v....0...........3..a.5}.@F..v./..$ +# +T 127.0.0.1:33122 -> 127.0.0.1:9000 [AP] +..........INITIATE......!.*.=0.-......D..]{...A\.tz...!2.....A./ +6.......Y.h.N....cb.U.|..f..)....W..3..X.2U.3PGl.........m..95.(......NJ....5.'..W.GQ..B/.....\%.,Q..r.'L5.......{.W<=._.$.(6j.G... +...37.H..Th...'.........0 ........,..q....U..G..M.`!_..w....f.".......... +.d.K.Y.>f.n.kV. +# +T 127.0.0.1:9000 -> 127.0.0.1:33122 [AP] +.2.READY............A...e.)......*.8y....k.<.N1Z.4.. +# +T 127.0.0.1:33122 -> 127.0.0.1:9000 [AP] +.+.MESSAGE........o...*M..,.... +.r..w..[.GwcU +### + diff --git a/src/leap/common/events/examples/client.py b/src/leap/common/events/examples/client.py new file mode 100644 index 0000000..d6d8985 --- /dev/null +++ b/src/leap/common/events/examples/client.py @@ -0,0 +1,2 @@ +from leap.common.events.txclient import emit +emit('stuff!') diff --git a/src/leap/common/events/examples/server.py b/src/leap/common/events/examples/server.py new file mode 100644 index 0000000..f40f8dc --- /dev/null +++ b/src/leap/common/events/examples/server.py @@ -0,0 +1,4 @@ +from twisted.internet import reactor +from leap.common.events.server import ensure_server +reactor.callWhenRunning(ensure_server) +reactor.run() diff --git a/src/leap/common/events/server.py b/src/leap/common/events/server.py index a69202e..05fc23e 100644 --- a/src/leap/common/events/server.py +++ b/src/leap/common/events/server.py @@ -14,33 +14,31 @@ # # 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 server for the events mechanism. """ - - import logging +import platform + import txzmq from leap.common.zmq_utils import zmq_has_curve - from leap.common.events.zmq_components import TxZmqServerComponent -if zmq_has_curve(): +if zmq_has_curve() or platform.system() == "Windows": + # Windows doesn't have ipc sockets, we need to use always tcp EMIT_ADDR = "tcp://127.0.0.1:9000" REG_ADDR = "tcp://127.0.0.1:9001" else: EMIT_ADDR = "ipc:///tmp/leap.common.events.socket.0" REG_ADDR = "ipc:///tmp/leap.common.events.socket.1" - logger = logging.getLogger(__name__) -def ensure_server(emit_addr=EMIT_ADDR, reg_addr=REG_ADDR): +def ensure_server(emit_addr=EMIT_ADDR, reg_addr=REG_ADDR, path_prefix=None, + factory=None, enable_curve=True): """ Make sure the server is running in the given addresses. @@ -52,7 +50,8 @@ def ensure_server(emit_addr=EMIT_ADDR, reg_addr=REG_ADDR): :return: an events server instance :rtype: EventsServer """ - _server = EventsServer(emit_addr, reg_addr) + _server = EventsServer(emit_addr, reg_addr, path_prefix, factory=factory, + enable_curve=enable_curve) return _server @@ -62,7 +61,8 @@ class EventsServer(TxZmqServerComponent): events in another address. """ - def __init__(self, emit_addr, reg_addr): + def __init__(self, emit_addr, reg_addr, path_prefix=None, factory=None, + enable_curve=True): """ Initialize the events server. @@ -71,7 +71,9 @@ class EventsServer(TxZmqServerComponent): :param reg_addr: The address to which publish events to clients. :type reg_addr: str """ - TxZmqServerComponent.__init__(self) + TxZmqServerComponent.__init__(self, path_prefix=path_prefix, + factory=factory, + enable_curve=enable_curve) # bind PULL and PUB sockets self._pull, self.pull_port = self._zmq_bind( txzmq.ZmqPullConnection, emit_addr) diff --git a/src/leap/common/events/tests/__init__.py b/src/leap/common/events/tests/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/src/leap/common/events/tests/__init__.py +++ /dev/null diff --git a/src/leap/common/events/tests/test_zmq_components.py b/src/leap/common/events/tests/test_zmq_components.py deleted file mode 100644 index c51e37e..0000000 --- a/src/leap/common/events/tests/test_zmq_components.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# test_zmq_components.py -# Copyright (C) 2014 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/>. -""" -Tests for the zmq_components module. -""" -try: - import unittest2 as unittest -except ImportError: - import unittest - -from leap.common.events import zmq_components - - -class AddrParseTestCase(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_addr_parsing(self): - addr_re = zmq_components.ADDRESS_RE - - self.assertEqual( - addr_re.search("ipc:///tmp/foo/bar/baaz-2/foo.0").groups(), - ("ipc", "/tmp/foo/bar/baaz-2/foo.0", None)) - self.assertEqual( - addr_re.search("tcp://localhost:9000").groups(), - ("tcp", "localhost", "9000")) - self.assertEqual( - addr_re.search("tcp://127.0.0.1:9000").groups(), - ("tcp", "127.0.0.1", "9000")) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/common/events/txclient.py b/src/leap/common/events/txclient.py index dfd0533..63f12d7 100644 --- a/src/leap/common/events/txclient.py +++ b/src/leap/common/events/txclient.py @@ -58,16 +58,19 @@ class EventsTxClient(TxZmqClientComponent, EventsClient): """ def __init__(self, emit_addr=EMIT_ADDR, reg_addr=REG_ADDR, - path_prefix=None): + path_prefix=None, factory=None, enable_curve=True): """ - Initialize the events server. + Initialize the events client. """ - TxZmqClientComponent.__init__(self, path_prefix=path_prefix) + TxZmqClientComponent.__init__( + self, path_prefix=path_prefix, factory=factory, + enable_curve=enable_curve) EventsClient.__init__(self, emit_addr, reg_addr) # connect SUB first, otherwise we might miss some event sent from this # same client self._sub = self._zmq_connect(txzmq.ZmqSubConnection, reg_addr) self._sub.gotMessage = self._gotMessage + self._push = self._zmq_connect(txzmq.ZmqPushConnection, emit_addr) def _gotMessage(self, msg, tag): @@ -122,7 +125,6 @@ class EventsTxClient(TxZmqClientComponent, EventsClient): callback(event, *content) def shutdown(self): - TxZmqClientComponent.shutdown(self) EventsClient.shutdown(self) diff --git a/src/leap/common/events/zmq_components.py b/src/leap/common/events/zmq_components.py index 51de02c..c533a74 100644 --- a/src/leap/common/events/zmq_components.py +++ b/src/leap/common/events/zmq_components.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # zmq.py -# Copyright (C) 2015 LEAP +# Copyright (C) 2015, 2016 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 @@ -14,60 +14,63 @@ # # 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 server for the events mechanism. """ - - import os import logging import txzmq import re -import time from abc import ABCMeta -# XXX some distros don't package libsodium, so we have to be prepared for -# absence of zmq.auth try: import zmq.auth - from zmq.auth.thread import ThreadAuthenticator + from leap.common.events.auth import TxAuthenticator + from leap.common.events.auth import TxAuthenticationRequest except ImportError: pass +from txzmq.connection import ZmqEndpoint, ZmqEndpointType + from leap.common.config import flags, get_path_prefix from leap.common.zmq_utils import zmq_has_curve from leap.common.zmq_utils import maybe_create_and_get_certificates from leap.common.zmq_utils import PUBLIC_KEYS_PREFIX - logger = logging.getLogger(__name__) - ADDRESS_RE = re.compile("^([a-z]+)://([^:]+):?(\d+)?$") +LOCALHOST_ALLOWED = '127.0.0.1' + class TxZmqComponent(object): """ A twisted-powered zmq events component. """ + _factory = txzmq.ZmqFactory() + _factory.registerForShutdown() + _auth = None __metaclass__ = ABCMeta _component_type = None - def __init__(self, path_prefix=None): + def __init__(self, path_prefix=None, enable_curve=True, factory=None): """ Initialize the txzmq component. """ - self._factory = txzmq.ZmqFactory() - self._factory.registerForShutdown() if path_prefix is None: path_prefix = get_path_prefix(flags.STANDALONE) + if factory is not None: + self._factory = factory self._config_prefix = os.path.join(path_prefix, "leap", "events") self._connections = [] + if enable_curve: + self.use_curve = zmq_has_curve() + else: + self.use_curve = False @property def component_type(self): @@ -77,105 +80,89 @@ class TxZmqComponent(object): "define a self._component_type!") return self._component_type - def _zmq_connect(self, connClass, address): + def _zmq_bind(self, connClass, address): """ - Connect to an address. + Bind to an address. :param connClass: The connection class to be used. :type connClass: txzmq.ZmqConnection - :param address: The address to connect to. + :param address: The address to bind to. :type address: str - :return: The binded connection. - :rtype: txzmq.ZmqConnection + :return: The binded connection and port. + :rtype: (txzmq.ZmqConnection, int) """ + proto, addr, port = ADDRESS_RE.search(address).groups() + + endpoint = ZmqEndpoint(ZmqEndpointType.bind, address) connection = connClass(self._factory) - # create and configure socket - socket = connection.socket - if zmq_has_curve(): + + if self.use_curve: + socket = connection.socket + public, secret = maybe_create_and_get_certificates( self._config_prefix, self.component_type) - server_public_file = os.path.join( - self._config_prefix, PUBLIC_KEYS_PREFIX, "server.key") - server_public, _ = zmq.auth.load_certificate(server_public_file) socket.curve_publickey = public socket.curve_secretkey = secret - socket.curve_serverkey = server_public - socket.connect(address) - logger.debug("Connected %s to %s." % (connClass, address)) - self._connections.append(connection) - return connection + self._start_authentication(connection.socket) - def _zmq_bind(self, connClass, address): + if proto == 'tcp' and int(port) == 0: + connection.endpoints.extend([endpoint]) + port = connection.socket.bind_to_random_port('tcp://%s' % addr) + else: + connection.addEndpoints([endpoint]) + + return connection, int(port) + + def _zmq_connect(self, connClass, address): """ - Bind to an address. + Connect to an address. :param connClass: The connection class to be used. :type connClass: txzmq.ZmqConnection - :param address: The address to bind to. + :param address: The address to connect to. :type address: str - :return: The binded connection and port. - :rtype: (txzmq.ZmqConnection, int) + :return: The binded connection. + :rtype: txzmq.ZmqConnection """ + endpoint = ZmqEndpoint(ZmqEndpointType.connect, address) connection = connClass(self._factory) - socket = connection.socket - if zmq_has_curve(): + + if self.use_curve: + socket = connection.socket public, secret = maybe_create_and_get_certificates( self._config_prefix, self.component_type) + server_public_file = os.path.join( + self._config_prefix, PUBLIC_KEYS_PREFIX, "server.key") + + server_public, _ = zmq.auth.load_certificate(server_public_file) socket.curve_publickey = public socket.curve_secretkey = secret - self._start_thread_auth(connection.socket) + socket.curve_serverkey = server_public - proto, addr, port = ADDRESS_RE.search(address).groups() + connection.addEndpoints([endpoint]) + return connection - if proto == "tcp": - if port is None or port is '0': - params = proto, addr - port = socket.bind_to_random_port("%s://%s" % params) - logger.debug("Binded %s to %s://%s." % ((connClass,) + params)) - else: - params = proto, addr, int(port) - socket.bind("%s://%s:%d" % params) - logger.debug( - "Binded %s to %s://%s:%d." % ((connClass,) + params)) - else: - params = proto, addr - socket.bind("%s://%s" % params) - logger.debug( - "Binded %s to %s://%s" % ((connClass,) + params)) - self._connections.append(connection) - return connection, port - - def _start_thread_auth(self, socket): - """ - Start the zmq curve thread authenticator. + def _start_authentication(self, socket): - :param socket: The socket in which to configure the authenticator. - :type socket: zmq.Socket - """ - authenticator = ThreadAuthenticator(self._factory.context) + if not TxZmqComponent._auth: + TxZmqComponent._auth = TxAuthenticator(self._factory) + TxZmqComponent._auth.start() - # Temporary fix until we understand what the problem is - # See https://leap.se/code/issues/7536 - time.sleep(0.5) + auth_req = TxAuthenticationRequest(self._factory) + auth_req.start() + auth_req.allow(LOCALHOST_ALLOWED) - authenticator.start() - # XXX do not hardcode this here. - authenticator.allow('127.0.0.1') # tell authenticator to use the certificate in a directory public_keys_dir = os.path.join(self._config_prefix, PUBLIC_KEYS_PREFIX) - authenticator.configure_curve(domain="*", location=public_keys_dir) - socket.curve_server = True # must come before bind + auth_req.configure_curve(domain="*", location=public_keys_dir) + auth_req.shutdown() + TxZmqComponent._auth.shutdown() - def shutdown(self): - """ - Shutdown the component. - """ - logger.debug("Shutting down component %s." % str(self)) - for conn in self._connections: - conn.shutdown() - self._factory.shutdown() + # This has to be set before binding the socket, that's why this method + # has to be called before addEndpoints() + socket.curve_server = True class TxZmqServerComponent(TxZmqComponent): diff --git a/src/leap/common/service_hooks.py b/src/leap/common/service_hooks.py new file mode 100644 index 0000000..96e95cc --- /dev/null +++ b/src/leap/common/service_hooks.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# service_hooks.py +# Copyright (C) 2016 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/>. +""" +Hooks for service composition. +""" +from collections import defaultdict + +from twisted.application.service import IService, Service +from twisted.python import log + +from zope.interface import implementer + + +@implementer(IService) +class HookableService(Service): + + """ + This service allows for other services in a Twisted Service tree to be + notified whenever a certain kind of hook is triggered. + + During the service composition, one is expected to register + a hook name with the name of the service that wants to react to the + triggering of the hook. All the services, both hooked and listeners, should + be registered against the same parent service. + + Upon the hook being triggered, the method "hook_<name>" will be called with + the passed data in the listener service. + """ + + def register_hook(self, name, listener): + if not hasattr(self, 'event_listeners'): + self.event_listeners = defaultdict(list) + log.msg("Registering hook %s->%s" % (name, listener)) + self.event_listeners[name].append(listener) + + def trigger_hook(self, name, **data): + + def react_to_hook(listener, name, **kw): + try: + getattr(listener, 'hook_' + name)(**kw) + except AttributeError: + raise RuntimeError( + "Tried to notify a hook, but the listener service class %s" + "has not defined the proper method" % listener.__class__) + + if not hasattr(self, 'event_listeners'): + self.event_listeners = defaultdict(list) + listeners = self._get_listener_services(name) + + for listener in listeners: + react_to_hook(listener, name, **data) + + def _get_sibling_service(self, name): + return self.parent.getServiceNamed(name) + + def _get_listener_services(self, hook): + if hook in self.event_listeners: + service_names = self.event_listeners[hook] + services = [ + self._get_sibling_service(name) for name in service_names] + return services diff --git a/src/leap/common/testing/basetest.py b/src/leap/common/testing/basetest.py index 3d3cee0..2e84a25 100644 --- a/src/leap/common/testing/basetest.py +++ b/src/leap/common/testing/basetest.py @@ -52,7 +52,7 @@ class BaseLeapTest(unittest.TestCase): cls.tearDownEnv() @classmethod - def setUpEnv(cls): + def setUpEnv(cls, launch_events_server=True): """ Sets up common facilities for testing this TestCase: - custom PATH and HOME environmental variables @@ -72,14 +72,15 @@ class BaseLeapTest(unittest.TestCase): os.environ["PATH"] = bin_tdir os.environ["HOME"] = cls.tempdir os.environ["XDG_CONFIG_HOME"] = os.path.join(cls.tempdir, ".config") - cls._init_events() + if launch_events_server: + cls._init_events() @classmethod def _init_events(cls): if flags.EVENTS_ENABLED: cls._server = events_server.ensure_server( - emit_addr="tcp://127.0.0.1:0", - reg_addr="tcp://127.0.0.1:0") + emit_addr="tcp://127.0.0.1", + reg_addr="tcp://127.0.0.1") events_client.configure_client( emit_addr="tcp://127.0.0.1:%d" % cls._server.pull_port, reg_addr="tcp://127.0.0.1:%d" % cls._server.pub_port) diff --git a/src/leap/common/testing/test_basetest.py b/src/leap/common/testing/test_basetest.py deleted file mode 100644 index ec42a62..0000000 --- a/src/leap/common/testing/test_basetest.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- 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): - self.setUpEnv() - - def tearDown(self): - self.tearDownEnv() - - 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() diff --git a/src/leap/common/tests/__init__.py b/src/leap/common/tests/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/src/leap/common/tests/__init__.py +++ /dev/null diff --git a/src/leap/common/tests/test_certs.py b/src/leap/common/tests/test_certs.py deleted file mode 100644 index 8ebc0f4..0000000 --- a/src/leap/common/tests/test_certs.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -# test_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/>. -""" -Tests for: - * leap/common/certs.py -""" -import os -import time - -try: - import unittest2 as unittest -except ImportError: - import unittest - -from leap.common import certs -from leap.common.testing.basetest import BaseLeapTest - -TEST_CERT_PEM = os.path.join( - os.path.split(__file__)[0], - '..', 'testing', "leaptest_combined_keycert.pem") - -# Values from the test cert file: -# Not Before: Sep 3 17:52:16 2013 GMT -# Not After : Sep 1 17:52:16 2023 GMT -CERT_NOT_BEFORE = (2013, 9, 3, 17, 52, 16, 1, 246, 0) -CERT_NOT_AFTER = (2023, 9, 1, 17, 52, 16, 4, 244, 0) - - -class CertsTest(BaseLeapTest): - - def setUp(self): - self.setUpEnv() - - def tearDown(self): - self.tearDownEnv() - - def test_should_redownload_if_no_cert(self): - self.assertTrue(certs.should_redownload(certfile="")) - - def test_should_redownload_if_invalid_pem(self): - cert_path = self.get_tempfile('test_pem_file.pem') - - with open(cert_path, 'w') as f: - f.write('this is some invalid data for the pem file') - - self.assertTrue(certs.should_redownload(cert_path)) - - def test_should_redownload_if_before(self): - def new_now(): - time.struct_time(CERT_NOT_BEFORE) - self.assertTrue(certs.should_redownload(TEST_CERT_PEM, now=new_now)) - - def test_should_redownload_if_after(self): - def new_now(): - time.struct_time(CERT_NOT_AFTER) - self.assertTrue(certs.should_redownload(TEST_CERT_PEM, now=new_now)) - - def test_not_should_redownload(self): - self.assertFalse(certs.should_redownload(TEST_CERT_PEM)) - - def test_is_valid_pemfile(self): - with open(TEST_CERT_PEM) as f: - cert_data = f.read() - - self.assertTrue(certs.is_valid_pemfile(cert_data)) - - def test_not_is_valid_pemfile(self): - cert_data = 'this is some invalid data for the pem file' - - self.assertFalse(certs.is_valid_pemfile(cert_data)) - - def test_get_cert_time_boundaries(self): - """ - This test ensures us that the returned values are returned in UTC/GMT. - """ - with open(TEST_CERT_PEM) as f: - cert_data = f.read() - - valid_from, valid_to = certs.get_cert_time_boundaries(cert_data) - self.assertEqual(tuple(valid_from), CERT_NOT_BEFORE) - self.assertEqual(tuple(valid_to), CERT_NOT_AFTER) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/common/tests/test_check.py b/src/leap/common/tests/test_check.py deleted file mode 100644 index cd488ff..0000000 --- a/src/leap/common/tests/test_check.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -# test_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/>. -""" -Tests for: - * leap/common/check.py -""" -try: - import unittest2 as unittest -except ImportError: - import unittest - -import mock - -from leap.common import check - - -class CheckTests(unittest.TestCase): - def test_raises_on_false_condition(self): - with self.assertRaises(AssertionError): - check.leap_assert(False, "Condition") - - def test_raises_on_none_condition(self): - with self.assertRaises(AssertionError): - check.leap_assert(None, "Condition") - - def test_suceeds_with_good_condition(self): - check.leap_assert(True, "") - - def test_raises_on_bad_type(self): - with self.assertRaises(AssertionError): - check.leap_assert_type(42, str) - - def test_succeeds_on_good_type(self): - check.leap_assert_type(42, int) - - @mock.patch("traceback.extract_stack", mock.MagicMock(return_value=None)) - def test_does_not_raise_on_bug(self): - with self.assertRaises(AssertionError): - check.leap_assert(False, "") diff --git a/src/leap/common/tests/test_events.py b/src/leap/common/tests/test_events.py deleted file mode 100644 index 2ad097e..0000000 --- a/src/leap/common/tests/test_events.py +++ /dev/null @@ -1,198 +0,0 @@ -# -*- coding: utf-8 -*- -# test_events.py -# Copyright (C) 2013 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import os -import logging -import time - -from twisted.internet.reactor import callFromThread -from twisted.trial import unittest -from twisted.internet import defer - -from leap.common.events import server -from leap.common.events import client -from leap.common.events import flags -from leap.common.events import txclient -from leap.common.events import catalog -from leap.common.events.errors import CallbackAlreadyRegisteredError - - -if 'DEBUG' in os.environ: - logging.basicConfig(level=logging.DEBUG) - - -class EventsGenericClientTestCase(object): - - def setUp(self): - flags.set_events_enabled(True) - self._server = server.ensure_server( - emit_addr="tcp://127.0.0.1:0", - reg_addr="tcp://127.0.0.1:0") - self._client.configure_client( - emit_addr="tcp://127.0.0.1:%d" % self._server.pull_port, - reg_addr="tcp://127.0.0.1:%d" % self._server.pub_port) - - def tearDown(self): - self._client.shutdown() - self._server.shutdown() - flags.set_events_enabled(False) - # wait a bit for sockets to close properly - time.sleep(0.1) - - def test_client_register(self): - """ - Ensure clients can register callbacks. - """ - callbacks = self._client.instance().callbacks - self.assertTrue(len(callbacks) == 0, - 'There should be no callback for this event.') - # register one event - event1 = catalog.CLIENT_UID - - def cbk1(event, _): - return True - - uid1 = self._client.register(event1, cbk1) - # assert for correct registration - self.assertTrue(len(callbacks) == 1) - self.assertTrue(callbacks[event1][uid1] == cbk1, - 'Could not register event in local client.') - # register another event - event2 = catalog.CLIENT_SESSION_ID - - def cbk2(event, _): - return True - - uid2 = self._client.register(event2, cbk2) - # assert for correct registration - self.assertTrue(len(callbacks) == 2) - self.assertTrue(callbacks[event2][uid2] == cbk2, - 'Could not register event in local client.') - - def test_register_signal_replace(self): - """ - Make sure clients can replace already registered callbacks. - """ - event = catalog.CLIENT_UID - d = defer.Deferred() - - def cbk_fail(event, _): - return callFromThread(d.errback, event) - - def cbk_succeed(event, _): - return callFromThread(d.callback, event) - - self._client.register(event, cbk_fail, uid=1) - self._client.register(event, cbk_succeed, uid=1, replace=True) - self._client.emit(event, None) - return d - - def test_register_signal_replace_fails_when_replace_is_false(self): - """ - Make sure clients trying to replace already registered callbacks fail - when replace=False - """ - event = catalog.CLIENT_UID - self._client.register(event, lambda event, _: None, uid=1) - self.assertRaises( - CallbackAlreadyRegisteredError, - self._client.register, - event, lambda event, _: None, uid=1, replace=False) - - def test_register_more_than_one_callback_works(self): - """ - Make sure clients can replace already registered callbacks. - """ - event = catalog.CLIENT_UID - d1 = defer.Deferred() - - def cbk1(event, _): - return callFromThread(d1.callback, event) - - d2 = defer.Deferred() - - def cbk2(event, _): - return d2.callback(event) - - self._client.register(event, cbk1) - self._client.register(event, cbk2) - self._client.emit(event, None) - d = defer.gatherResults([d1, d2]) - return d - - def test_client_receives_signal(self): - """ - Ensure clients can receive signals. - """ - event = catalog.CLIENT_UID - d = defer.Deferred() - - def cbk(events, _): - callFromThread(d.callback, event) - - self._client.register(event, cbk) - self._client.emit(event, None) - return d - - def test_client_unregister_all(self): - """ - Test that the client can unregister all events for one signal. - """ - event1 = catalog.CLIENT_UID - d = defer.Deferred() - # register more than one callback for the same event - self._client.register( - event1, lambda ev, _: callFromThread(d.errback, None)) - self._client.register( - event1, lambda ev, _: callFromThread(d.errback, None)) - # unregister and emit the event - self._client.unregister(event1) - self._client.emit(event1, None) - # register and emit another event so the deferred can succeed - event2 = catalog.CLIENT_SESSION_ID - self._client.register( - event2, lambda ev, _: callFromThread(d.callback, None)) - self._client.emit(event2, None) - return d - - def test_client_unregister_by_uid(self): - """ - Test that the client can unregister an event by uid. - """ - event = catalog.CLIENT_UID - d = defer.Deferred() - # register one callback that would fail - uid = self._client.register( - event, lambda ev, _: callFromThread(d.errback, None)) - # register one callback that will succeed - self._client.register( - event, lambda ev, _: callFromThread(d.callback, None)) - # unregister by uid and emit the event - self._client.unregister(event, uid=uid) - self._client.emit(event, None) - return d - - -class EventsTxClientTestCase(EventsGenericClientTestCase, unittest.TestCase): - - _client = txclient - - -class EventsClientTestCase(EventsGenericClientTestCase, unittest.TestCase): - - _client = client diff --git a/src/leap/common/tests/test_http.py b/src/leap/common/tests/test_http.py deleted file mode 100644 index f44550f..0000000 --- a/src/leap/common/tests/test_http.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -# test_http.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/>. -""" -Tests for: - * leap/common/http.py -""" -import os -try: - import unittest2 as unittest -except ImportError: - import unittest - -from leap.common import http -from leap.common.testing.basetest import BaseLeapTest - -TEST_CERT_PEM = os.path.join( - os.path.split(__file__)[0], - '..', 'testing', "leaptest_combined_keycert.pem") - - -class HTTPClientTest(BaseLeapTest): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_agents_sharing_pool_by_default(self): - client = http.HTTPClient() - client2 = http.HTTPClient(TEST_CERT_PEM) - self.assertNotEquals( - client._agent, client2._agent, "Expected dedicated agents") - self.assertEquals( - client._agent._pool, client2._agent._pool, - "Pool was not reused by default") - - def test_agent_can_have_dedicated_custom_pool(self): - custom_pool = http._HTTPConnectionPool( - None, - timeout=10, - maxPersistentPerHost=42, - persistent=False - ) - self.assertEquals(custom_pool.maxPersistentPerHost, 42, - "Custom persistent connections " - "limit is not being respected") - self.assertFalse(custom_pool.persistent, - "Custom persistence is not being respected") - default_client = http.HTTPClient() - custom_client = http.HTTPClient(pool=custom_pool) - - self.assertNotEquals( - default_client._agent, custom_client._agent, - "No agent reuse is expected") - self.assertEquals( - custom_pool, custom_client._agent._pool, - "Custom pool usage was not respected") - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/common/tests/test_memoize.py b/src/leap/common/tests/test_memoize.py deleted file mode 100644 index c923fc5..0000000 --- a/src/leap/common/tests/test_memoize.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -# test_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/>. -""" -Tests for: - * leap/common/decorators._memoized -""" -try: - import unittest2 as unittest -except ImportError: - import unittest - -from time import sleep - -import mock - -from leap.common.decorators import _memoized - - -class MemoizeTests(unittest.TestCase): - - def test_memoized_call(self): - """ - Test that a memoized function only calls once. - """ - witness = mock.Mock() - - @_memoized - def callmebaby(): - return witness() - - for i in range(10): - callmebaby() - witness.assert_called_once_with() - - def test_cache_invalidation(self): - """ - Test that time makes the cache invalidation expire. - """ - witness = mock.Mock() - - cache_with_alzheimer = _memoized - cache_with_alzheimer.CACHE_INVALIDATION_DELTA = 1 - - @cache_with_alzheimer - def callmebaby(*args): - return witness(*args) - - for i in range(10): - callmebaby() - witness.assert_called_once_with() - - sleep(2) - callmebaby("onemoretime") - - expected = [mock.call(), mock.call("onemoretime")] - self.assertEqual( - witness.call_args_list, - expected) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/leap/common/zmq_utils.py b/src/leap/common/zmq_utils.py index 0a781de..39a49c7 100644 --- a/src/leap/common/zmq_utils.py +++ b/src/leap/common/zmq_utils.py @@ -19,6 +19,7 @@ Utilities to handle ZMQ certificates. """ import os import logging +import platform import stat import shutil @@ -52,6 +53,10 @@ def zmq_has_curve(): `zmq.auth` module is new in version 14.1 `zmq.has()` is new in version 14.1, new in version libzmq-4.1. """ + if platform.system() == "Windows": + # TODO: curve is not working on windows #7919 + return False + zmq_version = zmq.zmq_version_info() pyzmq_version = zmq.pyzmq_version_info() |