Unverified Commit f4503842 authored by Kali Kaneko's avatar Kali Kaneko
Browse files

[feature] landing of bitmask.core

parent 6fd1c73d
......@@ -15,7 +15,6 @@ ui_*.py
!.gitattributes
bin/
build/
core
dist/
docs/_build
docs/covhtml
......
APPNAME = "bitmask.core"
ENDPOINT = "ipc:///tmp/%s.sock" % APPNAME
# -*- 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')
# 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)
# -*- 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 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 <http://bugs.python.org/issue1443504>
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)
# -*- 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
# 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})
# -*- 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 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()
This diff is collapsed.
# -*- 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.