summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2015-11-18 19:27:47 -0400
committerKali Kaneko <kali@leap.se>2015-11-18 19:55:19 -0400
commite4e1c31e7d9742e7286cb43e168c9954de9bc5b3 (patch)
treed0c5e087fa22679706169cb13af61bc153f3de14
parentb2cfebaf3173d80386da2a234fdd864f2f883181 (diff)
[feat] bonafide zmq service
-rw-r--r--README.rst2
-rwxr-xr-xsrc/leap/bonafide/bonafide_cli2112
-rw-r--r--src/leap/bonafide/config.py152
-rw-r--r--src/leap/bonafide/cred_srp.py9
-rw-r--r--src/leap/bonafide/provider.py1
-rw-r--r--src/leap/bonafide/service.py120
-rw-r--r--src/leap/bonafide/zmq_service.py97
7 files changed, 487 insertions, 6 deletions
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 <http://www.gnu.org/licenses/>.
+"""
+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 <http://www.gnu.org/licenses/>.
+"""
+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 <http://www.gnu.org/licenses/>.
+"""
+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 <http://www.gnu.org/licenses/>.
+"""
+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()