From e4e1c31e7d9742e7286cb43e168c9954de9bc5b3 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Wed, 18 Nov 2015 19:27:47 -0400 Subject: [feat] bonafide zmq service --- README.rst | 2 +- src/leap/bonafide/bonafide_cli2 | 112 +++++++++++++++++++++++++++++ src/leap/bonafide/config.py | 152 +++++++++++++++++++++++++++++++++++++++ src/leap/bonafide/cred_srp.py | 9 ++- src/leap/bonafide/provider.py | 1 + src/leap/bonafide/service.py | 120 +++++++++++++++++++++++++++++++ src/leap/bonafide/zmq_service.py | 97 +++++++++++++++++++++++++ 7 files changed, 487 insertions(+), 6 deletions(-) create mode 100755 src/leap/bonafide/bonafide_cli2 create mode 100644 src/leap/bonafide/config.py create mode 100644 src/leap/bonafide/service.py create mode 100644 src/leap/bonafide/zmq_service.py diff --git a/README.rst b/README.rst index ffcf2ff..044ac71 100644 --- a/README.rst +++ b/README.rst @@ -17,4 +17,4 @@ the package in development mode by running:: from the parent folder. -Then you can use `bonafide_cli -h` to see the available commands. +Then you can use `bonafide_cli2 -h` to see the available commands. diff --git a/src/leap/bonafide/bonafide_cli2 b/src/leap/bonafide/bonafide_cli2 new file mode 100755 index 0000000..18506d9 --- /dev/null +++ b/src/leap/bonafide/bonafide_cli2 @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# bonafide_cli2.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 . +""" +Bonafide command line interface: zmq client. +""" +import sys +import getpass +import argparse + +from colorama import init as color_init +from colorama import Fore +from twisted.internet import reactor +from txzmq import ZmqEndpoint, ZmqFactory, ZmqREQConnection +import zmq + +from leap.bonafide import config + +description = (Fore.YELLOW + 'Manage and configure a LEAP Account ' + 'using the bonafide protocol. This client connects to ' + 'a running Bonafide service.' + Fore.RESET) + +parser = argparse.ArgumentParser(description=description) +parser.add_argument("--stats", dest="do_stats", action="store_true", + help="print service stats") +parser.add_argument("--signup", action="store_true", dest="do_signup", + help="signup new user") +parser.add_argument("--auth", dest="do_auth", action="store_true", + help="authenticate the passed user") +parser.add_argument("--logout", dest="do_logout", action="store_true", + help="logout this user") +parser.add_argument("--username", dest="username", + help="user to operate with") +parser.add_argument("--shutdown", dest="do_shutdown", action="store_true", + help="shutdown the bonafide service.") +ns = parser.parse_args() + + +def get_zmq_connection(): + zf = ZmqFactory() + e = ZmqEndpoint('connect', config.ENDPOINT) + return ZmqREQConnection(zf, e) + + +def error(msg): + print Fore.RED + "[!] %s" % msg + Fore.RESET + sys.exit(1) + +if len(sys.argv) < 2: + error("Too few arguments. Try %s --help" % sys.argv[0]) + + +if (ns.do_signup or ns.do_auth or ns.do_logout) and not ns.username: + error(Fore.RED + "Need to pass a username for signup/auth/logout" + + Fore.RESET) + +if ns.username and '@' not in ns.username: + error(Fore.RED + "Username must be in the form user@provider" + Fore.RESET) + + +def do_print(stuff): + print Fore.GREEN + stuff[0] + Fore.RESET + + +def send_command(): + + cb = do_print + if ns.do_shutdown: + data = ("shutdown",) + + elif ns.do_stats: + data = ("stats",) + + elif ns.do_signup: + passwd = getpass.getpass() + data = ("signup", ns.username, passwd) + + elif ns.do_auth: + passwd = getpass.getpass() + data = ("authenticate", ns.username, passwd) + + elif ns.do_logout: + passwd = getpass.getpass() + data = ("logout", ns.username, passwd) + + s = get_zmq_connection() + try: + d = s.sendMsg(*data) + except zmq.error.Again: + print Fore.RED + "[ERROR] Server is down :(" + Fore.RESET + d.addCallback(cb) + d.addCallback(lambda x: reactor.stop()) + + +if __name__ == "__main__": + color_init() + reactor.callWhenRunning(reactor.callLater, 0, send_command) + reactor.run() diff --git a/src/leap/bonafide/config.py b/src/leap/bonafide/config.py new file mode 100644 index 0000000..9490f55 --- /dev/null +++ b/src/leap/bonafide/config.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +# config.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 . +""" +Configuration for a LEAP provider. +""" +import datetime +import os +import sys + +from leap.bonafide._http import httpRequest + +from leap.common.check import leap_assert +from leap.common.config import get_path_prefix as common_get_path_prefix +from leap.common.files import check_and_fix_urw_only, get_mtime, mkdir_p + + +APPNAME = "bonafide" +ENDPOINT = "ipc:///tmp/%s.sock" % APPNAME + + +def get_path_prefix(standalone=False): + return common_get_path_prefix(standalone) + + +def get_provider_path(domain): + """ + Returns relative path for provider config. + + :param domain: the domain to which this providerconfig belongs to. + :type domain: str + :returns: the path + :rtype: str + """ + leap_assert(domain is not None, "get_provider_path: We need a domain") + return os.path.join("leap", "providers", domain, "provider.json") + + +def get_modification_ts(path): + """ + Gets modification time of a file. + + :param path: the path to get ts from + :type path: str + :returns: modification time + :rtype: datetime object + """ + ts = os.path.getmtime(path) + return datetime.datetime.fromtimestamp(ts) + + +def update_modification_ts(path): + """ + Sets modification time of a file to current time. + + :param path: the path to set ts to. + :type path: str + :returns: modification time + :rtype: datetime object + """ + os.utime(path, None) + return get_modification_ts(path) + + +def is_file(path): + """ + Returns True if the path exists and is a file. + """ + return os.path.isfile(path) + + +def is_empty_file(path): + """ + Returns True if the file at path is empty. + """ + return os.stat(path).st_size is 0 + + +def make_address(user, provider): + """ + Return a full identifier for an user, as a email-like + identifier. + + :param user: the username + :type user: basestring + :param provider: the provider domain + :type provider: basestring + """ + return "%s@%s" % (user, provider) + + +def get_username_and_provider(full_id): + return full_id.split('@') + + +class ProviderConfig(object): + # TODO add file config for enabled services + + def __init__(self, domain): + self._api_base = None + self._domain = domain + + def is_configured(self): + provider_json = self._get_provider_json_path() + # XXX check if all the services are there + if is_file(provider_json): + return True + return False + + def download_provider_info(self): + """ + Download the provider.json info from the main domain. + This SHOULD only be used once with the DOMAIN url. + """ + # TODO handle pre-seeded providers? + # or let client handle that? We could move them to bonafide. + provider_json = self._get_provider_json_path() + if is_file(provider_json): + raise RuntimeError('File already exists') + + def update_provider_info(self): + """ + Get more recent copy of provider.json from the api URL. + """ + pass + + def _http_request(self, *args, **kw): + # XXX pass if-modified-since header + return httpRequest(*args, **kw) + + def _get_provider_json_path(self): + domain = self._domain.encode(sys.getfilesystemencoding()) + provider_json = os.path.join(get_path_prefix(), get_provider_path(domain)) + return provider_json + +if __name__ == '__main__': + config = ProviderConfig('cdev.bitmask.net') + config.is_configured() + config.download_provider_info() diff --git a/src/leap/bonafide/cred_srp.py b/src/leap/bonafide/cred_srp.py index 5188830..9fcb97b 100644 --- a/src/leap/bonafide/cred_srp.py +++ b/src/leap/bonafide/cred_srp.py @@ -31,7 +31,7 @@ from twisted.cred import portal, credentials, error as credError from twisted.cred.checkers import ICredentialsChecker from twisted.internet import defer, reactor -from leap.bonafide.session import LeapSession +from leap.bonafide.session import Session @implementer(ICredentialsChecker) @@ -63,7 +63,7 @@ class SRPCredentialsChecker(object): self._check_srp_auth) def _check_srp_auth(session, username): - if session.authenticated: + if session.is_authenticated: # is ok! --- should add it to some global cache? return defer.succeed(username) else: @@ -72,9 +72,8 @@ class SRPCredentialsChecker(object): def _get_leap_session(credentials): - session = LeapSession(credentials) - d = session.handshake() - d.addCallback(lambda _: session.authenticate()) + session = Session(credentials) + d = session.authenticate() d.addCallback(lambda _: session) return d diff --git a/src/leap/bonafide/provider.py b/src/leap/bonafide/provider.py index c22be3c..1d0b5b7 100644 --- a/src/leap/bonafide/provider.py +++ b/src/leap/bonafide/provider.py @@ -93,6 +93,7 @@ class Api(object): # TODO when should the provider-api object be created? # TODO pass a Provider object to constructor, with autoconf flag. # TODO make the actions attribute depend on the api version + # TODO missing UPDATE USER RECORD __metaclass__ = _MetaActionDispatcher _actions = { diff --git a/src/leap/bonafide/service.py b/src/leap/bonafide/service.py new file mode 100644 index 0000000..1f7295a --- /dev/null +++ b/src/leap/bonafide/service.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# service.py +# Copyright (C) 2014 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Bonafide service. +""" +import os +import resource +from collections import defaultdict + +from leap.bonafide import config +from leap.bonafide import provider +from leap.bonafide.session import Session, OK + +from twisted.cred.credentials import UsernamePassword +from twisted.internet.defer import fail +from twisted.python import log + + +# TODO [ ] enable-disable services +# TODO [ ] read provider info + +COMMANDS = 'signup', 'authenticate', 'logout', 'stats' + + +class BonafideService(object): + """ + Expose the Bonafide Service API. + """ + + _apis = defaultdict(None) + _sessions = defaultdict(None) + + def _get_api(self, provider_id): + if provider_id in self._apis: + return self._apis[provider_id] + + # XXX lookup the provider config instead + # TODO defer the autoconfig for the provider if needed... + api = provider.Api('https://api.%s:4430' % provider_id) + self._apis[provider_id] = api + return api + + def _get_session(self, full_id, password=""): + if full_id in self._sessions: + return self._sessions[full_id] + + # TODO if password/username null, then pass AnonymousCreds + # TODO use twisted.cred instead + username, provider_id = config.get_username_and_provider(full_id) + credentials = UsernamePassword(username, password) + api = self._get_api(provider_id) + cdev_pem = os.path.expanduser( + '~/.config/leap/providers/%s/keys/ca/cacert.pem' % + provider_id) + session = Session(credentials, api, cdev_pem) + self._sessions[full_id] = session + return session + + # Service public methods + + def do_signup(self, full_id, password): + # XXX check it's unauthenticated + def return_user(result, _session): + return_code, user = result + if return_code == OK: + return user + + log.msg('SIGNUP for %s' % full_id) + session = self._get_session(full_id, password) + username, provider_id = config.get_username_and_provider(full_id) + + d = session.signup(username, password) + d.addCallback(return_user, session) + return d + + def do_authenticate(self, full_id, password): + def return_token(result, _session): + if result == OK: + return str(_session.token) + + log.msg('AUTH for %s' % full_id) + session = self._get_session(full_id, password) + d = session.authenticate() + d.addCallback(return_token, session) + return d + + def do_logout(self, full_id, password): + # XXX use the AVATAR here + log.msg('LOGOUT for %s' % full_id) + session = self._get_session(full_id) + if not session.is_authenticated: + return fail(RuntimeError("There is no session for such user")) + try: + d = session.logout() + except Exception as exc: + log.err(exc) + return fail(exc) + + d.addCallback(lambda _: self._sessions.pop(full_id)) + d.addCallback(lambda _: '%s logged out' % full_id) + return d + + def do_stats(self): + mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + return '[+] Bonafide service: [%s sessions] [Mem usage: %s KB]' % ( + len(self._sessions), mem / 1024) diff --git a/src/leap/bonafide/zmq_service.py b/src/leap/bonafide/zmq_service.py new file mode 100644 index 0000000..e33df3d --- /dev/null +++ b/src/leap/bonafide/zmq_service.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# zmq_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 . +""" +Bonafide ZMQ Service +""" +from leap.bonafide import config +from leap.bonafide.service import BonafideService, COMMANDS + +from txzmq import ZmqEndpoint, ZmqFactory, ZmqREPConnection + +from twisted.python import log + + +class BonafideZmqREPConnection(ZmqREPConnection): + + def initialize(self): + self._service = BonafideService() + + def do_greet(self): + print "[+] Bonafide service running..." + + def do_bye(self): + print "[+] Bonafide service stopped. Have a nice day." + reactor.stop() + + def gotMessage(self, msgId, *parts): + def defer_reply(response): + reactor.callLater(0, self.reply, msgId, str(response)) + + def log_err(failure): + log.err(failure) + print "FAILURE", failure + defer_reply("ERROR: %r" % failure) + + cmd = parts[0] + + if cmd == "shutdown": + defer_reply('ok, shutting down') + reactor.callLater(1, self.do_bye) + + if cmd not in COMMANDS: + response = 'INVALID COMMAND' + defer_reply(response) + + elif cmd == 'signup': + username, password = parts[1], parts[2] + d = self._service.do_signup(username, password) + d.addCallback(lambda response: defer_reply( + 'REGISTERED -> %s' % response)) + d.addErrback(log_err) + + elif cmd == 'authenticate': + username, password = parts[1], parts[2] + d = self._service.do_authenticate(username, password) + d.addCallback(lambda response: defer_reply( + 'TOKEN -> %s' % response)) + d.addErrback(log_err) + + elif cmd == 'logout': + username, password = parts[1], parts[2] + d = self._service.do_logout(username, password) + d.addCallback(lambda response: defer_reply( + 'LOGOUT -> ok')) + d.addErrback(log_err) + + elif cmd == 'stats': + response = self._service.do_stats() + defer_reply(response) + + +def get_zmq_connection(): + zf = ZmqFactory() + e = ZmqEndpoint("bind", config.ENDPOINT) + return BonafideZmqREPConnection(zf, e) + + +if __name__ == "__main__": + from twisted.internet import reactor + + s = get_zmq_connection() + reactor.callWhenRunning(s.initialize) + reactor.callWhenRunning(s.do_greet) + reactor.run() -- cgit v1.2.3