summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKali Kaneko (leap communications) <kali@leap.se>2016-08-30 10:53:32 -0400
committerKali Kaneko (leap communications) <kali@leap.se>2016-08-30 10:53:32 -0400
commit22eec36ff81ae2ec2b924087ed6253894b92278a (patch)
tree43d6fb8b41c55739fb18e3d005f601b2ecd254e8 /src
parenta039711adaf25cc43681fa89f49d9ef574f255ad (diff)
[pkg] initial migration of bitmask.{core,cli}
Diffstat (limited to 'src')
-rw-r--r--src/leap/bitmask/cli/__init__.py0
-rwxr-xr-xsrc/leap/bitmask/cli/bitmask_cli.py119
-rw-r--r--src/leap/bitmask/cli/command.py120
-rw-r--r--src/leap/bitmask/cli/eip.py37
-rw-r--r--src/leap/bitmask/cli/keys.py124
-rw-r--r--src/leap/bitmask/cli/mail.py40
-rw-r--r--src/leap/bitmask/cli/user.py76
-rw-r--r--src/leap/bitmask/core/__init__.py2
-rw-r--r--src/leap/bitmask/core/_zmq.py68
-rw-r--r--src/leap/bitmask/core/api.py54
-rw-r--r--src/leap/bitmask/core/bitmaskd.tac11
-rw-r--r--src/leap/bitmask/core/configurable.py106
-rw-r--r--src/leap/bitmask/core/dispatcher.py328
-rw-r--r--src/leap/bitmask/core/dummy.py80
-rw-r--r--src/leap/bitmask/core/flags.py1
-rw-r--r--src/leap/bitmask/core/launcher.py47
-rw-r--r--src/leap/bitmask/core/mail_services.py690
-rw-r--r--src/leap/bitmask/core/service.py209
-rw-r--r--src/leap/bitmask/core/uuid_map.py115
-rw-r--r--src/leap/bitmask/core/web/__init__.py0
-rw-r--r--src/leap/bitmask/core/web/index.html70
-rw-r--r--src/leap/bitmask/core/web/root.py0
-rw-r--r--src/leap/bitmask/core/websocket.py98
23 files changed, 2395 insertions, 0 deletions
diff --git a/src/leap/bitmask/cli/__init__.py b/src/leap/bitmask/cli/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/bitmask/cli/__init__.py
diff --git a/src/leap/bitmask/cli/bitmask_cli.py b/src/leap/bitmask/cli/bitmask_cli.py
new file mode 100755
index 0000000..1f104c9
--- /dev/null
+++ b/src/leap/bitmask/cli/bitmask_cli.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# bitmask_cli
+# 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 <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: zmq client.
+"""
+import json
+import sys
+
+from colorama import Fore
+from twisted.internet import reactor, defer
+
+from leap.bitmask.cli.eip import Eip
+from leap.bitmask.cli.keys import Keys
+from leap.bitmask.cli.mail import Mail
+from leap.bitmask.cli import command
+from leap.bitmask.cli.user import User
+
+
+class BitmaskCLI(command.Command):
+ usage = '''bitmaskctl <command> [<args>]
+
+Controls the Bitmask application.
+
+SERVICE COMMANDS:
+
+ user Handles Bitmask accounts
+ mail Bitmask Encrypted Mail
+ eip Encrypted Internet Proxy
+ keys Bitmask Keymanager
+
+GENERAL COMMANDS:
+
+ version prints version number and exit
+ launch launch the Bitmask backend daemon
+ shutdown shutdown Bitmask backend daemon
+ status displays general status about the running Bitmask services
+ stats show some debug info about bitmask-core
+ help show this help message
+
+'''
+ epilog = ("Use 'bitmaskctl <command> help' to learn more "
+ "about each command.")
+ commands = ['shutdown', 'stats']
+
+ def user(self, raw_args):
+ user = User()
+ return user.execute(raw_args)
+
+ def mail(self, raw_args):
+ mail = Mail()
+ return mail.execute(raw_args)
+
+ def eip(self, raw_args):
+ eip = Eip()
+ return eip.execute(raw_args)
+
+ def keys(self, raw_args):
+ keys = Keys()
+ return keys.execute(raw_args)
+
+ # Single commands
+
+ def launch(self, raw_args):
+ # XXX careful! Should see if the process in PID is running,
+ # avoid launching again.
+ import commands
+ commands.getoutput('bitmaskd')
+ return defer.succeed(None)
+
+ def version(self, raw_args):
+ print(Fore.GREEN + 'bitmaskctl: ' + Fore.RESET + '0.0.1')
+ self.data = ['version']
+ return self._send(printer=self._print_version)
+
+ def _print_version(self, version):
+ corever = version['version_core']
+ print(Fore.GREEN + 'bitmask_core: ' + Fore.RESET + corever)
+
+ def status(self, raw_args):
+ self.data = ['status']
+ return self._send(printer=self._print_status)
+
+ def _print_status(self, status):
+ statusdict = json.loads(status)
+ for key, value in statusdict.items():
+ color = Fore.GREEN
+ if value == 'stopped':
+ color = Fore.RED
+ print(key.ljust(10) + ': ' + color +
+ value + Fore.RESET)
+
+
+def execute():
+ cli = BitmaskCLI()
+ d = cli.execute(sys.argv[1:])
+ d.addCallback(lambda _: reactor.stop())
+
+
+def main():
+ reactor.callWhenRunning(reactor.callLater, 0, execute)
+ reactor.run()
+
+if __name__ == "__main__":
+ main()
diff --git a/src/leap/bitmask/cli/command.py b/src/leap/bitmask/cli/command.py
new file mode 100644
index 0000000..36552c0
--- /dev/null
+++ b/src/leap/bitmask/cli/command.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+# sender
+# Copyright (C) 2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: zmq sender.
+"""
+import argparse
+import json
+import sys
+
+from colorama import init as color_init
+from colorama import Fore
+from twisted.internet import defer
+from txzmq import ZmqEndpoint, ZmqEndpointType
+from txzmq import ZmqFactory, ZmqREQConnection
+from txzmq import ZmqRequestTimeoutError
+
+from leap.bitmask.core import ENDPOINT
+
+
+appname = 'bitmaskctl'
+
+
+def _print_result(result):
+ print Fore.GREEN + '%s' % result + Fore.RESET
+
+
+def default_dict_printer(result):
+ for key, value in result.items():
+ if value is None:
+ value = str(value)
+ print(Fore.RESET + key.ljust(10) + Fore.GREEN + value + Fore.RESET)
+
+
+class Command(object):
+ """A generic command dispatcher.
+ Any command in the class attribute `commands` will be dispached and
+ represented with a generic printer."""
+ service = ''
+ usage = '''{name} <subcommand>'''.format(name=appname)
+ epilog = ("Use bitmaskctl <subcommand> --help' to learn more "
+ "about each command.")
+ commands = []
+
+ def __init__(self):
+ color_init()
+ zf = ZmqFactory()
+ e = ZmqEndpoint(ZmqEndpointType.connect, ENDPOINT)
+ self._conn = ZmqREQConnection(zf, e)
+
+ self.data = []
+ if self.service:
+ self.data = [self.service]
+
+ def execute(self, raw_args):
+ self.parser = argparse.ArgumentParser(usage=self.usage,
+ epilog=self.epilog)
+ self.parser.add_argument('command', help='Subcommand to run')
+ try:
+ args = self.parser.parse_args(raw_args[0:1])
+ except SystemExit:
+ return defer.succeed(None)
+
+ # if command is in the default list, send the bare command
+ # and use the default printer
+ if args.command in self.commands:
+ self.data += [args.command]
+ return self._send(printer=default_dict_printer)
+
+ elif (args.command == 'execute' or
+ args.command.startswith('_') or
+ not hasattr(self, args.command)):
+ print 'Unrecognized command'
+ return self.help([])
+
+ try:
+ # use dispatch pattern to invoke method with same name
+ return getattr(self, args.command)(raw_args[1:])
+ except SystemExit:
+ return defer.succeed(None)
+
+ def help(self, raw_args):
+ self.parser.print_help()
+ return defer.succeed(None)
+
+ def _send(self, printer=_print_result):
+ d = self._conn.sendMsg(*self.data, timeout=60)
+ d.addCallback(self._check_err, printer)
+ d.addErrback(self._timeout_handler)
+ return d
+
+ def _error(self, msg):
+ print Fore.RED + "[!] %s" % msg + Fore.RESET
+ sys.exit(1)
+
+ def _check_err(self, stuff, printer):
+ obj = json.loads(stuff[0])
+ if not obj['error']:
+ return printer(obj['result'])
+ else:
+ print Fore.RED + 'ERROR:' + '%s' % obj['error'] + Fore.RESET
+
+ def _timeout_handler(self, failure):
+ # TODO ---- could try to launch the bitmask daemon here and retry
+ if failure.trap(ZmqRequestTimeoutError) == ZmqRequestTimeoutError:
+ print (Fore.RED + "[ERROR] Timeout contacting the bitmask daemon. "
+ "Is it running?" + Fore.RESET)
diff --git a/src/leap/bitmask/cli/eip.py b/src/leap/bitmask/cli/eip.py
new file mode 100644
index 0000000..eac8682
--- /dev/null
+++ b/src/leap/bitmask/cli/eip.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# eip
+# Copyright (C) 2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: eip
+"""
+from leap.bitmask.cli import command
+
+
+class Eip(command.Command):
+ service = 'eip'
+ usage = '''{name} eip <subcommand>
+
+Bitmask Encrypted Internet Service
+
+SUBCOMMANDS:
+
+ start Start service
+ stop Stop service
+ status Display status about service
+
+'''.format(name=command.appname)
+
+ commands = ['start', 'stop', 'status']
diff --git a/src/leap/bitmask/cli/keys.py b/src/leap/bitmask/cli/keys.py
new file mode 100644
index 0000000..d74c40a
--- /dev/null
+++ b/src/leap/bitmask/cli/keys.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+# keys
+# Copyright (C) 2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: keys
+"""
+import argparse
+import sys
+
+from colorama import Fore
+
+from leap.bitmask.cli import command
+from leap.keymanager.validation import ValidationLevels
+
+
+class Keys(command.Command):
+ service = 'keys'
+ usage = '''{name} keys <subcommand>
+
+Bitmask Keymanager management service
+
+SUBCOMMANDS:
+
+ list List all known keys
+ export Export a given key
+ insert Insert a key to the key storage
+ delete Delete a key from the key storage
+'''.format(name=command.appname)
+
+ def list(self, raw_args):
+ parser = argparse.ArgumentParser(
+ description='Bitmask list keys',
+ prog='%s %s %s' % tuple(sys.argv[:3]))
+ parser.add_argument('--private', action='store_true',
+ help='Use private keys (by default uses public)')
+ subargs = parser.parse_args(raw_args)
+
+ self.data += ['list']
+ if subargs.private:
+ self.data += ['private']
+ else:
+ self.data += ['public']
+
+ return self._send(self._print_key_list)
+
+ def export(self, raw_args):
+ parser = argparse.ArgumentParser(
+ description='Bitmask export key',
+ prog='%s %s %s' % tuple(sys.argv[:3]))
+ parser.add_argument('--private', action='store_true',
+ help='Use private keys (by default uses public)')
+ parser.add_argument('address', nargs=1,
+ help='email address of the key')
+ subargs = parser.parse_args(raw_args)
+ self.data += ['export', subargs.address[0]]
+
+ return self._send(self._print_key)
+
+ def insert(self, raw_args):
+ parser = argparse.ArgumentParser(
+ description='Bitmask import key',
+ prog='%s %s %s' % tuple(sys.argv[:3]))
+ parser.add_argument('--validation', choices=list(ValidationLevels),
+ default='Fingerprint',
+ help='Validation level for the key')
+ parser.add_argument('file', nargs=1,
+ help='file where the key is stored')
+ parser.add_argument('address', nargs=1,
+ help='email address of the key')
+ subargs = parser.parse_args(raw_args)
+
+ with open(subargs.file[0], 'r') as keyfile:
+ rawkey = keyfile.read()
+ self.data += ['insert', subargs.address[0], subargs.validation,
+ rawkey]
+
+ return self._send(self._print_key)
+
+ def delete(self, raw_args):
+ parser = argparse.ArgumentParser(
+ description='Bitmask delete key',
+ prog='%s %s %s' % tuple(sys.argv[:3]))
+ parser.add_argument('--private', action='store_true',
+ help='Use private keys (by default uses public)')
+ parser.add_argument('address', nargs=1,
+ help='email address of the key')
+ subargs = parser.parse_args(raw_args)
+ self.data += ['delete', subargs.address[0]]
+
+ return self._send()
+
+ def _print_key_list(self, keys):
+ for key in keys:
+ print(Fore.GREEN +
+ key["fingerprint"] + " " + key['address'] +
+ Fore.RESET)
+
+ def _print_key(self, key):
+ print(Fore.GREEN)
+ print("Uids: " + ', '.join(key['uids']))
+ print("Fingerprint:" + key['fingerprint'])
+ print("Length: " + str(key['length']))
+ print("Expiration: " + key['expiry_date'])
+ print("Validation: " + key['validation'])
+ print("Used: " + "sig:" +
+ str(key['sign_used']) + ", encr:" +
+ str(key['encr_used']))
+ print("Refreshed: " + key['refreshed_at'])
+ print(Fore.RESET)
+ print("")
+ print(key['key_data'])
diff --git a/src/leap/bitmask/cli/mail.py b/src/leap/bitmask/cli/mail.py
new file mode 100644
index 0000000..f0fa972
--- /dev/null
+++ b/src/leap/bitmask/cli/mail.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# mail
+# Copyright (C) 2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: mail
+"""
+from leap.bitmask.cli import command
+
+
+class Mail(command.Command):
+ service = 'mail'
+ usage = '''{name} mail <subcommand>
+
+Bitmask Encrypted Email Service
+
+SUBCOMMANDS:
+
+ enable Start service
+ disable Stop service
+ status Display status about service
+ get_token Returns token for the mail service
+ get_smtp_certificate Downloads a new smtp certificate
+
+'''.format(name=command.appname)
+
+ commands = ['enable', 'disable', 'status', 'get_token',
+ 'get_smtp_certificate']
diff --git a/src/leap/bitmask/cli/user.py b/src/leap/bitmask/cli/user.py
new file mode 100644
index 0000000..dccfc7d
--- /dev/null
+++ b/src/leap/bitmask/cli/user.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# user
+# Copyright (C) 2016 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Bitmask Command Line interface: user
+"""
+import argparse
+import getpass
+import sys
+
+from leap.bitmask.cli import command
+
+
+class User(command.Command):
+ service = 'user'
+ usage = '''{name} user <subcommand>
+
+Bitmask account service
+
+SUBCOMMANDS:
+
+ create Registers new user, if possible
+ auth Logs in against the provider
+ logout Ends any active session with the provider
+ active Shows the active user, if any
+
+'''.format(name=command.appname)
+
+ commands = ['active']
+
+ def create(self, raw_args):
+ username = self.username(raw_args)
+ passwd = getpass.getpass()
+ self.data += ['signup', username, passwd]
+ return self._send(printer=command.default_dict_printer)
+
+ def auth(self, raw_args):
+ username = self.username(raw_args)
+ passwd = getpass.getpass()
+ self.data += ['authenticate', username, passwd]
+ return self._send(printer=command.default_dict_printer)
+
+ def logout(self, raw_args):
+ username = self.username(raw_args)
+ self.data += ['logout', username]
+ return self._send(printer=command.default_dict_printer)
+
+ def username(self, raw_args):
+ args = tuple([command.appname] + sys.argv[1:3])
+ parser = argparse.ArgumentParser(
+ description='Bitmask user',
+ prog='%s %s %s' % args)
+ parser.add_argument('username', nargs=1,
+ help='username ID, in the form <user@example.org>')
+ subargs = parser.parse_args(raw_args)
+
+ username = subargs.username[0]
+ if not username:
+ self._error("Missing username ID but needed for this command")
+ if '@' not in username:
+ self._error("Username ID must be in the form <user@example.org>")
+
+ return username
diff --git a/src/leap/bitmask/core/__init__.py b/src/leap/bitmask/core/__init__.py
new file mode 100644
index 0000000..bda4b8d
--- /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 0000000..a656fc6
--- /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 <http://www.gnu.org/licenses/>.
+"""
+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/api.py b/src/leap/bitmask/core/api.py
new file mode 100644
index 0000000..9f3725d
--- /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 <http://www.gnu.org/licenses/>.
+"""
+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/bitmaskd.tac b/src/leap/bitmask/core/bitmaskd.tac
new file mode 100644
index 0000000..3c9b1d8
--- /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 0000000..8e33de9
--- /dev/null
+++ b/src/leap/bitmask/core/configurable.py
@@ -0,0 +1,106 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+"""
+Configurable Backend for Bitmask Service.
+"""
+import ConfigParser
+import os
+
+from twisted.application import service
+
+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):
+ """
+ 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=DEFAULT_BASEDIR):
+ 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 = self._get_config_path()
+ raise MissingConfigEntry("%s is missing the [%s]%s entry"
+ % 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)
+
+ with open(bitmaskd_cfg, "rb") as f:
+ self.config.readfp(f)
+
+ 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
+"""
diff --git a/src/leap/bitmask/core/dispatcher.py b/src/leap/bitmask/core/dispatcher.py
new file mode 100644
index 0000000..e81cad6
--- /dev/null
+++ b/src/leap/bitmask/core/dispatcher.py
@@ -0,0 +1,328 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+"""
+Command dispatcher.
+"""
+import json
+
+from twisted.internet import defer
+from twisted.python import failure, log
+
+from .api import APICommand, register_method
+
+
+class SubCommand(object):
+
+ __metaclass__ = APICommand
+
+ def dispatch(self, service, *parts, **kw):
+ subcmd = parts[1]
+
+ _method = getattr(self, 'do_' + subcmd.upper(), None)
+ if not _method:
+ raise RuntimeError('No such subcommand')
+ return defer.maybeDeferred(_method, service, *parts, **kw)
+
+
+class UserCmd(SubCommand):
+
+ label = 'user'
+
+ @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
+
+ @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
+
+ @register_method("{'logout': 'ok'}")
+ def do_LOGOUT(self, bonafide, *parts):
+ user = parts[2]
+ d = defer.maybeDeferred(bonafide.do_logout, user)
+ return d
+
+ @register_method('str')
+ def do_ACTIVE(self, bonafide, *parts):
+ d = defer.maybeDeferred(bonafide.do_get_active_user)
+ return d
+
+
+class EIPCmd(SubCommand):
+
+ label = 'eip'
+
+ @register_method('dict')
+ def do_ENABLE(self, service, *parts):
+ d = service.do_enable_service(self.label)
+ return d
+
+ @register_method('dict')
+ def do_DISABLE(self, service, *parts):
+ d = service.do_disable_service(self.label)
+ return d
+
+ @register_method('dict')
+ def do_STATUS(self, eip, *parts):
+ d = eip.do_status()
+ return d
+
+ @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
+
+ @register_method('dict')
+ def do_STOP(self, eip, *parts):
+ d = eip.do_stop()
+ return d
+
+
+class MailCmd(SubCommand):
+
+ label = 'mail'
+
+ @register_method('dict')
+ def do_ENABLE(self, service, *parts, **kw):
+ # FIXME -- service doesn't have this method
+ d = service.do_enable_service(self.label)
+ return d
+
+ @register_method('dict')
+ def do_DISABLE(self, service, *parts, **kw):
+ d = service.do_disable_service(self.label)
+ return d
+
+ @register_method('dict')
+ def do_STATUS(self, mail, *parts, **kw):
+ d = mail.do_status()
+ return d
+
+ @register_method('dict')
+ def do_GET_TOKEN(self, mail, *parts, **kw):
+ d = mail.get_token()
+ return d
+
+ @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
+
+
+class KeysCmd(SubCommand):
+
+ label = 'keys'
+
+ @register_method("[dict]")
+ def do_LIST(self, service, *parts, **kw):
+ private = False
+ if parts[-1] == 'private':
+ private = True
+
+ bonafide = kw['bonafide']
+ d = bonafide.do_get_active_user()
+ d.addCallback(service.do_list_keys, private)
+ return d
+
+ @register_method('dict')
+ def do_EXPORT(self, service, *parts, **kw):
+ if len(parts) < 3:
+ return defer.fail("An email address is needed")
+ address = parts[2]
+
+ private = False
+ if parts[-1] == 'private':
+ private = True
+
+ bonafide = kw['bonafide']
+ d = bonafide.do_get_active_user()
+ d.addCallback(service.do_export, address, private)
+ return d
+
+ @register_method('dict')
+ def do_INSERT(self, service, *parts, **kw):
+ if len(parts) < 5:
+ return defer.fail("An email address is needed")
+ address = parts[2]
+ validation = parts[3]
+ rawkey = parts[4]
+
+ bonafide = kw['bonafide']
+ d = bonafide.do_get_active_user()
+ d.addCallback(service.do_insert, address, rawkey, validation)
+ return d
+
+ @register_method('str')
+ def do_DELETE(self, service, *parts, **kw):
+ if len(parts) < 3:
+ return defer.fail("An email address is needed")
+ address = parts[2]
+
+ private = False
+ if parts[-1] == 'private':
+ private = True
+
+ bonafide = kw['bonafide']
+ d = bonafide.do_get_active_user()
+ d.addCallback(service.do_delete, address, private)
+ return d
+
+
+class CommandDispatcher(object):
+
+ __metaclass__ = APICommand
+
+ label = 'core'
+
+ def __init__(self, core):
+
+ self.core = core
+ self.subcommand_user = UserCmd()
+ self.subcommand_eip = EIPCmd()
+ self.subcommand_mail = MailCmd()
+ self.subcommand_keys = KeysCmd()
+
+ # 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]
+
+ dispatch = self._subcommand_eip.dispatch
+ if subcmd in ('enable', 'disable'):
+ d = dispatch(self.core, *parts)
+ else:
+ d = dispatch(eip, *parts)
+
+ d.addCallbacks(_format_result, _format_error)
+ return d
+
+ def do_MAIL(self, *parts):
+ subcmd = parts[1]
+ dispatch = self.subcommand_mail.dispatch
+
+ if subcmd == 'enable':
+ d = dispatch(self.core, *parts)
+
+ mail = self._get_service(self.subcommand_mail.label)
+ bonafide = self._get_service('bonafide')
+ kw = {'bonafide': bonafide}
+
+ if not mail:
+ return _format_result('mail: disabled')
+
+ if subcmd == 'disable':
+ d = dispatch(self.core)
+ else:
+ d = dispatch(mail, *parts, **kw)
+
+ d.addCallbacks(_format_result, _format_error)
+ return d
+
+ def do_KEYS(self, *parts):
+ dispatch = self.subcommand_keys.dispatch
+
+ keymanager_label = 'keymanager'
+ keymanager = self._get_service(keymanager_label)
+ bonafide = self._get_service('bonafide')
+ kw = {'bonafide': bonafide}
+
+ if not keymanager:
+ return _format_result('keymanager: disabled')
+
+ d = dispatch(keymanager, *parts, **kw)
+ 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})
+
+
+def _format_error(failure):
+ log.err(failure)
+ return json.dumps({'error': failure.value.message, 'result': None})
diff --git a/src/leap/bitmask/core/dummy.py b/src/leap/bitmask/core/dummy.py
new file mode 100644
index 0000000..7b6be39
--- /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 <http://www.gnu.org/licenses/>.
+"""
+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):
+ return {'logout': 'ok'}
+
+ def do_get_active_user(self):
+ return 'dummyuser@provider.example.org'
diff --git a/src/leap/bitmask/core/flags.py b/src/leap/bitmask/core/flags.py
new file mode 100644
index 0000000..9a40c70
--- /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
new file mode 100644
index 0000000..b8916a1
--- /dev/null
+++ b/src/leap/bitmask/core/launcher.py
@@ -0,0 +1,47 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+"""
+Run bitmask daemon.
+"""
+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',
+ '--logfile', '/tmp/bitmaskd.log',
+ '--umask=0022',
+ ]
+ print '[+] launching bitmaskd...'
+ run()
+
+
+if __name__ == "__main__":
+ run_bitmaskd()
diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py
new file mode 100644
index 0000000..68cb444
--- /dev/null
+++ b/src/leap/bitmask/core/mail_services.py
@@ -0,0 +1,690 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+"""
+Mail services.
+
+This is quite moving work still.
+This should be moved to the different packages when it stabilizes.
+"""
+import json
+import os
+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
+from leap.keymanager.errors import KeyNotFound
+from leap.keymanager.validation import ValidationLevels
+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
+from leap.bitmask.core.configurable import DEFAULT_BASEDIR
+
+
+class Container(object):
+
+ 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
+
+
+class SoledadContainer(Container):
+
+ def __init__(self, service=None, basedir=DEFAULT_BASEDIR):
+ self._basedir = os.path.expanduser(basedir)
+ self._usermap = UserMap()
+ super(SoledadContainer, self).__init__(service=service)
+
+ 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)
+
+ super(SoledadContainer, self).add_instance(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, soledad_path,
+ server_url, cert_file, token):
+ # setup soledad info
+ 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
+ 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(HookableService):
+
+ def __init__(self, basedir):
+ service.Service.__init__(self)
+ self._basedir = basedir
+
+ def startService(self):
+ log.msg('Starting Soledad Service')
+ self._container = SoledadContainer(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, service=None, basedir=DEFAULT_BASEDIR):
+ self._basedir = os.path.expanduser(basedir)
+ super(KeymanagerContainer, self).__init__(service=service)
+
+ 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
+ super(KeymanagerContainer, self).add_instance(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()
+ 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()
+ 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, 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)
+
+ # 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",
+ "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(HookableService):
+
+ def __init__(self, basedir=DEFAULT_BASEDIR):
+ 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... %s ' % token)
+ self.tokens[userid] = token
+
+ # commands
+
+ def do_list_keys(self, userid, private=False):
+ km = self._container.get_instance(userid)
+ d = km.get_all_keys(private=private)
+ d.addCallback(lambda keys: [dict(key) for key in keys])
+ return d
+
+ def do_export(self, userid, address, private=False):
+ km = self._container.get_instance(userid)
+ d = km.get_key(address, private=private, fetch_remote=False)
+ d.addCallback(lambda key: dict(key))
+ return d
+
+ def do_insert(self, userid, address, rawkey, validation='Fingerprint'):
+ km = self._container.get_instance(userid)
+ validation = ValidationLevels.get(validation)
+ d = km.put_raw_key(rawkey, address, validation=validation)
+ d.addCallback(lambda _: km.get_key(address, fetch_remote=False))
+ d.addCallback(lambda key: dict(key))
+ return d
+
+ @defer.inlineCallbacks
+ def do_delete(self, userid, address, private=False):
+ km = self._container.get_instance(userid)
+ key = yield km.get_key(address, private=private, fetch_remote=False)
+ km.delete_key(key)
+ defer.returnValue(key.fingerprint)
+
+
+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._service_tokens = {}
+ self._active_user = None
+ super(StandardMailService, self).__init__()
+ self.initializeChildrenServices()
+
+ def initializeChildrenServices(self):
+ self.addService(IMAPService(self._soledad_sessions))
+ 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...')
+ 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 registerToken(token):
+ self._service_tokens[userid] = token
+ self._active_user = userid
+
+ d = soledad.get_or_create_service_token('mail_auth')
+ d.addCallback(registerToken)
+ 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):
+ status = 'running' if self.running else 'disabled'
+ return {'mail': status}
+
+ def get_token(self):
+ active_user = self._active_user
+ if not active_user:
+ return defer.succeed({'user': None})
+ token = self._service_tokens.get(active_user)
+ return defer.succeed({'user': active_user, 'token': 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=DEFAULT_BASEDIR):
+
+ 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, userid)
+ 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 <describe>.
+ 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 0000000..99132c2
--- /dev/null
+++ b/src/leap/bitmask/core/service.py
@@ -0,0 +1,209 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+"""
+Bitmask-core Service.
+"""
+import json
+import resource
+
+from twisted.internet import reactor
+from twisted.python import log
+
+from leap.bitmask import __version__
+from leap.bitmask.core import configurable
+from leap.bitmask.core import _zmq
+from leap.bitmask.core import flags
+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)
+
+ 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):
+ # FIXME -- land EIP into leap.vpn
+ pass
+ # self._maybe_start_service('eip', EIPService)
+
+ def init_zmq(self):
+ zs = _zmq.ZMQServerService(self)
+ zs.setServiceParent(self)
+
+ def init_web(self):
+ from leap.bitmask.core import websocket
+ 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
+
+ def do_stats(self):
+ return self.core_commands.do_stats()
+
+ def do_status(self):
+ return self.core_commands.do_status()
+
+ def do_version(self):
+ return self.core_commands.do_version()
+
+ def do_shutdown(self):
+ return self.core_commands.do_shutdown()
+
+ # Service Toggling
+
+ 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'
+
+
+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'}
diff --git a/src/leap/bitmask/core/uuid_map.py b/src/leap/bitmask/core/uuid_map.py
new file mode 100644
index 0000000..5edc721
--- /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 <http://www.gnu.org/licenses/>.
+"""
+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 0000000..e69de29
--- /dev/null
+++ b/src/leap/bitmask/core/web/__init__.py
diff --git a/src/leap/bitmask/core/web/index.html b/src/leap/bitmask/core/web/index.html
new file mode 100644
index 0000000..9490eca
--- /dev/null
+++ b/src/leap/bitmask/core/web/index.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Bitmask WebSockets Endpoint</title>
+ <script type="text/javascript">
+ var sock = null;
+ var ellog = null;
+
+ window.onload = function() {
+
+ ellog = document.getElementById('log');
+
+ var wsuri;
+ if (window.location.protocol === "file:") {
+ wsuri = "ws://127.0.0.1:8080/ws";
+ } else {
+ wsuri = "ws://" + window.location.hostname + ":8080/bitmask";
+ }
+
+ if ("WebSocket" in window) {
+ sock = new WebSocket(wsuri);
+ } else if ("MozWebSocket" in window) {
+ sock = new MozWebSocket(wsuri);
+ } else {
+ log("Browser does not support WebSocket!");
+ window.location = "http://autobahn.ws/unsupportedbrowser";
+ }
+
+ if (sock) {
+ sock.onopen = function() {
+ log("Connected to " + wsuri);
+ }
+
+ sock.onclose = function(e) {
+ log("Connection closed (wasClean = " + e.wasClean + ", code = " + e.code + ", reason = '" + e.reason + "')");
+ sock = null;
+ }
+
+ sock.onmessage = function(e) {
+ log("[res] " + e.data + '\n');
+ }
+ }
+ };
+
+ function send() {
+ var msg = document.getElementById('message').value;
+ if (sock) {
+ sock.send(msg);
+ log("[cmd] " + msg);
+ } else {
+ log("Not connected.");
+ }
+ };
+
+ function log(m) {
+ ellog.innerHTML += m + '\n';
+ ellog.scrollTop = ellog.scrollHeight;
+ };
+ </script>
+ </head>
+ <body>
+ <h1>Bitmask Control Panel</h1>
+ <noscript>You must enable JavaScript</noscript>
+ <form>
+ <p>Command: <input id="message" type="text" size="50" maxlength="50" value="status"></p>
+ </form>
+ <button onclick='send();'>Send Command</button>
+ <pre id="log" style="height: 20em; overflow-y: scroll; background-color: #faa;"></pre>
+ </body>
+</html>
diff --git a/src/leap/bitmask/core/web/root.py b/src/leap/bitmask/core/web/root.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/leap/bitmask/core/web/root.py
diff --git a/src/leap/bitmask/core/websocket.py b/src/leap/bitmask/core/websocket.py
new file mode 100644
index 0000000..5569c6c
--- /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 <http://www.gnu.org/licenses/>.
+
+"""
+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)