From f4503842fdc288fb6336211099ad0a8d01165df3 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Fri, 4 Mar 2016 12:02:26 -0400 Subject: [feature] landing of bitmask.core --- src/leap/bitmask/core/__init__.py | 2 + src/leap/bitmask/core/_zmq.py | 68 ++++ src/leap/bitmask/core/bitmaskd.tac | 11 + src/leap/bitmask/core/configurable.py | 262 +++++++++++++ src/leap/bitmask/core/dispatcher.py | 196 ++++++++++ src/leap/bitmask/core/launcher.py | 37 ++ src/leap/bitmask/core/mail_services.py | 672 +++++++++++++++++++++++++++++++++ src/leap/bitmask/core/service.py | 174 +++++++++ src/leap/bitmask/core/uuid_map.py | 115 ++++++ src/leap/bitmask/core/web/__init__.py | 0 src/leap/bitmask/core/web/index.html | 70 ++++ src/leap/bitmask/core/web/root.py | 0 src/leap/bitmask/core/websocket.py | 98 +++++ 13 files changed, 1705 insertions(+) create mode 100644 src/leap/bitmask/core/__init__.py create mode 100644 src/leap/bitmask/core/_zmq.py create mode 100644 src/leap/bitmask/core/bitmaskd.tac create mode 100644 src/leap/bitmask/core/configurable.py create mode 100644 src/leap/bitmask/core/dispatcher.py create mode 100644 src/leap/bitmask/core/launcher.py create mode 100644 src/leap/bitmask/core/mail_services.py create mode 100644 src/leap/bitmask/core/service.py create mode 100644 src/leap/bitmask/core/uuid_map.py create mode 100644 src/leap/bitmask/core/web/__init__.py create mode 100644 src/leap/bitmask/core/web/index.html create mode 100644 src/leap/bitmask/core/web/root.py create mode 100644 src/leap/bitmask/core/websocket.py (limited to 'src/leap/bitmask/core') diff --git a/src/leap/bitmask/core/__init__.py b/src/leap/bitmask/core/__init__.py new file mode 100644 index 00000000..bda4b8d0 --- /dev/null +++ b/src/leap/bitmask/core/__init__.py @@ -0,0 +1,2 @@ +APPNAME = "bitmask.core" +ENDPOINT = "ipc:///tmp/%s.sock" % APPNAME diff --git a/src/leap/bitmask/core/_zmq.py b/src/leap/bitmask/core/_zmq.py new file mode 100644 index 00000000..a656fc65 --- /dev/null +++ b/src/leap/bitmask/core/_zmq.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# _zmq.py +# Copyright (C) 2015 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 . +""" +ZMQ REQ-REP Dispatcher. +""" + +from twisted.application import service +from twisted.internet import reactor +from twisted.python import log + +from txzmq import ZmqEndpoint, ZmqEndpointType +from txzmq import ZmqFactory, ZmqREPConnection + +from leap.bitmask.core import ENDPOINT +from leap.bitmask.core.dispatcher import CommandDispatcher + + +class ZMQServerService(service.Service): + + def __init__(self, core): + self._core = core + + def startService(self): + zf = ZmqFactory() + e = ZmqEndpoint(ZmqEndpointType.bind, ENDPOINT) + + self._conn = _DispatcherREPConnection(zf, e, self._core) + reactor.callWhenRunning(self._conn.do_greet) + service.Service.startService(self) + + def stopService(self): + service.Service.stopService(self) + + +class _DispatcherREPConnection(ZmqREPConnection): + + def __init__(self, zf, e, core): + ZmqREPConnection.__init__(self, zf, e) + self.dispatcher = CommandDispatcher(core) + + def gotMessage(self, msgId, *parts): + + r = self.dispatcher.dispatch(parts) + r.addCallback(self.defer_reply, msgId) + + def defer_reply(self, response, msgId): + reactor.callLater(0, self.reply, msgId, str(response)) + + def log_err(self, failure, msgId): + log.err(failure) + self.defer_reply("ERROR: %r" % failure, msgId) + + def do_greet(self): + log.msg('starting ZMQ dispatcher') diff --git a/src/leap/bitmask/core/bitmaskd.tac b/src/leap/bitmask/core/bitmaskd.tac new file mode 100644 index 00000000..3c9b1d8b --- /dev/null +++ b/src/leap/bitmask/core/bitmaskd.tac @@ -0,0 +1,11 @@ +# Service composition for bitmask-core. +# Run as: twistd -n -y bitmaskd.tac +# +from twisted.application import service + +from leap.bitmask.core.service import BitmaskBackend + + +bb = BitmaskBackend() +application = service.Application("bitmaskd") +bb.setServiceParent(application) diff --git a/src/leap/bitmask/core/configurable.py b/src/leap/bitmask/core/configurable.py new file mode 100644 index 00000000..3b97916d --- /dev/null +++ b/src/leap/bitmask/core/configurable.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +# configurable.py +# 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 +# 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 . +""" +Configurable Backend for Bitmask Service. +""" +import ConfigParser +import locale +import os +import re +import sys + +from twisted.application import service +from twisted.python import log + +from leap.common import files + + +class MissingConfigEntry(Exception): + """ + A required config entry was not found. + """ + + +class ConfigurableService(service.MultiService): + + config_file = u"bitmaskd.cfg" + service_names = ('mail', 'eip', 'zmq', 'web') + + def __init__(self, basedir='~/.config/leap'): + service.MultiService.__init__(self) + + path = os.path.abspath(os.path.expanduser(basedir)) + if not os.path.isdir(path): + files.mkdir_p(path) + self.basedir = path + + # creates self.config + self.read_config() + + def get_config(self, section, option, default=None, boolean=False): + try: + if boolean: + return self.config.getboolean(section, option) + + item = self.config.get(section, option) + return item + + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + if default is None: + fn = os.path.join(self.basedir, self.config_file) + raise MissingConfigEntry("%s is missing the [%s]%s entry" + % (_quote_output(fn), + section, option)) + return default + + def set_config(self, section, option, value): + if not self.config.has_section(section): + self.config.add_section(section) + self.config.set(section, option, value) + self.save_config() + self.read_config() + assert self.config.get(section, option) == value + + def read_config(self): + self.config = ConfigParser.SafeConfigParser() + bitmaskd_cfg = self._get_config_path() + + if not os.path.isfile(bitmaskd_cfg): + self._create_default_config(bitmaskd_cfg) + + try: + with open(bitmaskd_cfg, "rb") as f: + self.config.readfp(f) + except EnvironmentError: + if os.path.exists(bitmaskd_cfg): + raise + + def save_config(self): + bitmaskd_cfg = self._get_config_path() + with open(bitmaskd_cfg, 'wb') as f: + self.config.write(f) + + def _create_default_config(self, path): + with open(path, 'w') as outf: + outf.write(DEFAULT_CONFIG) + + def _get_config_path(self): + return os.path.join(self.basedir, self.config_file) + + +DEFAULT_CONFIG = """ +[services] +mail = True +eip = True +zmq = True +web = False +""" + + +def canonical_encoding(encoding): + if encoding is None: + log.msg("Warning: falling back to UTF-8 encoding.", level=log.WEIRD) + encoding = 'utf-8' + encoding = encoding.lower() + if encoding == "cp65001": + encoding = 'utf-8' + elif (encoding == "us-ascii" or encoding == "646" or encoding == + "ansi_x3.4-1968"): + encoding = 'ascii' + + return encoding + + +def check_encoding(encoding): + # sometimes Python returns an encoding name that it doesn't support for + # conversion fail early if this happens + try: + u"test".encode(encoding) + except (LookupError, AttributeError): + raise AssertionError( + "The character encoding '%s' is not supported for conversion." % ( + encoding,)) + +filesystem_encoding = None +io_encoding = None +is_unicode_platform = False + + +def _reload(): + global filesystem_encoding, io_encoding, is_unicode_platform + + filesystem_encoding = canonical_encoding(sys.getfilesystemencoding()) + check_encoding(filesystem_encoding) + + if sys.platform == 'win32': + # On Windows we install UTF-8 stream wrappers for sys.stdout and + # sys.stderr, and reencode the arguments as UTF-8 (see + # scripts/runner.py). + io_encoding = 'utf-8' + else: + ioenc = None + if hasattr(sys.stdout, 'encoding'): + ioenc = sys.stdout.encoding + if ioenc is None: + try: + ioenc = locale.getpreferredencoding() + except Exception: + pass # work around + io_encoding = canonical_encoding(ioenc) + + check_encoding(io_encoding) + + is_unicode_platform = sys.platform in ["win32", "darwin"] + +_reload() + + +def _quote_output(s, quotemarks=True, quote_newlines=None, encoding=None): + """ + Encode either a Unicode string or a UTF-8-encoded bytestring for + representation on stdout or stderr, tolerating errors. If 'quotemarks' is + True, the string is always quoted; otherwise, it is quoted only if + necessary to avoid ambiguity or control bytes in the output. (Newlines are + counted as control bytes iff quote_newlines is True.) + + Quoting may use either single or double quotes. Within single quotes, all + characters stand for themselves, and ' will not appear. Within double + quotes, Python-compatible backslash escaping is used. + + If not explicitly given, quote_newlines is True when quotemarks is True. + """ + assert isinstance(s, (str, unicode)) + if quote_newlines is None: + quote_newlines = quotemarks + + if isinstance(s, str): + try: + s = s.decode('utf-8') + except UnicodeDecodeError: + return 'b"%s"' % ( + ESCAPABLE_8BIT.sub( + lambda m: _str_escape(m, quote_newlines), s),) + + must_double_quote = (quote_newlines and MUST_DOUBLE_QUOTE_NL or + MUST_DOUBLE_QUOTE) + if must_double_quote.search(s) is None: + try: + out = s.encode(encoding or io_encoding) + if quotemarks or out.startswith('"'): + return "'%s'" % (out,) + else: + return out + except (UnicodeDecodeError, UnicodeEncodeError): + pass + + escaped = ESCAPABLE_UNICODE.sub( + lambda m: _unicode_escape(m, quote_newlines), s) + return '"%s"' % ( + escaped.encode(encoding or io_encoding, 'backslashreplace'),) + + +def _unicode_escape(m, quote_newlines): + u = m.group(0) + if u == u'"' or u == u'$' or u == u'`' or u == u'\\': + return u'\\' + u + elif u == u'\n' and not quote_newlines: + return u + if len(u) == 2: + codepoint = ( + ord(u[0]) - 0xD800) * 0x400 + ord(u[1]) - 0xDC00 + 0x10000 + else: + codepoint = ord(u) + if codepoint > 0xFFFF: + return u'\\U%08x' % (codepoint,) + elif codepoint > 0xFF: + return u'\\u%04x' % (codepoint,) + else: + return u'\\x%02x' % (codepoint,) + + +def _str_escape(m, quote_newlines): + c = m.group(0) + if c == '"' or c == '$' or c == '`' or c == '\\': + return '\\' + c + elif c == '\n' and not quote_newlines: + return c + else: + return '\\x%02x' % (ord(c),) + +MUST_DOUBLE_QUOTE_NL = re.compile( + ur'[^\x20-\x26\x28-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]', + re.DOTALL) +MUST_DOUBLE_QUOTE = re.compile( + ur'[^\n\x20-\x26\x28-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]', + re.DOTALL) + +ESCAPABLE_8BIT = re.compile( + r'[^ !#\x25-\x5B\x5D-\x5F\x61-\x7E]', + re.DOTALL) + +# if we must double-quote, then we have to escape ", $ and `, but need not +# escape ' + +ESCAPABLE_UNICODE = re.compile( + ur'([\uD800-\uDBFF][\uDC00-\uDFFF])|' # valid surrogate pairs + ur'[^ !#\x25-\x5B\x5D-\x5F\x61-\x7E\u00A0-\uD7FF' + ur'\uE000-\uFDCF\uFDF0-\uFFFC]', + re.DOTALL) diff --git a/src/leap/bitmask/core/dispatcher.py b/src/leap/bitmask/core/dispatcher.py new file mode 100644 index 00000000..4d7e1813 --- /dev/null +++ b/src/leap/bitmask/core/dispatcher.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +# dispatcher.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 . +""" +Command dispatcher. +""" +import json + +from twisted.internet import defer +from twisted.python import failure, log + + +# TODO implement sub-classes to dispatch subcommands (user, mail). + + +class CommandDispatcher(object): + + def __init__(self, core): + + self.core = core + + def _get_service(self, name): + + try: + return self.core.getServiceNamed(name) + except KeyError: + return None + + def dispatch(self, msg): + cmd = msg[0] + + _method = getattr(self, 'do_' + cmd.upper(), None) + + if not _method: + return defer.fail(failure.Failure(RuntimeError('No such command'))) + + return defer.maybeDeferred(_method, *msg) + + def do_STATS(self, *parts): + return _format_result(self.core.do_stats()) + + def do_VERSION(self, *parts): + return _format_result(self.core.do_version()) + + def do_STATUS(self, *parts): + return _format_result(self.core.do_status()) + + def do_SHUTDOWN(self, *parts): + return _format_result(self.core.do_shutdown()) + + def do_USER(self, *parts): + + subcmd = parts[1] + user, password = parts[2], parts[3] + + bf = self._get_service('bonafide') + + if subcmd == 'authenticate': + d = bf.do_authenticate(user, password) + + elif subcmd == 'signup': + d = bf.do_signup(user, password) + + elif subcmd == 'logout': + d = bf.do_logout(user, password) + + elif subcmd == 'active': + d = bf.do_get_active_user() + + d.addCallbacks(_format_result, _format_error) + return d + + def do_EIP(self, *parts): + subcmd = parts[1] + eip_label = 'eip' + + if subcmd == 'enable': + return _format_result( + self.core.do_enable_service(eip_label)) + + eip = self._get_service(eip_label) + if not eip: + return _format_result('eip: disabled') + + if subcmd == 'status': + return _format_result(eip.do_status()) + + elif subcmd == 'disable': + return _format_result( + self.core.do_disable_service(eip_label)) + + elif subcmd == 'start': + # TODO --- attempt to get active provider + # TODO or catch the exception and send error + provider = parts[2] + d = eip.do_start(provider) + d.addCallbacks(_format_result, _format_error) + return d + + elif subcmd == 'stop': + d = eip.do_stop() + d.addCallbacks(_format_result, _format_error) + return d + + def do_MAIL(self, *parts): + + subcmd = parts[1] + mail_label = 'mail' + + if subcmd == 'enable': + return _format_result( + self.core.do_enable_service(mail_label)) + + m = self._get_service(mail_label) + bf = self._get_service('bonafide') + + if not m: + return _format_result('mail: disabled') + + if subcmd == 'status': + return _format_result(m.do_status()) + + elif subcmd == 'disable': + return _format_result(self.core.do_disable_service(mail_label)) + + elif subcmd == 'get_imap_token': + d = m.get_imap_token() + d.addCallbacks(_format_result, _format_error) + return d + + elif subcmd == 'get_smtp_token': + d = m.get_smtp_token() + d.addCallbacks(_format_result, _format_error) + return d + + elif subcmd == 'get_smtp_certificate': + # TODO move to mail service + # TODO should ask for confirmation? like --force or something, + # if we already have a valid one. or better just refuse if cert + # exists. + # TODO how should we pass the userid?? + # - Keep an 'active' user in bonafide (last authenticated) + # (doing it now) + # - Get active user from Mail Service (maybe preferred?) + # - Have a command/method to set 'active' user. + + @defer.inlineCallbacks + def save_cert(cert_data): + userid, cert_str = cert_data + cert_path = yield m.do_get_smtp_cert_path(userid) + with open(cert_path, 'w') as outf: + outf.write(cert_str) + defer.returnValue('certificate saved to %s' % cert_path) + + d = bf.do_get_smtp_cert() + d.addCallback(save_cert) + d.addCallbacks(_format_result, _format_error) + return d + + def do_KEYS(self, *parts): + subcmd = parts[1] + + keymanager_label = 'keymanager' + km = self._get_service(keymanager_label) + bf = self._get_service('bonafide') + + if not km: + return _format_result('keymanager: disabled') + + if subcmd == 'list_keys': + d = bf.do_get_active_user() + d.addCallback(km.do_list_keys) + d.addCallbacks(_format_result, _format_error) + return d + + +def _format_result(result): + return json.dumps({'error': None, 'result': result}) + + +def _format_error(failure): + log.err(failure) + return json.dumps({'error': failure.value.message, 'result': None}) diff --git a/src/leap/bitmask/core/launcher.py b/src/leap/bitmask/core/launcher.py new file mode 100644 index 00000000..7d658017 --- /dev/null +++ b/src/leap/bitmask/core/launcher.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# launcher.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 . +""" +Run bitmask daemon. +""" +from twisted.scripts.twistd import run +from os.path import join, dirname +from sys import argv + +from leap.bitmask import core + + +def run_bitmaskd(): + # TODO --- configure where to put the logs... (get --logfile, --logdir + # from the bitmask_cli + argv[1:] = [ + '-y', join(dirname(core.__file__), "bitmaskd.tac"), + '--pidfile', '/tmp/bitmaskd.pid', + '--logfile', '/tmp/bitmaskd.log', + '--umask=0022', + ] + print '[+] launching bitmaskd...' + run() diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py new file mode 100644 index 00000000..9858d498 --- /dev/null +++ b/src/leap/bitmask/core/mail_services.py @@ -0,0 +1,672 @@ +""" +Mail services. + +This is quite moving work still. +This should be moved to the different packages when it stabilizes. +""" +import json +import os +from glob import glob +from collections import defaultdict +from collections import namedtuple + +from twisted.application import service +from twisted.internet import defer +from twisted.python import log + +from leap.bonafide import config +from leap.common.service_hooks import HookableService +from leap.keymanager import KeyManager, openpgp +from leap.keymanager.errors import KeyNotFound +from leap.soledad.client.api import Soledad +from leap.mail.constants import INBOX_NAME +from leap.mail.mail import Account +from leap.mail.imap.service import imap +from leap.mail.incoming.service import IncomingMail, INCOMING_CHECK_PERIOD +from leap.mail import smtp + +from leap.bitmask.core.uuid_map import UserMap + + +class Container(object): + + def __init__(self): + self._instances = defaultdict(None) + + def get_instance(self, key): + return self._instances.get(key, None) + + +class ImproperlyConfigured(Exception): + pass + + +def get_all_soledad_uuids(): + return [os.path.split(p)[-1].split('.db')[0] for p in + glob(os.path.expanduser('~/.config/leap/soledad/*.db'))] + # FIXME do not hardcode basedir + + +class SoledadContainer(Container): + + def __init__(self, basedir='~/.config/leap'): + # FIXME do not hardcode basedir + self._basedir = os.path.expanduser(basedir) + self._usermap = UserMap() + super(SoledadContainer, self).__init__() + + def add_instance(self, userid, passphrase, uuid=None, token=None): + + if not uuid: + bootstrapped_uuid = self._usermap.lookup_uuid(userid, passphrase) + uuid = bootstrapped_uuid + if not uuid: + return + else: + self._usermap.add(userid, uuid, passphrase) + + user, provider = userid.split('@') + + soledad_path = os.path.join(self._basedir, 'soledad') + soledad_url = _get_soledad_uri(self._basedir, provider) + cert_path = _get_ca_cert_path(self._basedir, provider) + + soledad = self._create_soledad_instance( + uuid, passphrase, soledad_path, soledad_url, + cert_path, token) + + self._instances[userid] = soledad + + data = {'user': userid, 'uuid': uuid, 'token': token, + 'soledad': soledad} + self.service.trigger_hook('on_new_soledad_instance', **data) + + def _create_soledad_instance(self, uuid, passphrase, basedir, server_url, + cert_file, token): + # setup soledad info + secrets_path = os.path.join( + basedir, '%s.secret' % uuid) + local_db_path = os.path.join( + basedir, '%s.db' % uuid) + + if token is None: + syncable = False + token = '' + else: + syncable = True + + return Soledad( + uuid, + unicode(passphrase), + secrets_path=secrets_path, + local_db_path=local_db_path, + server_url=server_url, + cert_file=cert_file, + auth_token=token, + defer_encryption=True, + syncable=syncable) + + def set_remote_auth_token(self, userid, token): + self.get_instance(userid).token = token + + def set_syncable(self, userid, state): + # TODO should check that there's a token! + self.get_instance(userid).set_syncable(bool(state)) + + def sync(self, userid): + self.get_instance(userid).sync() + + +def _get_provider_from_full_userid(userid): + _, provider_id = config.get_username_and_provider(userid) + return config.Provider(provider_id) + + +def is_service_ready(service, provider): + """ + Returns True when the following conditions are met: + - Provider offers that service. + - We have the config files for the service. + - The service is enabled. + """ + has_service = provider.offers_service(service) + has_config = provider.has_config_for_service(service) + is_enabled = provider.is_service_enabled(service) + return has_service and has_config and is_enabled + + +class SoledadService(service.Service, HookableService): + + subscribed_to_hooks = ('on_bonafide_auth', 'on_passphrase_entry') + + def __init__(self, basedir): + service.Service.__init__(self) + self._basedir = basedir + + def startService(self): + log.msg('Starting Soledad Service') + self._container = SoledadContainer() + self._container.service = self + super(SoledadService, self).startService() + + # hooks + + def hook_on_passphrase_entry(self, **kw): + userid = kw.get('username') + provider = _get_provider_from_full_userid(userid) + provider.callWhenReady(self._hook_on_passphrase_entry, provider, **kw) + + def _hook_on_passphrase_entry(self, provider, **kw): + if is_service_ready('mx', provider): + userid = kw.get('username') + password = kw.get('password') + uuid = kw.get('uuid') + container = self._container + log.msg("on_passphrase_entry: New Soledad Instance: %s" % userid) + if not container.get_instance(userid): + container.add_instance(userid, password, uuid=uuid, token=None) + else: + log.msg('Service MX is not ready...') + + def hook_on_bonafide_auth(self, **kw): + userid = kw['username'] + provider = _get_provider_from_full_userid(userid) + provider.callWhenReady(self._hook_on_bonafide_auth, provider, **kw) + + def _hook_on_bonafide_auth(self, provider, **kw): + if provider.offers_service('mx'): + userid = kw['username'] + password = kw['password'] + token = kw['token'] + uuid = kw['uuid'] + + container = self._container + if container.get_instance(userid): + log.msg("Passing a new SRP Token to Soledad: %s" % userid) + container.set_remote_auth_token(userid, token) + container.set_syncable(userid, True) + else: + log.msg("Adding a new Soledad Instance: %s" % userid) + container.add_instance( + userid, password, uuid=uuid, token=token) + + +class KeymanagerContainer(Container): + + def __init__(self, basedir): + self._basedir = os.path.expanduser(basedir) + super(KeymanagerContainer, self).__init__() + + def add_instance(self, userid, token, uuid, soledad): + + keymanager = self._create_keymanager_instance( + userid, token, uuid, soledad) + + d = self._get_or_generate_keys(keymanager, userid) + d.addCallback(self._on_keymanager_ready_cb, userid, soledad) + return d + + def set_remote_auth_token(self, userid, token): + self.get_instance(userid)._token = token + + def _on_keymanager_ready_cb(self, keymanager, userid, soledad): + # TODO use onready-deferreds instead + self._instances[userid] = keymanager + + log.msg("Adding Keymanager instance for: %s" % userid) + data = {'userid': userid, 'soledad': soledad, 'keymanager': keymanager} + self.service.trigger_hook('on_new_keymanager_instance', **data) + + def _get_or_generate_keys(self, keymanager, userid): + + def if_not_found_generate(failure): + # TODO -------------- should ONLY generate if INITIAL_SYNC_DONE. + # ie: put callback on_soledad_first_sync_ready ----------------- + # -------------------------------------------------------------- + failure.trap(KeyNotFound) + log.msg("Core: Key not found. Generating key for %s" % (userid,)) + d = keymanager.gen_key(openpgp.OpenPGPKey) + d.addCallbacks(send_key, log_key_error("generating")) + return d + + def send_key(ignored): + # ---------------------------------------------------------------- + # It might be the case that we have generated a key-pair + # but this hasn't been successfully uploaded. How do we know that? + # XXX Should this be a method of bonafide instead? + # ----------------------------------------------------------------- + d = keymanager.send_key(openpgp.OpenPGPKey) + d.addCallbacks( + lambda _: log.msg( + "Key generated successfully for %s" % userid), + log_key_error("sending")) + return d + + def log_key_error(step): + def log_error(failure): + log.err("Error while %s key!" % step) + log.err(failure) + return failure + return log_error + + d = keymanager.get_key( + userid, openpgp.OpenPGPKey, private=True, fetch_remote=False) + d.addErrback(if_not_found_generate) + d.addCallback(lambda _: keymanager) + return d + + def _create_keymanager_instance(self, userid, token, uuid, soledad): + user, provider = userid.split('@') + nickserver_uri = self._get_nicknym_uri(provider) + + cert_path = _get_ca_cert_path(self._basedir, provider) + api_uri = self._get_api_uri(provider) + + if not token: + token = self.service.tokens.get(userid) + + km_args = (userid, nickserver_uri, soledad) + km_kwargs = { + "token": token, "uid": uuid, + "api_uri": api_uri, "api_version": "1", + "ca_cert_path": cert_path, + "gpgbinary": "/usr/bin/gpg" + } + keymanager = KeyManager(*km_args, **km_kwargs) + return keymanager + + def _get_api_uri(self, provider): + # TODO get this from service.json (use bonafide service) + api_uri = "https://api.{provider}:4430".format( + provider=provider) + return api_uri + + def _get_nicknym_uri(self, provider): + return 'https://nicknym.{provider}:6425'.format( + provider=provider) + + +class KeymanagerService(service.Service, HookableService): + + subscribed_to_hooks = ('on_new_soledad_instance', 'on_bonafide_auth') + + def __init__(self, basedir='~/.config/leap'): + service.Service.__init__(self) + self._basedir = basedir + + def startService(self): + log.msg('Starting Keymanager Service') + self._container = KeymanagerContainer(self._basedir) + self._container.service = self + self.tokens = {} + super(KeymanagerService, self).startService() + + # hooks + + def hook_on_new_soledad_instance(self, **kw): + container = self._container + user = kw['user'] + token = kw['token'] + uuid = kw['uuid'] + soledad = kw['soledad'] + if not container.get_instance(user): + log.msg('Adding a new Keymanager instance for %s' % user) + if not token: + token = self.tokens.get(user) + container.add_instance(user, token, uuid, soledad) + + def hook_on_bonafide_auth(self, **kw): + userid = kw['username'] + provider = _get_provider_from_full_userid(userid) + provider.callWhenReady(self._hook_on_bonafide_auth, provider, **kw) + + def _hook_on_bonafide_auth(self, provider, **kw): + if provider.offers_service('mx'): + userid = kw['username'] + token = kw['token'] + + container = self._container + if container.get_instance(userid): + log.msg('Passing a new SRP Token to Keymanager: %s' % userid) + container.set_remote_auth_token(userid, token) + else: + log.msg('storing the keymanager token...') + self.tokens[userid] = token + + # commands + + def do_list_keys(self, userid): + km = self._container.get_instance(userid) + d = km.get_all_keys() + d.addCallback( + lambda keys: [ + (key.uids, key.fingerprint) for key in keys]) + return d + + +class StandardMailService(service.MultiService, HookableService): + """ + A collection of Services. + + This is the parent service, that launches 3 different services that expose + Encrypted Mail Capabilities on specific ports: + + - SMTP service, on port 2013 + - IMAP service, on port 1984 + - The IncomingMail Service, which doesn't listen on any port, but + watches and processes the Incoming Queue and saves the processed mail + into the matching INBOX. + """ + + name = 'mail' + + # TODO factor out Mail Service to inside mail package. + + subscribed_to_hooks = ('on_new_keymanager_instance',) + + def __init__(self, basedir): + self._basedir = basedir + self._soledad_sessions = {} + self._keymanager_sessions = {} + self._sendmail_opts = {} + self._imap_tokens = {} + self._smtp_tokens = {} + self._active_user = None + super(StandardMailService, self).__init__() + self.initializeChildrenServices() + + def initializeChildrenServices(self): + self.addService(IMAPService(self._soledad_sessions)) + self.addService(IncomingMailService(self)) + self.addService(SMTPService( + self._soledad_sessions, self._keymanager_sessions, + self._sendmail_opts)) + + def startService(self): + log.msg('Starting Mail Service...') + super(StandardMailService, self).startService() + + def stopService(self): + super(StandardMailService, self).stopService() + + def startInstance(self, userid, soledad, keymanager): + username, provider = userid.split('@') + + self._soledad_sessions[userid] = soledad + self._keymanager_sessions[userid] = keymanager + + sendmail_opts = _get_sendmail_opts(self._basedir, provider, username) + self._sendmail_opts[userid] = sendmail_opts + + incoming = self.getServiceNamed('incoming_mail') + incoming.startInstance(userid) + + def registerIMAPToken(token): + self._imap_tokens[userid] = token + self._active_user = userid + return token + + def registerSMTPToken(token): + self._smtp_tokens[userid] = token + return token + + d = soledad.get_or_create_service_token('imap') + d.addCallback(registerIMAPToken) + d.addCallback( + lambda _: soledad.get_or_create_service_token('smtp')) + d.addCallback(registerSMTPToken) + return d + + def stopInstance(self): + pass + + # hooks + + def hook_on_new_keymanager_instance(self, **kw): + # XXX we can specify this as a waterfall, or just AND the two + # conditions. + userid = kw['userid'] + soledad = kw['soledad'] + keymanager = kw['keymanager'] + + # TODO --- only start instance if "autostart" is True. + self.startInstance(userid, soledad, keymanager) + + # commands + + def do_status(self): + return 'mail: %s' % 'running' if self.running else 'disabled' + + def get_imap_token(self): + active_user = self._active_user + if not active_user: + return defer.succeed('NO ACTIVE USER') + token = self._imap_tokens.get(active_user) + return defer.succeed("IMAP TOKEN (%s): %s" % (active_user, token)) + + def get_smtp_token(self): + active_user = self._active_user + if not active_user: + return defer.succeed('NO ACTIVE USER') + token = self._smtp_tokens.get(active_user) + return defer.succeed("SMTP TOKEN (%s): %s" % (active_user, token)) + + def do_get_smtp_cert_path(self, userid): + username, provider = userid.split('@') + return _get_smtp_client_cert_path(self._basedir, provider, username) + + # access to containers + + def get_soledad_session(self, userid): + return self._soledad_sessions.get(userid) + + def get_keymanager_session(self, userid): + return self._keymanager_sessions.get(userid) + + +class IMAPService(service.Service): + + name = 'imap' + + def __init__(self, soledad_sessions): + port, factory = imap.run_service(soledad_sessions) + + self._port = port + self._factory = factory + self._soledad_sessions = soledad_sessions + super(IMAPService, self).__init__() + + def startService(self): + log.msg('Starting IMAP Service') + super(IMAPService, self).startService() + + def stopService(self): + self._port.stopListening() + self._factory.doStop() + super(IMAPService, self).stopService() + + +class SMTPService(service.Service): + + name = 'smtp' + + def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts, + basedir='~/.config/leap'): + + self._basedir = os.path.expanduser(basedir) + port, factory = smtp.run_service( + soledad_sessions, keymanager_sessions, sendmail_opts) + self._port = port + self._factory = factory + self._soledad_sessions = soledad_sessions + self._keymanager_sessions = keymanager_sessions + self._sendmail_opts = sendmail_opts + super(SMTPService, self).__init__() + + def startService(self): + log.msg('Starting SMTP Service') + super(SMTPService, self).startService() + + def stopService(self): + # TODO cleanup all instances + super(SMTPService, self).stopService() + + +class IncomingMailService(service.Service): + + name = 'incoming_mail' + + def __init__(self, mail_service): + super(IncomingMailService, self).__init__() + self._mail = mail_service + self._instances = {} + + def startService(self): + log.msg('Starting IncomingMail Service') + super(IncomingMailService, self).startService() + + def stopService(self): + super(IncomingMailService, self).stopService() + + # Individual accounts + + # TODO IncomingMail *IS* already a service. + # I think we should better model the current Service + # as a startInstance inside a container, and get this + # multi-tenant service inside the leap.mail.incoming.service. + # ... or just simply make it a multiService and set per-user + # instances as Child of this parent. + + def startInstance(self, userid): + soledad = self._mail.get_soledad_session(userid) + keymanager = self._mail.get_keymanager_session(userid) + + log.msg('Starting Incoming Mail instance for %s' % userid) + self._start_incoming_mail_instance( + keymanager, soledad, userid) + + def stopInstance(self, userid): + # TODO toggle offline! + pass + + def _start_incoming_mail_instance(self, keymanager, soledad, + userid, start_sync=True): + + def setUpIncomingMail(inbox): + incoming_mail = IncomingMail( + keymanager, soledad, + inbox, userid, + check_period=INCOMING_CHECK_PERIOD) + return incoming_mail + + def registerInstance(incoming_instance): + self._instances[userid] = incoming_instance + if start_sync: + incoming_instance.startService() + + acc = Account(soledad) + d = acc.callWhenReady( + lambda _: acc.get_collection_by_mailbox(INBOX_NAME)) + d.addCallback(setUpIncomingMail) + d.addCallback(registerInstance) + d.addErrback(log.err) + return d + +# -------------------------------------------------------------------- +# +# config utilities. should be moved to bonafide +# + +SERVICES = ('soledad', 'smtp', 'eip') + + +Provider = namedtuple( + 'Provider', ['hostname', 'ip_address', 'location', 'port']) + +SendmailOpts = namedtuple( + 'SendmailOpts', ['cert', 'key', 'hostname', 'port']) + + +def _get_ca_cert_path(basedir, provider): + path = os.path.join( + basedir, 'providers', provider, 'keys', 'ca', 'cacert.pem') + return path + + +def _get_sendmail_opts(basedir, provider, username): + cert = _get_smtp_client_cert_path(basedir, provider, username) + key = cert + prov = _get_provider_for_service('smtp', basedir, provider) + hostname = prov.hostname + port = prov.port + opts = SendmailOpts(cert, key, hostname, port) + return opts + + +def _get_smtp_client_cert_path(basedir, provider, username): + path = os.path.join( + basedir, 'providers', provider, 'keys', 'client', 'stmp_%s.pem' % + username) + return path + + +def _get_config_for_service(service, basedir, provider): + if service not in SERVICES: + raise ImproperlyConfigured('Tried to use an unknown service') + + config_path = os.path.join( + basedir, 'providers', provider, '%s-service.json' % service) + try: + with open(config_path) as config: + config = json.loads(config.read()) + except IOError: + # FIXME might be that the provider DOES NOT offer this service! + raise ImproperlyConfigured( + 'could not open config file %s' % config_path) + else: + return config + + +def first(xs): + return xs[0] + + +def _pick_server(config, strategy=first): + """ + Picks a server from a list of possible choices. + The service files have a . + This implementation just picks the FIRST available server. + """ + servers = config['hosts'].keys() + choice = config['hosts'][strategy(servers)] + return choice + + +def _get_subdict(d, keys): + return {key: d.get(key) for key in keys} + + +def _get_provider_for_service(service, basedir, provider): + + if service not in SERVICES: + raise ImproperlyConfigured('Tried to use an unknown service') + + config = _get_config_for_service(service, basedir, provider) + p = _pick_server(config) + attrs = _get_subdict(p, ('hostname', 'ip_address', 'location', 'port')) + provider = Provider(**attrs) + return provider + + +def _get_smtp_uri(basedir, provider): + prov = _get_provider_for_service('smtp', basedir, provider) + url = 'https://{hostname}:{port}'.format( + hostname=prov.hostname, port=prov.port) + return url + + +def _get_soledad_uri(basedir, provider): + prov = _get_provider_for_service('soledad', basedir, provider) + url = 'https://{hostname}:{port}'.format( + hostname=prov.hostname, port=prov.port) + return url diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py new file mode 100644 index 00000000..4c18ab5d --- /dev/null +++ b/src/leap/bitmask/core/service.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# service.py +# Copyright (C) 2015 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 . +""" +Bitmask-core Service. +""" +import resource + +from twisted.internet import reactor +from twisted.python import log + +from leap.bonafide.service import BonafideService + +from leap.bitmask.core import configurable +from leap.bitmask.core import mail_services +from leap.bitmask.core import _zmq +from leap.bitmask.core import websocket +from leap.bitmask.core._version import get_versions + +from leap.common.events import server as event_server +from leap.vpn import EIPService + + +class BitmaskBackend(configurable.ConfigurableService): + + def __init__(self, basedir='~/.config/leap'): + + configurable.ConfigurableService.__init__(self, basedir) + + def enabled(service): + return self.get_config('services', service, False, boolean=True) + + on_start = reactor.callWhenRunning + + on_start(self.init_events) + on_start(self.init_bonafide) + + if enabled('mail'): + on_start(self.init_soledad) + on_start(self.init_keymanager) + on_start(self.init_mail) + + if enabled('eip'): + on_start(self.init_eip) + + if enabled('zmq'): + on_start(self.init_zmq) + + if enabled('web'): + on_start(self.init_web) + + def init_events(self): + event_server.ensure_server() + + def init_bonafide(self): + bf = BonafideService(self.basedir) + bf.setName("bonafide") + bf.setServiceParent(self) + # TODO ---- these hooks should be activated only if + # (1) we have enabled that service + # (2) provider offers this service + bf.register_hook('on_passphrase_entry', listener='soledad') + bf.register_hook('on_bonafide_auth', listener='soledad') + bf.register_hook('on_bonafide_auth', listener='keymanager') + + def init_soledad(self): + service = mail_services.SoledadService + sol = self._maybe_start_service( + 'soledad', service, self.basedir) + if sol: + sol.register_hook( + 'on_new_soledad_instance', listener='keymanager') + + def init_keymanager(self): + service = mail_services.KeymanagerService + km = self._maybe_start_service( + 'keymanager', service, self.basedir) + if km: + km.register_hook('on_new_keymanager_instance', listener='mail') + + def init_mail(self): + service = mail_services.StandardMailService + self._maybe_start_service('mail', service, self.basedir) + + def init_eip(self): + self._maybe_start_service('eip', EIPService) + + def init_zmq(self): + zs = _zmq.ZMQServerService(self) + zs.setServiceParent(self) + + def init_web(self): + ws = websocket.WebSocketsDispatcherService(self) + ws.setServiceParent(self) + + def _maybe_start_service(self, label, klass, *args, **kw): + try: + self.getServiceNamed(label) + except KeyError: + service = klass(*args, **kw) + service.setName(label) + service.setServiceParent(self) + return service + + # General commands for the BitmaskBackend Core Service + + def do_stats(self): + log.msg('BitmaskCore Service STATS') + mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + return 'BitmaskCore: [Mem usage: %s KB]' % (mem / 1024) + + def do_status(self): + # we may want to make this tuple a class member + services = ('soledad', 'keymanager', 'mail', 'eip') + + status_messages = [] + for name in services: + status = 'stopped' + try: + if self.getServiceNamed(name).running: + status = "running" + except KeyError: + pass + status_messages.append("[{}: {}]".format(name, status)) + + return " ".join(status_messages) + + def do_version(self): + version = get_versions()['version'] + return 'BitmaskCore: %s' % version + + def do_shutdown(self): + self.stopService() + reactor.callLater(1, reactor.stop) + return 'shutting down...' + + def do_enable_service(self, service): + assert service in self.service_names + self.set_config('services', service, 'True') + + if service == 'mail': + self.init_soledad() + self.init_keymanager() + self.init_mail() + + elif service == 'eip': + self.init_eip() + + elif service == 'zmq': + self.init_zmq() + + elif service == 'web': + self.init_web() + + return 'ok' + + def do_disable_service(self, service): + assert service in self.service_names + # TODO -- should stop also? + self.set_config('services', service, 'False') + return 'ok' diff --git a/src/leap/bitmask/core/uuid_map.py b/src/leap/bitmask/core/uuid_map.py new file mode 100644 index 00000000..5edc7216 --- /dev/null +++ b/src/leap/bitmask/core/uuid_map.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# uuid_map.py +# 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 +# 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 . +""" +UUID Map: a persistent mapping between user-ids and uuids. +""" + +import base64 +import os +import re + +import scrypt + +from leap.common.config import get_path_prefix + + +MAP_PATH = os.path.join(get_path_prefix(), 'leap', 'uuids') + + +class UserMap(object): + + """ + A persistent mapping between user-ids and uuids. + """ + + # TODO Add padding to the encrypted string + + def __init__(self): + self._d = {} + self._lines = set([]) + if os.path.isfile(MAP_PATH): + self.load() + + def add(self, userid, uuid, passwd): + """ + Add a new userid-uuid mapping, and encrypt the record with the user + password. + """ + self._add_to_cache(userid, uuid) + self._lines.add(_encode_uuid_map(userid, uuid, passwd)) + self.dump() + + def _add_to_cache(self, userid, uuid): + self._d[userid] = uuid + + def load(self): + """ + Load a mapping from a default file. + """ + with open(MAP_PATH, 'r') as infile: + lines = infile.readlines() + self._lines = set(lines) + + def dump(self): + """ + Dump the mapping to a default file. + """ + with open(MAP_PATH, 'w') as out: + out.write('\n'.join(self._lines)) + + def lookup_uuid(self, userid, passwd=None): + """ + Lookup the uuid for a given userid. + + If no password is given, try to lookup on cache. + Else, try to decrypt all the records that we know about with the + passed password. + """ + if not passwd: + return self._d.get(userid) + + for line in self._lines: + guess = _decode_uuid_line(line, passwd) + if guess: + record_userid, uuid = guess + if record_userid == userid: + self._add_to_cache(userid, uuid) + return uuid + + def lookup_userid(self, uuid): + """ + Get the userid for the given uuid from cache. + """ + rev_d = {v: k for (k, v) in self._d.items()} + return rev_d.get(uuid) + + +def _encode_uuid_map(userid, uuid, passwd): + data = 'userid:%s:uuid:%s' % (userid, uuid) + encrypted = scrypt.encrypt(data, passwd, maxtime=0.05) + return base64.encodestring(encrypted).replace('\n', '') + + +def _decode_uuid_line(line, passwd): + decoded = base64.decodestring(line) + try: + maybe_decrypted = scrypt.decrypt(decoded, passwd, maxtime=0.1) + except scrypt.error: + return None + match = re.findall("userid\:(.+)\:uuid\:(.+)", maybe_decrypted) + if match: + return match[0] diff --git a/src/leap/bitmask/core/web/__init__.py b/src/leap/bitmask/core/web/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/leap/bitmask/core/web/index.html b/src/leap/bitmask/core/web/index.html new file mode 100644 index 00000000..9490eca8 --- /dev/null +++ b/src/leap/bitmask/core/web/index.html @@ -0,0 +1,70 @@ + + + + Bitmask WebSockets Endpoint + + + +

Bitmask Control Panel

+ +
+

Command:

+
+ +

+   
+
diff --git a/src/leap/bitmask/core/web/root.py b/src/leap/bitmask/core/web/root.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/leap/bitmask/core/websocket.py b/src/leap/bitmask/core/websocket.py
new file mode 100644
index 00000000..5569c6c7
--- /dev/null
+++ b/src/leap/bitmask/core/websocket.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# websocket.py
+# 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
+# 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 .
+
+"""
+WebSockets Dispatcher Service.
+"""
+
+import os
+import pkg_resources
+
+from twisted.internet import reactor
+from twisted.application import service
+
+from twisted.web.server import Site
+from twisted.web.static import File
+
+from autobahn.twisted.resource import WebSocketResource
+from autobahn.twisted.websocket import WebSocketServerFactory
+from autobahn.twisted.websocket import WebSocketServerProtocol
+
+from leap.bitmask.core.dispatcher import CommandDispatcher
+
+
+class WebSocketsDispatcherService(service.Service):
+
+    """
+    A Dispatcher for BitmaskCore exposing a WebSockets Endpoint.
+    """
+
+    def __init__(self, core, port=8080, debug=False):
+        self._core = core
+        self.port = port
+        self.debug = debug
+
+    def startService(self):
+
+        factory = WebSocketServerFactory(u"ws://127.0.0.1:%d" % self.port,
+                                         debug=self.debug)
+        factory.protocol = DispatcherProtocol
+        factory.protocol.dispatcher = CommandDispatcher(self._core)
+
+        # FIXME: Site.start/stopFactory should start/stop factories wrapped as
+        # Resources
+        factory.startFactory()
+
+        resource = WebSocketResource(factory)
+
+        # we server static files under "/" ..
+        webdir = os.path.abspath(
+            pkg_resources.resource_filename("leap.bitmask.core", "web"))
+        root = File(webdir)
+
+        # and our WebSocket server under "/ws"
+        root.putChild(u"bitmask", resource)
+
+        # both under one Twisted Web Site
+        site = Site(root)
+
+        self.site = site
+        self.factory = factory
+
+        self.listener = reactor.listenTCP(self.port, site)
+
+    def stopService(self):
+        self.factory.stopFactory()
+        self.site.stopFactory()
+        self.listener.stopListening()
+
+
+class DispatcherProtocol(WebSocketServerProtocol):
+
+    def onMessage(self, msg, binary):
+        parts = msg.split()
+        r = self.dispatcher.dispatch(parts)
+        r.addCallback(self.defer_reply, binary)
+
+    def reply(self, response, binary):
+        self.sendMessage(response, binary)
+
+    def defer_reply(self, response, binary):
+        reactor.callLater(0, self.reply, response, binary)
+
+    def _get_service(self, name):
+        return self.core.getServiceNamed(name)
-- 
cgit v1.2.3


From 23d5143d7b66e30f84556be85eeced5f914598a9 Mon Sep 17 00:00:00 2001
From: Kali Kaneko 
Date: Tue, 8 Mar 2016 11:36:21 -0400
Subject: [refactor] use __version__ directly

---
 src/leap/bitmask/core/service.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

(limited to 'src/leap/bitmask/core')

diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py
index 4c18ab5d..87349ef6 100644
--- a/src/leap/bitmask/core/service.py
+++ b/src/leap/bitmask/core/service.py
@@ -22,16 +22,14 @@ import resource
 from twisted.internet import reactor
 from twisted.python import log
 
-from leap.bonafide.service import BonafideService
-
+from leap.bitmask import __version__
 from leap.bitmask.core import configurable
 from leap.bitmask.core import mail_services
 from leap.bitmask.core import _zmq
 from leap.bitmask.core import websocket
-from leap.bitmask.core._version import get_versions
-
+from leap.bonafide.service import BonafideService
 from leap.common.events import server as event_server
-from leap.vpn import EIPService
+#from leap.vpn import EIPService
 
 
 class BitmaskBackend(configurable.ConfigurableService):
@@ -96,7 +94,9 @@ class BitmaskBackend(configurable.ConfigurableService):
         self._maybe_start_service('mail', service, self.basedir)
 
     def init_eip(self):
-        self._maybe_start_service('eip', EIPService)
+        # FIXME -- land EIP into leap.vpn
+        pass
+        #self._maybe_start_service('eip', EIPService)
 
     def init_zmq(self):
         zs = _zmq.ZMQServerService(self)
@@ -139,7 +139,7 @@ class BitmaskBackend(configurable.ConfigurableService):
         return " ".join(status_messages)
 
     def do_version(self):
-        version = get_versions()['version']
+        version = __version__
         return 'BitmaskCore: %s' % version
 
     def do_shutdown(self):
-- 
cgit v1.2.3


From b3f5538984502d97c77a9666f39c8d5b8f4a5c31 Mon Sep 17 00:00:00 2001
From: Kali Kaneko 
Date: Tue, 8 Mar 2016 11:36:37 -0400
Subject: [feature] add bitmaskd entrypoint

---
 src/leap/bitmask/core/launcher.py | 4 ++++
 1 file changed, 4 insertions(+)

(limited to 'src/leap/bitmask/core')

diff --git a/src/leap/bitmask/core/launcher.py b/src/leap/bitmask/core/launcher.py
index 7d658017..e363046e 100644
--- a/src/leap/bitmask/core/launcher.py
+++ b/src/leap/bitmask/core/launcher.py
@@ -35,3 +35,7 @@ def run_bitmaskd():
     ]
     print '[+] launching bitmaskd...'
     run()
+
+
+if __name__ == "__main__":
+    run_bitmaskd()
-- 
cgit v1.2.3


From 449bb9f2336d2f50a63d8558d1198b64b1ec4814 Mon Sep 17 00:00:00 2001
From: Kali Kaneko 
Date: Wed, 9 Mar 2016 16:27:36 -0400
Subject: [pkg] adapt launcher to work inside frozen binary

---
 src/leap/bitmask/core/launcher.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

(limited to 'src/leap/bitmask/core')

diff --git a/src/leap/bitmask/core/launcher.py b/src/leap/bitmask/core/launcher.py
index e363046e..b2319077 100644
--- a/src/leap/bitmask/core/launcher.py
+++ b/src/leap/bitmask/core/launcher.py
@@ -18,9 +18,10 @@
 Run bitmask daemon.
 """
 from twisted.scripts.twistd import run
-from os.path import join, dirname
+from os.path import join
 from sys import argv
 
+from leap.bitmask.util import here
 from leap.bitmask import core
 
 
@@ -28,7 +29,7 @@ def run_bitmaskd():
     # TODO --- configure where to put the logs... (get --logfile, --logdir
     # from the bitmask_cli
     argv[1:] = [
-        '-y', join(dirname(core.__file__), "bitmaskd.tac"),
+        '-y', join(here(core), "bitmaskd.tac"),
         '--pidfile', '/tmp/bitmaskd.pid',
         '--logfile', '/tmp/bitmaskd.log',
         '--umask=0022',
-- 
cgit v1.2.3


From c8de65eab85a8fd0a416dbcc3282f5fc105a5716 Mon Sep 17 00:00:00 2001
From: Kali Kaneko 
Date: Wed, 9 Mar 2016 16:34:43 -0400
Subject: [feature] add logging

---
 src/leap/bitmask/core/mail_services.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/leap/bitmask/core')

diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py
index 9858d498..ba428258 100644
--- a/src/leap/bitmask/core/mail_services.py
+++ b/src/leap/bitmask/core/mail_services.py
@@ -330,7 +330,7 @@ class KeymanagerService(service.Service, HookableService):
                 log.msg('Passing a new SRP Token to Keymanager: %s' % userid)
                 container.set_remote_auth_token(userid, token)
             else:
-                log.msg('storing the keymanager token...')
+                log.msg('storing the keymanager token... %s ' % token)
                 self.tokens[userid] = token
 
     # commands
-- 
cgit v1.2.3


From 9130d203bf15629c51dcc90bce83a8cd730900c4 Mon Sep 17 00:00:00 2001
From: Kali Kaneko 
Date: Wed, 9 Mar 2016 22:34:10 -0400
Subject: [refactor] use latest implementation of service_hooks

---
 src/leap/bitmask/core/mail_services.py | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

(limited to 'src/leap/bitmask/core')

diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py
index ba428258..a0be1211 100644
--- a/src/leap/bitmask/core/mail_services.py
+++ b/src/leap/bitmask/core/mail_services.py
@@ -135,9 +135,7 @@ def is_service_ready(service, provider):
     return has_service and has_config and is_enabled
 
 
-class SoledadService(service.Service, HookableService):
-
-    subscribed_to_hooks = ('on_bonafide_auth', 'on_passphrase_entry')
+class SoledadService(HookableService):
 
     def __init__(self, basedir):
         service.Service.__init__(self)
@@ -286,9 +284,7 @@ class KeymanagerContainer(Container):
             provider=provider)
 
 
-class KeymanagerService(service.Service, HookableService):
-
-    subscribed_to_hooks = ('on_new_soledad_instance', 'on_bonafide_auth')
+class KeymanagerService(HookableService):
 
     def __init__(self, basedir='~/.config/leap'):
         service.Service.__init__(self)
-- 
cgit v1.2.3


From c6db8a1f9ba7b6fc9dd117c0a699f4dfd339a375 Mon Sep 17 00:00:00 2001
From: Kali Kaneko 
Date: Mon, 18 Apr 2016 18:27:29 -0400
Subject: some fixes after review

---
 src/leap/bitmask/core/configurable.py  | 174 ++-------------------------------
 src/leap/bitmask/core/dispatcher.py    |   1 +
 src/leap/bitmask/core/mail_services.py |  64 +++++++-----
 3 files changed, 52 insertions(+), 187 deletions(-)

(limited to 'src/leap/bitmask/core')

diff --git a/src/leap/bitmask/core/configurable.py b/src/leap/bitmask/core/configurable.py
index 3b97916d..8e33de95 100644
--- a/src/leap/bitmask/core/configurable.py
+++ b/src/leap/bitmask/core/configurable.py
@@ -18,15 +18,15 @@
 Configurable Backend for Bitmask Service.
 """
 import ConfigParser
-import locale
 import os
-import re
-import sys
 
 from twisted.application import service
-from twisted.python import log
 
 from leap.common import files
+from leap.common.config import get_path_prefix
+
+
+DEFAULT_BASEDIR = os.path.join(get_path_prefix(), 'leap')
 
 
 class MissingConfigEntry(Exception):
@@ -40,7 +40,7 @@ class ConfigurableService(service.MultiService):
     config_file = u"bitmaskd.cfg"
     service_names = ('mail', 'eip', 'zmq', 'web')
 
-    def __init__(self, basedir='~/.config/leap'):
+    def __init__(self, basedir=DEFAULT_BASEDIR):
         service.MultiService.__init__(self)
 
         path = os.path.abspath(os.path.expanduser(basedir))
@@ -61,10 +61,9 @@ class ConfigurableService(service.MultiService):
 
         except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
             if default is None:
-                fn = os.path.join(self.basedir, self.config_file)
+                fn = self._get_config_path()
                 raise MissingConfigEntry("%s is missing the [%s]%s entry"
-                                         % (_quote_output(fn),
-                                            section, option))
+                                         % fn, section, option)
             return default
 
     def set_config(self, section, option, value):
@@ -82,12 +81,8 @@ class ConfigurableService(service.MultiService):
         if not os.path.isfile(bitmaskd_cfg):
             self._create_default_config(bitmaskd_cfg)
 
-        try:
-            with open(bitmaskd_cfg, "rb") as f:
-                self.config.readfp(f)
-        except EnvironmentError:
-            if os.path.exists(bitmaskd_cfg):
-                raise
+        with open(bitmaskd_cfg, "rb") as f:
+            self.config.readfp(f)
 
     def save_config(self):
         bitmaskd_cfg = self._get_config_path()
@@ -109,154 +104,3 @@ eip = True
 zmq = True
 web = False
 """
-
-
-def canonical_encoding(encoding):
-    if encoding is None:
-        log.msg("Warning: falling back to UTF-8 encoding.", level=log.WEIRD)
-        encoding = 'utf-8'
-    encoding = encoding.lower()
-    if encoding == "cp65001":
-        encoding = 'utf-8'
-    elif (encoding == "us-ascii" or encoding == "646" or encoding ==
-          "ansi_x3.4-1968"):
-        encoding = 'ascii'
-
-    return encoding
-
-
-def check_encoding(encoding):
-    # sometimes Python returns an encoding name that it doesn't support for
-    # conversion fail early if this happens
-    try:
-        u"test".encode(encoding)
-    except (LookupError, AttributeError):
-        raise AssertionError(
-            "The character encoding '%s' is not supported for conversion." % (
-                encoding,))
-
-filesystem_encoding = None
-io_encoding = None
-is_unicode_platform = False
-
-
-def _reload():
-    global filesystem_encoding, io_encoding, is_unicode_platform
-
-    filesystem_encoding = canonical_encoding(sys.getfilesystemencoding())
-    check_encoding(filesystem_encoding)
-
-    if sys.platform == 'win32':
-        # On Windows we install UTF-8 stream wrappers for sys.stdout and
-        # sys.stderr, and reencode the arguments as UTF-8 (see
-        # scripts/runner.py).
-        io_encoding = 'utf-8'
-    else:
-        ioenc = None
-        if hasattr(sys.stdout, 'encoding'):
-            ioenc = sys.stdout.encoding
-        if ioenc is None:
-            try:
-                ioenc = locale.getpreferredencoding()
-            except Exception:
-                pass  # work around 
-        io_encoding = canonical_encoding(ioenc)
-
-    check_encoding(io_encoding)
-
-    is_unicode_platform = sys.platform in ["win32", "darwin"]
-
-_reload()
-
-
-def _quote_output(s, quotemarks=True, quote_newlines=None, encoding=None):
-    """
-    Encode either a Unicode string or a UTF-8-encoded bytestring for
-    representation on stdout or stderr, tolerating errors. If 'quotemarks' is
-    True, the string is always quoted; otherwise, it is quoted only if
-    necessary to avoid ambiguity or control bytes in the output. (Newlines are
-    counted as control bytes iff quote_newlines is True.)
-
-    Quoting may use either single or double quotes. Within single quotes, all
-    characters stand for themselves, and ' will not appear. Within double
-    quotes, Python-compatible backslash escaping is used.
-
-    If not explicitly given, quote_newlines is True when quotemarks is True.
-    """
-    assert isinstance(s, (str, unicode))
-    if quote_newlines is None:
-        quote_newlines = quotemarks
-
-    if isinstance(s, str):
-        try:
-            s = s.decode('utf-8')
-        except UnicodeDecodeError:
-            return 'b"%s"' % (
-                ESCAPABLE_8BIT.sub(
-                    lambda m: _str_escape(m, quote_newlines), s),)
-
-    must_double_quote = (quote_newlines and MUST_DOUBLE_QUOTE_NL or
-                         MUST_DOUBLE_QUOTE)
-    if must_double_quote.search(s) is None:
-        try:
-            out = s.encode(encoding or io_encoding)
-            if quotemarks or out.startswith('"'):
-                return "'%s'" % (out,)
-            else:
-                return out
-        except (UnicodeDecodeError, UnicodeEncodeError):
-            pass
-
-    escaped = ESCAPABLE_UNICODE.sub(
-        lambda m: _unicode_escape(m, quote_newlines), s)
-    return '"%s"' % (
-        escaped.encode(encoding or io_encoding, 'backslashreplace'),)
-
-
-def _unicode_escape(m, quote_newlines):
-    u = m.group(0)
-    if u == u'"' or u == u'$' or u == u'`' or u == u'\\':
-        return u'\\' + u
-    elif u == u'\n' and not quote_newlines:
-        return u
-    if len(u) == 2:
-        codepoint = (
-            ord(u[0]) - 0xD800) * 0x400 + ord(u[1]) - 0xDC00 + 0x10000
-    else:
-        codepoint = ord(u)
-    if codepoint > 0xFFFF:
-        return u'\\U%08x' % (codepoint,)
-    elif codepoint > 0xFF:
-        return u'\\u%04x' % (codepoint,)
-    else:
-        return u'\\x%02x' % (codepoint,)
-
-
-def _str_escape(m, quote_newlines):
-    c = m.group(0)
-    if c == '"' or c == '$' or c == '`' or c == '\\':
-        return '\\' + c
-    elif c == '\n' and not quote_newlines:
-        return c
-    else:
-        return '\\x%02x' % (ord(c),)
-
-MUST_DOUBLE_QUOTE_NL = re.compile(
-    ur'[^\x20-\x26\x28-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]',
-    re.DOTALL)
-MUST_DOUBLE_QUOTE = re.compile(
-    ur'[^\n\x20-\x26\x28-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]',
-    re.DOTALL)
-
-ESCAPABLE_8BIT = re.compile(
-    r'[^ !#\x25-\x5B\x5D-\x5F\x61-\x7E]',
-    re.DOTALL)
-
-# if we must double-quote, then we have to escape ", $ and `, but need not
-# escape '
-
-ESCAPABLE_UNICODE = re.compile(
-    ur'([\uD800-\uDBFF][\uDC00-\uDFFF])|'  # valid surrogate pairs
-    ur'[^ !#\x25-\x5B\x5D-\x5F\x61-\x7E\u00A0-\uD7FF'
-    ur'\uE000-\uFDCF\uFDF0-\uFFFC]',
-    re.DOTALL)
diff --git a/src/leap/bitmask/core/dispatcher.py b/src/leap/bitmask/core/dispatcher.py
index 4d7e1813..648cbe9b 100644
--- a/src/leap/bitmask/core/dispatcher.py
+++ b/src/leap/bitmask/core/dispatcher.py
@@ -84,6 +84,7 @@ class CommandDispatcher(object):
         return d
 
     def do_EIP(self, *parts):
+
         subcmd = parts[1]
         eip_label = 'eip'
 
diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py
index a0be1211..6472bdc2 100644
--- a/src/leap/bitmask/core/mail_services.py
+++ b/src/leap/bitmask/core/mail_services.py
@@ -1,3 +1,19 @@
+# -*- coding: utf-8 -*-
+# mail_services.py
+# Copyright (C) 2016 LEAP Encryption Acess Project
+#
+# 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 .
 """
 Mail services.
 
@@ -6,7 +22,6 @@ This should be moved to the different packages when it stabilizes.
 """
 import json
 import os
-from glob import glob
 from collections import defaultdict
 from collections import namedtuple
 
@@ -26,31 +41,30 @@ from leap.mail.incoming.service import IncomingMail, INCOMING_CHECK_PERIOD
 from leap.mail import smtp
 
 from leap.bitmask.core.uuid_map import UserMap
+from leap.bitmask.core.configurable import DEFAULT_BASEDIR
 
 
 class Container(object):
 
-    def __init__(self):
+    def __init__(self, service=None):
         self._instances = defaultdict(None)
+        if service is not None:
+            self.service = service
 
     def get_instance(self, key):
         return self._instances.get(key, None)
 
+    def add_instance(self, key, data):
+        self._instances[key] = data
+
 
 class ImproperlyConfigured(Exception):
     pass
 
 
-def get_all_soledad_uuids():
-    return [os.path.split(p)[-1].split('.db')[0] for p in
-            glob(os.path.expanduser('~/.config/leap/soledad/*.db'))]
-    # FIXME do not hardcode basedir
-
-
 class SoledadContainer(Container):
 
-    def __init__(self, basedir='~/.config/leap'):
-        # FIXME do not hardcode basedir
+    def __init__(self, basedir=DEFAULT_BASEDIR):
         self._basedir = os.path.expanduser(basedir)
         self._usermap = UserMap()
         super(SoledadContainer, self).__init__()
@@ -75,19 +89,17 @@ class SoledadContainer(Container):
             uuid, passphrase, soledad_path, soledad_url,
             cert_path, token)
 
-        self._instances[userid] = soledad
+        self.add_instances(userid, soledad)
 
         data = {'user': userid, 'uuid': uuid, 'token': token,
                 'soledad': soledad}
         self.service.trigger_hook('on_new_soledad_instance', **data)
 
-    def _create_soledad_instance(self, uuid, passphrase, basedir, server_url,
-                                 cert_file, token):
+    def _create_soledad_instance(self, uuid, passphrase, soledad_path,
+                                 server_url, cert_file, token):
         # setup soledad info
-        secrets_path = os.path.join(
-            basedir, '%s.secret' % uuid)
-        local_db_path = os.path.join(
-            basedir, '%s.db' % uuid)
+        secrets_path = os.path.join(soledad_path, '%s.secret' % uuid)
+        local_db_path = os.path.join(soledad_path, '%s.db' % uuid)
 
         if token is None:
             syncable = False
@@ -143,8 +155,7 @@ class SoledadService(HookableService):
 
     def startService(self):
         log.msg('Starting Soledad Service')
-        self._container = SoledadContainer()
-        self._container.service = self
+        self._container = SoledadContainer(service=self)
         super(SoledadService, self).startService()
 
     # hooks
@@ -209,7 +220,7 @@ class KeymanagerContainer(Container):
 
     def _on_keymanager_ready_cb(self, keymanager, userid, soledad):
         # TODO use onready-deferreds instead
-        self._instances[userid] = keymanager
+        self.add_instance(userid, keymanager)
 
         log.msg("Adding Keymanager instance for: %s" % userid)
         data = {'userid': userid, 'soledad': soledad, 'keymanager': keymanager}
@@ -264,6 +275,11 @@ class KeymanagerContainer(Container):
             token = self.service.tokens.get(userid)
 
         km_args = (userid, nickserver_uri, soledad)
+
+        # TODO use the method in
+        # services.soledadbootstrapper._get_gpg_bin_path.
+        # That should probably live in keymanager package.
+
         km_kwargs = {
             "token": token, "uid": uuid,
             "api_uri": api_uri, "api_version": "1",
@@ -373,10 +389,12 @@ class StandardMailService(service.MultiService, HookableService):
 
     def initializeChildrenServices(self):
         self.addService(IMAPService(self._soledad_sessions))
-        self.addService(IncomingMailService(self))
         self.addService(SMTPService(
             self._soledad_sessions, self._keymanager_sessions,
             self._sendmail_opts))
+        # TODO adapt the service to receive soledad/keymanager sessions object.
+        # See also the TODO before IncomingMailService.startInstance
+        self.addService(IncomingMailService(self))
 
     def startService(self):
         log.msg('Starting Mail Service...')
@@ -438,6 +456,7 @@ class StandardMailService(service.MultiService, HookableService):
         if not active_user:
             return defer.succeed('NO ACTIVE USER')
         token = self._imap_tokens.get(active_user)
+        # TODO return just the tuple, no format.
         return defer.succeed("IMAP TOKEN (%s): %s" % (active_user, token))
 
     def get_smtp_token(self):
@@ -445,6 +464,7 @@ class StandardMailService(service.MultiService, HookableService):
         if not active_user:
             return defer.succeed('NO ACTIVE USER')
         token = self._smtp_tokens.get(active_user)
+        # TODO return just the tuple, no format.
         return defer.succeed("SMTP TOKEN (%s): %s" % (active_user, token))
 
     def do_get_smtp_cert_path(self, userid):
@@ -560,7 +580,7 @@ class IncomingMailService(service.Service):
             if start_sync:
                 incoming_instance.startService()
 
-        acc = Account(soledad)
+        acc = Account(soledad, userid)
         d = acc.callWhenReady(
             lambda _: acc.get_collection_by_mailbox(INBOX_NAME))
         d.addCallback(setUpIncomingMail)
-- 
cgit v1.2.3


From 5b90ad3552025436edb40665203ab98eceaa065b Mon Sep 17 00:00:00 2001
From: Kali Kaneko 
Date: Tue, 19 Apr 2016 12:07:09 -0400
Subject: pep8/flake8

---
 src/leap/bitmask/core/service.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'src/leap/bitmask/core')

diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py
index 87349ef6..13c8864a 100644
--- a/src/leap/bitmask/core/service.py
+++ b/src/leap/bitmask/core/service.py
@@ -29,7 +29,7 @@ from leap.bitmask.core import _zmq
 from leap.bitmask.core import websocket
 from leap.bonafide.service import BonafideService
 from leap.common.events import server as event_server
-#from leap.vpn import EIPService
+# from leap.vpn import EIPService
 
 
 class BitmaskBackend(configurable.ConfigurableService):
@@ -96,7 +96,7 @@ class BitmaskBackend(configurable.ConfigurableService):
     def init_eip(self):
         # FIXME -- land EIP into leap.vpn
         pass
-        #self._maybe_start_service('eip', EIPService)
+        # self._maybe_start_service('eip', EIPService)
 
     def init_zmq(self):
         zs = _zmq.ZMQServerService(self)
-- 
cgit v1.2.3


From da45ed15190ea0f9c7c38b6312239f27664545a4 Mon Sep 17 00:00:00 2001
From: Kali Kaneko 
Date: Thu, 28 Apr 2016 15:46:13 -0400
Subject: [bug] do not force autobahn dependency yet

websockets interface is not mature enough yet, make this dependency
optional, for the case the user actively enables it.

- Releases: 0.9.2
---
 src/leap/bitmask/core/service.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/leap/bitmask/core')

diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py
index 13c8864a..ceda6353 100644
--- a/src/leap/bitmask/core/service.py
+++ b/src/leap/bitmask/core/service.py
@@ -26,7 +26,6 @@ from leap.bitmask import __version__
 from leap.bitmask.core import configurable
 from leap.bitmask.core import mail_services
 from leap.bitmask.core import _zmq
-from leap.bitmask.core import websocket
 from leap.bonafide.service import BonafideService
 from leap.common.events import server as event_server
 # from leap.vpn import EIPService
@@ -103,6 +102,7 @@ class BitmaskBackend(configurable.ConfigurableService):
         zs.setServiceParent(self)
 
     def init_web(self):
+        from leap.bitmask.core import websocket
         ws = websocket.WebSocketsDispatcherService(self)
         ws.setServiceParent(self)
 
-- 
cgit v1.2.3


From 928f547a1d1235306056be1d81e8400d4d77ecce Mon Sep 17 00:00:00 2001
From: Kali Kaneko 
Date: Sat, 30 Apr 2016 12:15:16 -0400
Subject: [bug] fix hardcoded paths

---
 src/leap/bitmask/core/mail_services.py | 4 ++--
 src/leap/bitmask/core/service.py       | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

(limited to 'src/leap/bitmask/core')

diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py
index 6472bdc2..3495aa39 100644
--- a/src/leap/bitmask/core/mail_services.py
+++ b/src/leap/bitmask/core/mail_services.py
@@ -302,7 +302,7 @@ class KeymanagerContainer(Container):
 
 class KeymanagerService(HookableService):
 
-    def __init__(self, basedir='~/.config/leap'):
+    def __init__(self, basedir=DEFAULT_BASEDIR):
         service.Service.__init__(self)
         self._basedir = basedir
 
@@ -507,7 +507,7 @@ class SMTPService(service.Service):
     name = 'smtp'
 
     def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts,
-                 basedir='~/.config/leap'):
+                 basedir=DEFAULT_BASEDIR):
 
         self._basedir = os.path.expanduser(basedir)
         port, factory = smtp.run_service(
diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py
index ceda6353..ddd86155 100644
--- a/src/leap/bitmask/core/service.py
+++ b/src/leap/bitmask/core/service.py
@@ -33,7 +33,7 @@ from leap.common.events import server as event_server
 
 class BitmaskBackend(configurable.ConfigurableService):
 
-    def __init__(self, basedir='~/.config/leap'):
+    def __init__(self, basedir=configurable.DEFAULT_BASEDIR):
 
         configurable.ConfigurableService.__init__(self, basedir)
 
-- 
cgit v1.2.3


From 08da5b11103cdd132c3ac4110ba42fcc8510a78b Mon Sep 17 00:00:00 2001
From: Kali Kaneko 
Date: Sat, 30 Apr 2016 12:15:58 -0400
Subject: [refactor] pass backend to core service

---
 src/leap/bitmask/core/flags.py    |  1 +
 src/leap/bitmask/core/launcher.py |  7 ++++++-
 src/leap/bitmask/core/service.py  | 13 ++++++++-----
 3 files changed, 15 insertions(+), 6 deletions(-)
 create mode 100644 src/leap/bitmask/core/flags.py

(limited to 'src/leap/bitmask/core')

diff --git a/src/leap/bitmask/core/flags.py b/src/leap/bitmask/core/flags.py
new file mode 100644
index 00000000..9a40c70c
--- /dev/null
+++ b/src/leap/bitmask/core/flags.py
@@ -0,0 +1 @@
+BACKEND = 'default'
diff --git a/src/leap/bitmask/core/launcher.py b/src/leap/bitmask/core/launcher.py
index b2319077..b8916a1e 100644
--- a/src/leap/bitmask/core/launcher.py
+++ b/src/leap/bitmask/core/launcher.py
@@ -17,17 +17,22 @@
 """
 Run bitmask daemon.
 """
-from twisted.scripts.twistd import run
 from os.path import join
 from sys import argv
 
+from twisted.scripts.twistd import run
+
 from leap.bitmask.util import here
 from leap.bitmask import core
+from leap.bitmask.core import flags
 
 
 def run_bitmaskd():
     # TODO --- configure where to put the logs... (get --logfile, --logdir
     # from the bitmask_cli
+    for (index, arg) in enumerate(argv):
+        if arg == '--backend':
+            flags.BACKEND = argv[index + 1]
     argv[1:] = [
         '-y', join(here(core), "bitmaskd.tac"),
         '--pidfile', '/tmp/bitmaskd.pid',
diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py
index ddd86155..fca51048 100644
--- a/src/leap/bitmask/core/service.py
+++ b/src/leap/bitmask/core/service.py
@@ -17,6 +17,7 @@
 """
 Bitmask-core Service.
 """
+import json
 import resource
 
 from twisted.internet import reactor
@@ -26,6 +27,7 @@ from leap.bitmask import __version__
 from leap.bitmask.core import configurable
 from leap.bitmask.core import mail_services
 from leap.bitmask.core import _zmq
+from leap.bitmask.core import flags
 from leap.bonafide.service import BonafideService
 from leap.common.events import server as event_server
 # from leap.vpn import EIPService
@@ -126,17 +128,18 @@ class BitmaskBackend(configurable.ConfigurableService):
         # we may want to make this tuple a class member
         services = ('soledad', 'keymanager', 'mail', 'eip')
 
-        status_messages = []
+        status = {}
         for name in services:
-            status = 'stopped'
+            _status = 'stopped'
             try:
                 if self.getServiceNamed(name).running:
-                    status = "running"
+                    _status = 'running'
             except KeyError:
                 pass
-            status_messages.append("[{}: {}]".format(name, status))
+            status[name] = _status
+        status['backend'] = flags.BACKEND
 
-        return " ".join(status_messages)
+        return json.dumps(status)
 
     def do_version(self):
         version = __version__
-- 
cgit v1.2.3


From 1173e77cb8d635936c9730ba4ad8b88b24ad1be2 Mon Sep 17 00:00:00 2001
From: Kali Kaneko 
Date: Sun, 1 May 2016 11:09:07 -0400
Subject: [feature] pluggable backends and api registry

the idea behind this mechanism (partially implemented for that) is to be
able to check the backend output against some type annotations.

We want to be able to detect if a given backend (real services or
authoritative mocks) have diverged from what's specified in the API
annotations.
---
 src/leap/bitmask/core/api.py           |  54 +++++++
 src/leap/bitmask/core/api_contract.py  |  40 +++++
 src/leap/bitmask/core/dispatcher.py    | 286 +++++++++++++++++++++------------
 src/leap/bitmask/core/dummy.py         |  80 +++++++++
 src/leap/bitmask/core/mail_services.py |  10 +-
 src/leap/bitmask/core/service.py       |  86 ++++++----
 6 files changed, 419 insertions(+), 137 deletions(-)
 create mode 100644 src/leap/bitmask/core/api.py
 create mode 100644 src/leap/bitmask/core/api_contract.py
 create mode 100644 src/leap/bitmask/core/dummy.py

(limited to 'src/leap/bitmask/core')

diff --git a/src/leap/bitmask/core/api.py b/src/leap/bitmask/core/api.py
new file mode 100644
index 00000000..9f3725dc
--- /dev/null
+++ b/src/leap/bitmask/core/api.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# api.py
+# Copyright (C) 2016 LEAP Encryption Acess Project
+#
+# 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 .
+"""
+Registry for the public API for the Bitmask Backend.
+"""
+from collections import OrderedDict
+
+registry = OrderedDict()
+
+
+class APICommand(type):
+    """
+    A metaclass to keep a global registry of all the methods that compose the
+    public API for the Bitmask Backend.
+    """
+    def __init__(cls, name, bases, attrs):
+        for key, val in attrs.iteritems():
+            properties = getattr(val, 'register', None)
+            label = getattr(cls, 'label', None)
+            if label:
+                name = label
+            if properties is not None:
+                registry['%s.%s' % (name, key)] = properties
+
+
+def register_method(*args):
+    """
+    This method gathers info about all the methods that are supposed to
+    compose the public API to communicate with the backend.
+
+    It sets up a register property for any method that uses it.
+    A type annotation is supposed to be in this property.
+    The APICommand metaclass collects these properties of the methods and
+    stores them in the global api_registry object, where they can be
+    introspected at runtime.
+    """
+    def decorator(f):
+        f.register = tuple(args)
+        return f
+    return decorator
diff --git a/src/leap/bitmask/core/api_contract.py b/src/leap/bitmask/core/api_contract.py
new file mode 100644
index 00000000..86b600c1
--- /dev/null
+++ b/src/leap/bitmask/core/api_contract.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# api_contract.py
+# Copyright (C) 2016 LEAP Encryption Acess Project
+#
+# 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 .
+"""
+Display a human-readable representation of the methods that compound the public
+api for Bitmask Core.
+
+The values are meant to be type annotations.
+"""
+
+if __name__ == "__main__":
+    from leap.bitmask.core.service import BitmaskBackend
+    from leap.bitmask.core import api
+    backend = BitmaskBackend()
+
+    print '========= Bitmask Core API =================='
+    print
+
+    for key in api.registry:
+        human_key = key.replace('do_', '').lower()
+        value = api.registry[key]
+
+        print("{}:\t\t{}".format(
+            human_key,
+            ' '.join([x for x in value])))
+    print
+    print '============================================='
diff --git a/src/leap/bitmask/core/dispatcher.py b/src/leap/bitmask/core/dispatcher.py
index 648cbe9b..e7c961fd 100644
--- a/src/leap/bitmask/core/dispatcher.py
+++ b/src/leap/bitmask/core/dispatcher.py
@@ -22,154 +22,214 @@ import json
 from twisted.internet import defer
 from twisted.python import failure, log
 
+from .api import APICommand, register_method
 
-# TODO implement sub-classes to dispatch subcommands (user, mail).
 
+class SubCommand(object):
 
-class CommandDispatcher(object):
+    __metaclass__ = APICommand
 
-    def __init__(self, core):
+    def dispatch(self, service, *parts, **kw):
+        subcmd = parts[1]
 
-        self.core = core
+        _method = getattr(self, 'do_' + subcmd.upper(), None)
+        if not _method:
+            raise RuntimeError('No such subcommand')
+        return _method(service, *parts, **kw)
 
-    def _get_service(self, name):
 
-        try:
-            return self.core.getServiceNamed(name)
-        except KeyError:
-            return None
+class UserCmd(SubCommand):
 
-    def dispatch(self, msg):
-        cmd = msg[0]
+    label = 'user'
 
-        _method = getattr(self, 'do_' + cmd.upper(), None)
+    @register_method("{'srp_token': unicode, 'uuid': unicode}")
+    def do_AUTHENTICATE(self, bonafide, *parts):
+        user, password = parts[2], parts[3]
+        d = defer.maybeDeferred(bonafide.do_authenticate, user, password)
+        return d
 
-        if not _method:
-            return defer.fail(failure.Failure(RuntimeError('No such command')))
+    @register_method("{'signup': 'ok', 'user': str}")
+    def do_SIGNUP(self, bonafide, *parts):
+        user, password = parts[2], parts[3]
+        d = defer.maybeDeferred(bonafide.do_signup, user, password)
+        return d
 
-        return defer.maybeDeferred(_method, *msg)
+    @register_method("{'logout': 'ok'}")
+    def do_LOGOUT(self, bonafide, *parts):
+        user, password = parts[2], parts[3]
+        d = defer.maybeDeferred(bonafide.do_logout, user, password)
+        return d
 
-    def do_STATS(self, *parts):
-        return _format_result(self.core.do_stats())
+    @register_method('str')
+    def do_ACTIVE(self, bonafide, *parts):
+        d = defer.maybeDeferred(bonafide.do_get_active_user)
+        return d
 
-    def do_VERSION(self, *parts):
-        return _format_result(self.core.do_version())
 
-    def do_STATUS(self, *parts):
-        return _format_result(self.core.do_status())
+class EIPCmd(SubCommand):
 
-    def do_SHUTDOWN(self, *parts):
-        return _format_result(self.core.do_shutdown())
+    label = 'eip'
 
-    def do_USER(self, *parts):
+    @register_method('dict')
+    def do_ENABLE(self, service, *parts):
+        d = service.do_enable_service(self.label)
+        return d
 
-        subcmd = parts[1]
-        user, password = parts[2], parts[3]
+    @register_method('dict')
+    def do_DISABLE(self, service, *parts):
+        d = service.do_disable_service(self.label)
+        return d
 
-        bf = self._get_service('bonafide')
+    @register_method('dict')
+    def do_STATUS(self, eip, *parts):
+        d = eip.do_status()
+        return d
 
-        if subcmd == 'authenticate':
-            d = bf.do_authenticate(user, password)
+    @register_method('dict')
+    def do_START(self, eip, *parts):
+        # TODO --- attempt to get active provider
+        # TODO or catch the exception and send error
+        provider = parts[2]
+        d = eip.do_start(provider)
+        return d
 
-        elif subcmd == 'signup':
-            d = bf.do_signup(user, password)
+    @register_method('dict')
+    def do_STOP(self, eip, *parts):
+        d = eip.do_stop()
+        return d
 
-        elif subcmd == 'logout':
-            d = bf.do_logout(user, password)
 
-        elif subcmd == 'active':
-            d = bf.do_get_active_user()
+class MailCmd(SubCommand):
 
-        d.addCallbacks(_format_result, _format_error)
+    label = 'mail'
+
+    @register_method('dict')
+    def do_ENABLE(self, service, *parts):
+        d = service.do_enable_service(self.label)
         return d
 
-    def do_EIP(self, *parts):
+    @register_method('dict')
+    def do_DISABLE(self, service, *parts):
+        d = service.do_disable_service(self.label)
+        return d
 
-        subcmd = parts[1]
-        eip_label = 'eip'
+    @register_method('dict')
+    def do_STATUS(self, mail, *parts):
+        d = mail.do_status()
+        return d
 
-        if subcmd == 'enable':
-            return _format_result(
-                self.core.do_enable_service(eip_label))
+    @register_method('dict')
+    def do_GET_IMAP_TOKEN(self, mail, *parts):
+        d = mail.get_imap_token()
+        return d
 
-        eip = self._get_service(eip_label)
-        if not eip:
-            return _format_result('eip: disabled')
+    @register_method('dict')
+    def do_GET_SMTP_TOKEN(self, mail, *parts):
+        d = mail.get_smtp_token()
+        return d
 
-        if subcmd == 'status':
-            return _format_result(eip.do_status())
+    @register_method('dict')
+    def do_GET_SMTP_CERTIFICATE(self, mail, *parts, **kw):
+        # TODO move to mail service
+        # TODO should ask for confirmation? like --force or something,
+        # if we already have a valid one. or better just refuse if cert
+        # exists.
+        # TODO how should we pass the userid??
+        # - Keep an 'active' user in bonafide (last authenticated)
+        # (doing it now)
+        # - Get active user from Mail Service (maybe preferred?)
+        # - Have a command/method to set 'active' user.
+
+        @defer.inlineCallbacks
+        def save_cert(cert_data):
+            userid, cert_str = cert_data
+            cert_path = yield mail.do_get_smtp_cert_path(userid)
+            with open(cert_path, 'w') as outf:
+                outf.write(cert_str)
+            defer.returnValue('certificate saved to %s' % cert_path)
+
+        bonafide = kw['bonafide']
+        d = bonafide.do_get_smtp_cert()
+        d.addCallback(save_cert)
+        return d
 
-        elif subcmd == 'disable':
-            return _format_result(
-                self.core.do_disable_service(eip_label))
 
-        elif subcmd == 'start':
-            # TODO --- attempt to get active provider
-            # TODO or catch the exception and send error
-            provider = parts[2]
-            d = eip.do_start(provider)
-            d.addCallbacks(_format_result, _format_error)
-            return d
+class CommandDispatcher(object):
 
-        elif subcmd == 'stop':
-            d = eip.do_stop()
-            d.addCallbacks(_format_result, _format_error)
-            return d
+    __metaclass__ = APICommand
 
-    def do_MAIL(self, *parts):
+    label = 'core'
 
+    def __init__(self, core):
+
+        self.core = core
+        self.subcommand_user = UserCmd()
+        self.subcommand_eip = EIPCmd()
+        self.subcommand_mail = MailCmd()
+
+    # XXX --------------------------------------------
+    # TODO move general services to another subclass
+
+    @register_method("{'mem_usage': str}")
+    def do_STATS(self, *parts):
+        return _format_result(self.core.do_stats())
+
+    @register_method("{version_core': '0.0.0'}")
+    def do_VERSION(self, *parts):
+        return _format_result(self.core.do_version())
+
+    @register_method("{'mail': 'running'}")
+    def do_STATUS(self, *parts):
+        return _format_result(self.core.do_status())
+
+    @register_method("{'shutdown': 'ok'}")
+    def do_SHUTDOWN(self, *parts):
+        return _format_result(self.core.do_shutdown())
+
+    # -----------------------------------------------
+
+    def do_USER(self, *parts):
+        bonafide = self._get_service('bonafide')
+        d = self.subcommand_user.dispatch(bonafide, *parts)
+        d.addCallbacks(_format_result, _format_error)
+        return d
+
+    def do_EIP(self, *parts):
+        eip = self._get_service(self.subcommand_eip.label)
+        if not eip:
+            return _format_result('eip: disabled')
         subcmd = parts[1]
-        mail_label = 'mail'
 
-        if subcmd == 'enable':
-            return _format_result(
-                self.core.do_enable_service(mail_label))
+        dispatch = self._subcommand_eip.dispatch
+        if subcmd in ('enable', 'disable'):
+            d = dispatch(self.core, *parts)
+        else:
+            d = dispatch(eip, *parts)
 
-        m = self._get_service(mail_label)
-        bf = self._get_service('bonafide')
+        d.addCallbacks(_format_result, _format_error)
+        return d
 
-        if not m:
-            return _format_result('mail: disabled')
+    def do_MAIL(self, *parts):
+        subcmd = parts[1]
+        dispatch = self.subcommand_mail.dispatch
 
-        if subcmd == 'status':
-            return _format_result(m.do_status())
+        if subcmd == 'enable':
+            d = dispatch(self.core, *parts)
 
-        elif subcmd == 'disable':
-            return _format_result(self.core.do_disable_service(mail_label))
+        mail = self._get_service(self.subcommand_mail.label)
+        bonafide = self._get_service('bonafide')
+        kw = {'bonafide': bonafide}
 
-        elif subcmd == 'get_imap_token':
-            d = m.get_imap_token()
-            d.addCallbacks(_format_result, _format_error)
-            return d
+        if not mail:
+            return _format_result('mail: disabled')
 
-        elif subcmd == 'get_smtp_token':
-            d = m.get_smtp_token()
-            d.addCallbacks(_format_result, _format_error)
-            return d
+        if subcmd == 'disable':
+            d = dispatch(self.core)
+        else:
+            d = dispatch(mail, *parts, **kw)
 
-        elif subcmd == 'get_smtp_certificate':
-            # TODO move to mail service
-            # TODO should ask for confirmation? like --force or something,
-            # if we already have a valid one. or better just refuse if cert
-            # exists.
-            # TODO how should we pass the userid??
-            # - Keep an 'active' user in bonafide (last authenticated)
-            # (doing it now)
-            # - Get active user from Mail Service (maybe preferred?)
-            # - Have a command/method to set 'active' user.
-
-            @defer.inlineCallbacks
-            def save_cert(cert_data):
-                userid, cert_str = cert_data
-                cert_path = yield m.do_get_smtp_cert_path(userid)
-                with open(cert_path, 'w') as outf:
-                    outf.write(cert_str)
-                defer.returnValue('certificate saved to %s' % cert_path)
-
-            d = bf.do_get_smtp_cert()
-            d.addCallback(save_cert)
-            d.addCallbacks(_format_result, _format_error)
-            return d
+        d.addCallbacks(_format_result, _format_error)
+        return d
 
     def do_KEYS(self, *parts):
         subcmd = parts[1]
@@ -187,6 +247,22 @@ class CommandDispatcher(object):
             d.addCallbacks(_format_result, _format_error)
             return d
 
+    def dispatch(self, msg):
+        cmd = msg[0]
+
+        _method = getattr(self, 'do_' + cmd.upper(), None)
+
+        if not _method:
+            return defer.fail(failure.Failure(RuntimeError('No such command')))
+
+        return defer.maybeDeferred(_method, *msg)
+
+    def _get_service(self, name):
+        try:
+            return self.core.getServiceNamed(name)
+        except KeyError:
+            return None
+
 
 def _format_result(result):
     return json.dumps({'error': None, 'result': result})
diff --git a/src/leap/bitmask/core/dummy.py b/src/leap/bitmask/core/dummy.py
new file mode 100644
index 00000000..99dfafa5
--- /dev/null
+++ b/src/leap/bitmask/core/dummy.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+# dummy.py
+# Copyright (C) 2016 LEAP Encryption Acess Project
+#
+# 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 .
+"""
+An authoritative dummy backend for tests.
+"""
+import json
+
+from leap.common.service_hooks import HookableService
+
+
+class BackendCommands(object):
+
+    """
+    General commands for the BitmaskBackend Core Service.
+    """
+
+    def __init__(self, core):
+        self.core = core
+
+    def do_status(self):
+        return json.dumps(
+            {'soledad': 'running',
+             'keymanager': 'running',
+             'mail': 'running',
+             'eip': 'stopped',
+             'backend': 'dummy'})
+
+    def do_version(self):
+        return {'version_core': '0.0.1'}
+
+    def do_stats(self):
+        return {'mem_usage': '01 KB'}
+
+    def do_shutdown(self):
+        return {'shutdown': 'ok'}
+
+
+class mail_services(object):
+
+    class SoledadService(HookableService):
+        pass
+
+    class KeymanagerService(HookableService):
+        pass
+
+    class StandardMailService(HookableService):
+        pass
+
+
+class BonafideService(HookableService):
+
+    def __init__(self, basedir):
+        pass
+
+    def do_authenticate(self, user, password):
+        return {u'srp_token': u'deadbeef123456789012345678901234567890123',
+                u'uuid': u'01234567890abcde01234567890abcde'}
+
+    def do_signup(self, user, password):
+        return {'signup': 'ok', 'user': 'dummyuser@provider.example.org'}
+
+    def do_logout(self, user, password):
+        return {'logout': 'ok'}
+
+    def do_get_active_user(self):
+        return 'dummyuser@provider.example.org'
diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py
index 3495aa39..fb9ee698 100644
--- a/src/leap/bitmask/core/mail_services.py
+++ b/src/leap/bitmask/core/mail_services.py
@@ -64,10 +64,10 @@ class ImproperlyConfigured(Exception):
 
 class SoledadContainer(Container):
 
-    def __init__(self, basedir=DEFAULT_BASEDIR):
+    def __init__(self, service=None, basedir=DEFAULT_BASEDIR):
         self._basedir = os.path.expanduser(basedir)
         self._usermap = UserMap()
-        super(SoledadContainer, self).__init__()
+        super(SoledadContainer, self).__init__(service=service)
 
     def add_instance(self, userid, passphrase, uuid=None, token=None):
 
@@ -89,7 +89,7 @@ class SoledadContainer(Container):
             uuid, passphrase, soledad_path, soledad_url,
             cert_path, token)
 
-        self.add_instances(userid, soledad)
+        super(SoledadContainer, self).add_instance(userid, soledad)
 
         data = {'user': userid, 'uuid': uuid, 'token': token,
                 'soledad': soledad}
@@ -202,9 +202,9 @@ class SoledadService(HookableService):
 
 class KeymanagerContainer(Container):
 
-    def __init__(self, basedir):
+    def __init__(self, service=None, basedir=DEFAULT_BASEDIR):
         self._basedir = os.path.expanduser(basedir)
-        super(KeymanagerContainer, self).__init__()
+        super(KeymanagerContainer, self).__init__(service=service)
 
     def add_instance(self, userid, token, uuid, soledad):
 
diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py
index fca51048..99132c2d 100644
--- a/src/leap/bitmask/core/service.py
+++ b/src/leap/bitmask/core/service.py
@@ -25,19 +25,30 @@ from twisted.python import log
 
 from leap.bitmask import __version__
 from leap.bitmask.core import configurable
-from leap.bitmask.core import mail_services
 from leap.bitmask.core import _zmq
 from leap.bitmask.core import flags
-from leap.bonafide.service import BonafideService
 from leap.common.events import server as event_server
 # from leap.vpn import EIPService
 
 
+backend = flags.BACKEND
+
+if backend == 'default':
+    from leap.bitmask.core import mail_services
+    from leap.bonafide.service import BonafideService
+elif backend == 'dummy':
+    from leap.bitmask.core.dummy import mail_services
+    from leap.bitmask.core.dummy import BonafideService
+else:
+    raise RuntimeError('Backend not supported')
+
+
 class BitmaskBackend(configurable.ConfigurableService):
 
     def __init__(self, basedir=configurable.DEFAULT_BASEDIR):
 
         configurable.ConfigurableService.__init__(self, basedir)
+        self.core_commands = BackendCommands(self)
 
         def enabled(service):
             return self.get_config('services', service, False, boolean=True)
@@ -117,38 +128,19 @@ class BitmaskBackend(configurable.ConfigurableService):
             service.setServiceParent(self)
             return service
 
-    # General commands for the BitmaskBackend Core Service
-
     def do_stats(self):
-        log.msg('BitmaskCore Service STATS')
-        mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
-        return 'BitmaskCore: [Mem usage: %s KB]' % (mem / 1024)
+        return self.core_commands.do_stats()
 
     def do_status(self):
-        # we may want to make this tuple a class member
-        services = ('soledad', 'keymanager', 'mail', 'eip')
-
-        status = {}
-        for name in services:
-            _status = 'stopped'
-            try:
-                if self.getServiceNamed(name).running:
-                    _status = 'running'
-            except KeyError:
-                pass
-            status[name] = _status
-        status['backend'] = flags.BACKEND
-
-        return json.dumps(status)
+        return self.core_commands.do_status()
 
     def do_version(self):
-        version = __version__
-        return 'BitmaskCore: %s' % version
+        return self.core_commands.do_version()
 
     def do_shutdown(self):
-        self.stopService()
-        reactor.callLater(1, reactor.stop)
-        return 'shutting down...'
+        return self.core_commands.do_shutdown()
+
+    # Service Toggling
 
     def do_enable_service(self, service):
         assert service in self.service_names
@@ -175,3 +167,43 @@ class BitmaskBackend(configurable.ConfigurableService):
         # TODO -- should stop also?
         self.set_config('services', service, 'False')
         return 'ok'
+
+
+class BackendCommands(object):
+
+    """
+    General commands for the BitmaskBackend Core Service.
+    """
+
+    def __init__(self, core):
+        self.core = core
+
+    def do_status(self):
+        # we may want to make this tuple a class member
+        services = ('soledad', 'keymanager', 'mail', 'eip')
+
+        status = {}
+        for name in services:
+            _status = 'stopped'
+            try:
+                if self.core.getServiceNamed(name).running:
+                    _status = 'running'
+            except KeyError:
+                pass
+            status[name] = _status
+        status['backend'] = flags.BACKEND
+
+        return json.dumps(status)
+
+    def do_version(self):
+        return {'version_core': __version__}
+
+    def do_stats(self):
+        log.msg('BitmaskCore Service STATS')
+        mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
+        return {'mem_usage': '%s KB' % (mem / 1024)}
+
+    def do_shutdown(self):
+        self.core.stopService()
+        reactor.callLater(1, reactor.stop)
+        return {'shutdown': 'ok'}
-- 
cgit v1.2.3


From cfdd93937a7af65be73d5cc482686d1fe559d0a9 Mon Sep 17 00:00:00 2001
From: Kali Kaneko 
Date: Tue, 3 May 2016 10:18:06 -0400
Subject: [refactor] rename and move api_contract

---
 src/leap/bitmask/core/api_contract.py | 40 -----------------------------------
 1 file changed, 40 deletions(-)
 delete mode 100644 src/leap/bitmask/core/api_contract.py

(limited to 'src/leap/bitmask/core')

diff --git a/src/leap/bitmask/core/api_contract.py b/src/leap/bitmask/core/api_contract.py
deleted file mode 100644
index 86b600c1..00000000
--- a/src/leap/bitmask/core/api_contract.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# -*- coding: utf-8 -*-
-# api_contract.py
-# Copyright (C) 2016 LEAP Encryption Acess Project
-#
-# 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 .
-"""
-Display a human-readable representation of the methods that compound the public
-api for Bitmask Core.
-
-The values are meant to be type annotations.
-"""
-
-if __name__ == "__main__":
-    from leap.bitmask.core.service import BitmaskBackend
-    from leap.bitmask.core import api
-    backend = BitmaskBackend()
-
-    print '========= Bitmask Core API =================='
-    print
-
-    for key in api.registry:
-        human_key = key.replace('do_', '').lower()
-        value = api.registry[key]
-
-        print("{}:\t\t{}".format(
-            human_key,
-            ' '.join([x for x in value])))
-    print
-    print '============================================='
-- 
cgit v1.2.3