diff options
Diffstat (limited to 'bonafide/src')
-rw-r--r-- | bonafide/src/leap/__init__.py | 6 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/__init__.py | 4 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/_http.py | 95 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/_protocol.py | 174 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/_srp.py | 147 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/_version.py | 460 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/bootstrap.py | 0 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/config.py | 508 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/cred_srp.py | 157 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/provider.py | 186 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/service.py | 120 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/services/TODO | 1 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/services/__init__.py | 0 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/services/eip.py | 0 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/services/mail.py | 0 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/services/soledad.py | 0 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/session.py | 219 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/ssh_service.py | 2 | ||||
-rw-r--r-- | bonafide/src/leap/bonafide/tests/__init__.py | 0 |
19 files changed, 0 insertions, 2079 deletions
diff --git a/bonafide/src/leap/__init__.py b/bonafide/src/leap/__init__.py deleted file mode 100644 index f48ad10..0000000 --- a/bonafide/src/leap/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages -try: - __import__('pkg_resources').declare_namespace(__name__) -except ImportError: - from pkgutil import extend_path - __path__ = extend_path(__path__, __name__) diff --git a/bonafide/src/leap/bonafide/__init__.py b/bonafide/src/leap/bonafide/__init__.py deleted file mode 100644 index 74f4e66..0000000 --- a/bonafide/src/leap/bonafide/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ - -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions diff --git a/bonafide/src/leap/bonafide/_http.py b/bonafide/src/leap/bonafide/_http.py deleted file mode 100644 index 8f05b42..0000000 --- a/bonafide/src/leap/bonafide/_http.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -# _http.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/>. - -""" -twisted.web utils for bonafide. -""" -import base64 -import cookielib -import urllib - -from twisted.internet import defer, protocol, reactor -from twisted.internet.ssl import Certificate -from twisted.python.filepath import FilePath -from twisted.web.client import Agent, CookieAgent -from twisted.web.client import BrowserLikePolicyForHTTPS -from twisted.web.http_headers import Headers -from twisted.web.iweb import IBodyProducer -from zope.interface import implements - - -def cookieAgentFactory(verify_path, connectTimeout=30): - customPolicy = BrowserLikePolicyForHTTPS( - Certificate.loadPEM(FilePath(verify_path).getContent())) - agent = Agent(reactor, customPolicy, connectTimeout=connectTimeout) - cookiejar = cookielib.CookieJar() - return CookieAgent(agent, cookiejar) - - -def httpRequest(agent, url, values={}, headers={}, method='POST', token=None): - data = '' - if values: - data = urllib.urlencode(values) - headers['Content-Type'] = ['application/x-www-form-urlencoded'] - - if token: - headers['Authorization'] = ['Token token="%s"' % (bytes(token))] - - def handle_response(response): - # print "RESPONSE CODE", response.code - if response.code == 204: - d = defer.succeed('') - else: - class SimpleReceiver(protocol.Protocol): - def __init__(s, d): - s.buf = '' - s.d = d - - def dataReceived(s, data): - s.buf += data - - def connectionLost(s, reason): - # TODO: test if reason is twisted.web.client.ResponseDone, - # if not, do an errback - s.d.callback(s.buf) - d = defer.Deferred() - response.deliverBody(SimpleReceiver(d)) - return d - - d = agent.request(method, url, Headers(headers), - StringProducer(data) if data else None) - d.addCallback(handle_response) - return d - - -class StringProducer(object): - - implements(IBodyProducer) - - def __init__(self, body): - self.body = body - self.length = len(body) - - def startProducing(self, consumer): - consumer.write(self.body) - return defer.succeed(None) - - def pauseProducing(self): - pass - - def stopProducing(self): - pass diff --git a/bonafide/src/leap/bonafide/_protocol.py b/bonafide/src/leap/bonafide/_protocol.py deleted file mode 100644 index 726185e..0000000 --- a/bonafide/src/leap/bonafide/_protocol.py +++ /dev/null @@ -1,174 +0,0 @@ -# -*- coding: utf-8 -*- -# _protocol.py -# Copyright (C) 2014-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 protocol. -""" -import os -import resource -from collections import defaultdict - -from leap.bonafide import config -from leap.bonafide.provider import Api -from leap.bonafide.session import Session, OK -from leap.common.config import get_path_prefix - -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' -_preffix = get_path_prefix() - - -class BonafideProtocol(object): - """ - Expose the protocol that interacts with the Bonafide Service API. - """ - - _apis = defaultdict(None) - _sessions = defaultdict(None) - - def _get_api(self, provider): - # TODO should get deferred - if provider.domain in self._apis: - return self._apis[provider.domain] - - # TODO defer the autoconfig for the provider if needed... - api = Api(provider.api_uri, provider.version) - self._apis[provider.domain] = api - return api - - def _get_session(self, provider, 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) - provider_pem = _get_provider_ca_path(provider_id) - session = Session(credentials, api, provider_pem) - self._sessions[full_id] = session - return session - - def _del_session_errback(self, failure, full_id): - if full_id in self._sessions: - del self._sessions[full_id] - return failure - - # Service public methods - - def do_signup(self, full_id, password): - log.msg('SIGNUP for %s' % full_id) - _, provider_id = config.get_username_and_provider(full_id) - - provider = config.Provider(provider_id) - d = provider.callWhenReady( - self._do_signup, provider, full_id, password) - return d - - def _do_signup(self, provider, full_id, password): - - # XXX check it's unauthenticated - def return_user(result, _session): - return_code, user = result - if return_code == OK: - return user - - username, _ = config.get_username_and_provider(full_id) - # XXX get deferred? - session = self._get_session(provider, full_id, password) - d = session.signup(username, password) - d.addCallback(return_user, session) - d.addErrback(self._del_session_errback, full_id) - return d - - def do_authenticate(self, full_id, password): - _, provider_id = config.get_username_and_provider(full_id) - - provider = config.Provider(provider_id) - - def maybe_finish_provider_bootstrap(result, provider): - session = self._get_session(provider, full_id, password) - d = provider.download_services_config_with_auth(session) - d.addCallback(lambda _: result) - return d - - d = provider.callWhenReady( - self._do_authenticate, provider, full_id, password) - d.addCallback(maybe_finish_provider_bootstrap, provider) - return d - - def _do_authenticate(self, provider, full_id, password): - - def return_token_and_uuid(result, _session): - if result == OK: - # TODO -- turn this into JSON response - return str(_session.token), str(_session.uuid) - - log.msg('AUTH for %s' % full_id) - - # XXX get deferred? - session = self._get_session(provider, full_id, password) - d = session.authenticate() - d.addCallback(return_token_and_uuid, session) - d.addErrback(self._del_session_errback, full_id) - return d - - def do_logout(self, full_id): - # XXX use the AVATAR here - log.msg('LOGOUT for %s' % full_id) - if (full_id not in self._sessions or - not self._sessions[full_id].is_authenticated): - return fail(RuntimeError("There is no session for such user")) - session = self._sessions[full_id] - - d = session.logout() - d.addCallback(lambda _: self._sessions.pop(full_id)) - d.addCallback(lambda _: '%s logged out' % full_id) - return d - - def do_get_smtp_cert(self, full_id): - if (full_id not in self._sessions or - not self._sessions[full_id].is_authenticated): - return fail(RuntimeError("There is no session for such user")) - d = self._sessions[full_id].get_smtp_cert() - return d - - def do_get_vpn_cert(self): - # FIXME to be implemented - pass - - def do_update_user(self): - # FIXME to be implemented - pass - - def do_stats(self): - log.msg('Calculating Bonafide Service STATS') - mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss - return {'sessions': len(self._sessions), - 'mem': '%s KB' % (mem / 1024)} - - -def _get_provider_ca_path(provider_id): - return os.path.join( - _preffix, 'leap', 'providers', provider_id, 'keys', 'ca', 'cacert.pem') diff --git a/bonafide/src/leap/bonafide/_srp.py b/bonafide/src/leap/bonafide/_srp.py deleted file mode 100644 index 38f657b..0000000 --- a/bonafide/src/leap/bonafide/_srp.py +++ /dev/null @@ -1,147 +0,0 @@ -# -*- coding: utf-8 -*- -# _srp.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/>. - -""" -SRP Authentication. -""" - -import binascii -import json - -import srp - - -class SRPAuthMechanism(object): - - """ - Implement a protocol-agnostic SRP Authentication mechanism. - """ - - def __init__(self, username, password): - self.username = username - self.srp_user = srp.User(username, password, - srp.SHA256, srp.NG_1024) - _, A = self.srp_user.start_authentication() - self.A = A - self.M = None - self.M2 = None - - def get_handshake_params(self): - return {'login': bytes(self.username), - 'A': binascii.hexlify(self.A)} - - def process_handshake(self, handshake_response): - challenge = json.loads(handshake_response) - self._check_for_errors(challenge) - salt = challenge.get('salt', None) - B = challenge.get('B', None) - unhex_salt, unhex_B = self._unhex_salt_B(salt, B) - self.M = self.srp_user.process_challenge(unhex_salt, unhex_B) - - def get_authentication_params(self): - # It looks A is not used server side - return {'client_auth': binascii.hexlify(self.M), - 'A': binascii.hexlify(self.A)} - - def process_authentication(self, authentication_response): - auth = json.loads(authentication_response) - self._check_for_errors(auth) - uuid = auth.get('id', None) - token = auth.get('token', None) - self.M2 = auth.get('M2', None) - self._check_auth_params(uuid, token, self.M2) - return uuid, token - - def verify_authentication(self): - unhex_M2 = _safe_unhexlify(self.M2) - self.srp_user.verify_session(unhex_M2) - assert self.srp_user.authenticated() - - def _check_for_errors(self, response): - if 'errors' in response: - msg = response['errors']['base'] - raise SRPAuthError(unicode(msg).encode('utf-8')) - - def _unhex_salt_B(self, salt, B): - if salt is None: - raise SRPAuthNoSalt() - if B is None: - raise SRPAuthNoB() - try: - unhex_salt = _safe_unhexlify(salt) - unhex_B = _safe_unhexlify(B) - except (TypeError, ValueError) as e: - raise SRPAuthBadDataFromServer(str(e)) - return unhex_salt, unhex_B - - def _check_auth_params(self, uuid, token, M2): - if not all((uuid, token, M2)): - msg = '%s' % str((M2, uuid, token)) - raise SRPAuthBadDataFromServer(msg) - - -class SRPSignupMechanism(object): - - """ - Implement a protocol-agnostic SRP Registration mechanism. - """ - - def get_signup_params(self, username, password): - salt, verifier = srp.create_salted_verification_key( - bytes(username), bytes(password), - srp.SHA256, srp.NG_1024) - user_data = { - 'user[login]': username, - 'user[password_salt]': binascii.hexlify(salt), - 'user[password_verifier]': binascii.hexlify(verifier)} - return user_data - - def process_signup(self, signup_response): - signup = json.loads(signup_response) - errors = signup.get('errors') - if errors: - msg = 'username ' + errors.get('login')[0] - raise SRPRegistrationError(msg) - else: - username = signup.get('login') - return username - - -def _safe_unhexlify(val): - return binascii.unhexlify(val) \ - if (len(val) % 2 == 0) else binascii.unhexlify('0' + val) - - -class SRPAuthError(Exception): - """ - Base exception for srp authentication errors - """ - - -class SRPAuthNoSalt(SRPAuthError): - message = 'The server didn\'t send the salt parameter' - - -class SRPAuthNoB(SRPAuthError): - message = 'The server didn\'t send the B parameter' - - -class SRPAuthBadDataFromServer(SRPAuthError): - pass - -class SRPRegistrationError(Exception): - pass diff --git a/bonafide/src/leap/bonafide/_version.py b/bonafide/src/leap/bonafide/_version.py deleted file mode 100644 index 91fb65c..0000000 --- a/bonafide/src/leap/bonafide/_version.py +++ /dev/null @@ -1,460 +0,0 @@ - -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.15 (https://github.com/warner/python-versioneer) - -import errno -import os -import re -import subprocess -import sys - - -def get_keywords(): - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "$Format:%d$" - git_full = "$Format:%H$" - keywords = {"refnames": git_refnames, "full": git_full} - return keywords - - -class VersioneerConfig: - pass - - -def get_config(): - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "pep440" - cfg.tag_prefix = "None" - cfg.parentdir_prefix = "None" - cfg.versionfile_source = "src/leap/bonafide/_version.py" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - pass - - -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - def decorate(f): - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - return None - return stdout - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - # Source tarballs conventionally unpack into a directory that includes - # both the project name and a version string. - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%s', but '%s' doesn't start with " - "prefix '%s'" % (root, dirname, parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None} - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - if not keywords: - raise NotThisMethod("no keywords at all, weird") - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%s', no digits" % ",".join(refs-tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None - } - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags"} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - # this runs 'git' from the root of the source tree. This only gets called - # if the git-archive 'subst' keywords were *not* expanded, and - # _version.py hasn't already been rewritten with a short version string, - # meaning we're inside a checked out source tree. - - if not os.path.exists(os.path.join(root, ".git")): - if verbose: - print("no .git in %s" % root) - raise NotThisMethod("no .git directory") - - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - # if there is a tag, this yields TAG-NUM-gHEX[-dirty] - # if there are no tags, this yields HEX[-dirty] (no NUM) - describe_out = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long"], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - return pieces - - -def plus_or_dot(pieces): - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - # now build up version string, with post-release "local version - # identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - # get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - # exceptions: - # 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - # TAG[.post.devDISTANCE] . No -dirty - - # exceptions: - # 1: no tags. 0.post.devDISTANCE - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - # TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that - # .dev0 sorts backwards (a dirty tree will appear "older" than the - # corresponding clean one), but you shouldn't be releasing software with - # -dirty anyways. - - # exceptions: - # 1: no tags. 0.postDISTANCE[.dev0] - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_old(pieces): - # TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - - # exceptions: - # 1: no tags. 0.postDISTANCE[.dev0] - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - # TAG[-DISTANCE-gHEX][-dirty], like 'git describe --tags --dirty - # --always' - - # exceptions: - # 1: no tags. HEX[-dirty] (note: no 'g' prefix) - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - # TAG-DISTANCE-gHEX[-dirty], like 'git describe --tags --dirty - # --always -long'. The distance/hash is unconditional. - - # exceptions: - # 1: no tags. HEX[-dirty] (note: no 'g' prefix) - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"]} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None} - - -def get_versions(): - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree"} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version"} diff --git a/bonafide/src/leap/bonafide/bootstrap.py b/bonafide/src/leap/bonafide/bootstrap.py deleted file mode 100644 index e69de29..0000000 --- a/bonafide/src/leap/bonafide/bootstrap.py +++ /dev/null diff --git a/bonafide/src/leap/bonafide/config.py b/bonafide/src/leap/bonafide/config.py deleted file mode 100644 index ae66a0e..0000000 --- a/bonafide/src/leap/bonafide/config.py +++ /dev/null @@ -1,508 +0,0 @@ -# -*- 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 json -import os -import sys - -from collections import defaultdict -from urlparse import urlparse - -from twisted.internet import defer, reactor -from twisted.internet.ssl import ClientContextFactory -from twisted.python import log -from twisted.web.client import Agent, downloadPage - -from leap.bonafide._http import httpRequest -from leap.bonafide.provider import Discovery - -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 mkdir_p -# check_and_fix_urw_only, get_mtime - - -APPNAME = "bonafide" -ENDPOINT = "ipc:///tmp/%s.sock" % APPNAME - - -def get_path_prefix(standalone=False): - return common_get_path_prefix(standalone) - - -_preffix = get_path_prefix() - - -def get_provider_path(domain, config='provider.json'): - """ - Returns relative path for provider configs. - - :param domain: the domain to which this providerconfig belongs to. - :type domain: str - :returns: the path - :rtype: str - """ - # TODO sanitize domain - leap_assert(domain is not None, 'get_provider_path: We need a domain') - return os.path.join('providers', domain, config) - - -def get_ca_cert_path(domain): - # TODO sanitize domain - leap_assert(domain is not None, 'get_provider_path: We need a domain') - return os.path.join('providers', domain, 'keys', 'ca', 'cacert.pem') - - -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 Provider(object): - # TODO add validation - - SERVICES_MAP = { - 'openvpn': ['eip'], - 'mx': ['soledad', 'smtp']} - - first_bootstrap = defaultdict(None) - ongoing_bootstrap = defaultdict(None) - stuck_bootstrap = defaultdict(None) - - def __init__(self, domain, autoconf=True, basedir=None, - check_certificate=True): - if not basedir: - basedir = os.path.join(_preffix, 'leap') - self._basedir = os.path.expanduser(basedir) - self._domain = domain - self._disco = Discovery('https://%s' % domain) - self._provider_config = None - - is_configured = self.is_configured() - if not is_configured: - check_certificate = False - - if check_certificate: - self.contextFactory = None - else: - # XXX we should do this only for the FIRST provider download. - # For the rest, we should pass the ca cert to the agent. - # That means that RIGHT AFTER DOWNLOADING provider_info, - # we should instantiate a new Agent... - self.contextFactory = WebClientContextFactory() - self._agent = Agent(reactor, self.contextFactory) - - self._load_provider_json() - - if not is_configured and autoconf: - log.msg('provider %s not configured: downloading files...' % - domain) - self.bootstrap() - else: - log.msg('Provider already initialized') - self.first_bootstrap[self._domain] = defer.succeed( - 'already_initialized') - self.ongoing_bootstrap[self._domain] = defer.succeed( - 'already_initialized') - - @property - def domain(self): - return self._domain - - @property - def api_uri(self): - if not self._provider_config: - return 'https://api.%s:4430' % self._domain - return self._provider_config.api_uri - - @property - def version(self): - if not self._provider_config: - return 1 - return int(self._provider_config.api_version) - - def is_configured(self): - provider_json = self._get_provider_json_path() - # XXX check if all the services are there - if not is_file(provider_json): - return False - if not is_file(self._get_ca_cert_path()): - return False - if not self.has_config_for_all_services(): - return False - return True - - def bootstrap(self): - domain = self._domain - log.msg("Bootstrapping provider %s" % domain) - ongoing = self.ongoing_bootstrap.get(domain) - if ongoing: - log.msg('already bootstrapping this provider...') - return - - self.first_bootstrap[self._domain] = defer.Deferred() - - def first_bootstrap_done(ignored): - try: - self.first_bootstrap[domain].callback('got config') - except defer.AlreadyCalledError: - pass - - d = self.maybe_download_provider_info() - d.addCallback(self.maybe_download_ca_cert) - d.addCallback(self.validate_ca_cert) - d.addCallback(first_bootstrap_done) - d.addCallback(self.maybe_download_services_config) - self.ongoing_bootstrap[domain] = d - - def callWhenMainConfigReady(self, cb, *args, **kw): - d = self.first_bootstrap[self._domain] - d.addCallback(lambda _: cb(*args, **kw)) - return d - - def callWhenReady(self, cb, *args, **kw): - d = self.ongoing_bootstrap[self._domain] - d.addCallback(lambda _: cb(*args, **kw)) - return d - - def has_valid_certificate(self): - pass - - def maybe_download_provider_info(self, replace=False): - """ - 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) and not replace: - return defer.succeed('provider_info_already_exists') - - folders, f = os.path.split(provider_json) - mkdir_p(folders) - - uri = self._disco.get_provider_info_uri() - met = self._disco.get_provider_info_method() - - d = downloadPage(uri, provider_json, method=met) - d.addCallback(lambda _: self._load_provider_json()) - d.addErrback(log.err) - return d - - def update_provider_info(self): - """ - Get more recent copy of provider.json from the api URL. - """ - pass - - def maybe_download_ca_cert(self, ignored): - """ - :rtype: deferred - """ - path = self._get_ca_cert_path() - if is_file(path): - return defer.succeed('ca_cert_path_already_exists') - - uri = self._get_ca_cert_uri() - mkdir_p(os.path.split(path)[0]) - d = downloadPage(uri, path) - d.addErrback(log.err) - return d - - def validate_ca_cert(self, ignored): - # TODO Need to verify fingerprint against the one in provider.json - expected = self._get_expected_ca_cert_fingerprint() - print "EXPECTED FINGERPRINT:", expected - - def _get_expected_ca_cert_fingerprint(self): - try: - fgp = self._provider_config.ca_cert_fingerprint - except AttributeError: - fgp = None - return fgp - - # Services config files - - def has_fetched_services_config(self): - return os.path.isfile(self._get_configs_path()) - - def maybe_download_services_config(self, ignored): - - # TODO --- currently, some providers (mail.bitmask.net) raise 401 - # UNAUTHENTICATED if we try to get the services - # See: # https://leap.se/code/issues/7906 - - def further_bootstrap_needs_auth(ignored): - log.err('cannot download services config yet, need auth') - pending_deferred = defer.Deferred() - self.stuck_bootstrap[self._domain] = pending_deferred - return pending_deferred - - uri, met, path = self._get_configs_download_params() - - d = downloadPage(uri, path, method=met) - d.addCallback(lambda _: self._load_provider_json()) - d.addCallback( - lambda _: self._get_config_for_all_services(session=None)) - d.addErrback(further_bootstrap_needs_auth) - return d - - def download_services_config_with_auth(self, session): - - def verify_provider_configs(ignored): - self._load_provider_configs() - return True - - def workaround_for_config_fetch(failure): - # FIXME --- configs.json raises 500, see #7914. - # This is a workaround until that's fixed. - log.err(failure) - log.msg( - "COULD NOT VERIFY CONFIGS.JSON, WORKAROUND: DIRECT DOWNLOAD") - - if 'mx' in self._provider_config.services: - soledad_uri = '/1/config/soledad-service.json' - smtp_uri = '/1/config/smtp-service.json' - base = self._disco.netloc - - fetch = self._fetch_provider_configs_unauthenticated - get_path = self._get_service_config_path - - d1 = fetch( - 'https://' + str(base + soledad_uri), get_path('soledad')) - d2 = fetch( - 'https://' + str(base + smtp_uri), get_path('smtp')) - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: finish_stuck_after_workaround()) - return d - - def finish_stuck_after_workaround(): - stuck = self.stuck_bootstrap.get(self._domain, None) - if stuck: - stuck.callback('continue!') - - def complete_bootstrapping(ignored): - stuck = self.stuck_bootstrap.get(self._domain, None) - if stuck: - d = self._get_config_for_all_services(session) - d.addCallback(lambda _: stuck.callback('continue!')) - d.addErrback(log.err) - return d - - if not self.has_fetched_services_config(): - self._load_provider_json() - uri, met, path = self._get_configs_download_params() - d = session.fetch_provider_configs(uri, path) - d.addCallback(verify_provider_configs) - d.addCallback(complete_bootstrapping) - d.addErrback(workaround_for_config_fetch) - return d - else: - d = defer.succeed('already downloaded') - d.addCallback(complete_bootstrapping) - return d - - def _get_configs_download_params(self): - uri = self._disco.get_configs_uri() - met = self._disco.get_configs_method() - path = self._get_configs_path() - return uri, met, path - - def offers_service(self, service): - if service not in self.SERVICES_MAP.keys(): - raise RuntimeError('Unknown service: %s' % service) - return service in self._provider_config.services - - def is_service_enabled(self, service): - # TODO implement on some config file - return True - - def has_config_for_service(self, service): - has_file = os.path.isfile - path = self._get_service_config_path - smap = self.SERVICES_MAP - - result = all([has_file(path(subservice)) for - subservice in smap[service]]) - return result - - def has_config_for_all_services(self): - self._load_provider_json() - if not self._provider_config: - return False - all_services = self._provider_config.services - has_all = all( - [self.has_config_for_service(service) for service in - all_services]) - return has_all - - def _get_provider_json_path(self): - domain = self._domain.encode(sys.getfilesystemencoding()) - provider_json_path = os.path.join( - self._basedir, get_provider_path(domain, config='provider.json')) - return provider_json_path - - def _get_configs_path(self): - domain = self._domain.encode(sys.getfilesystemencoding()) - configs_path = os.path.join( - self._basedir, get_provider_path(domain, config='configs.json')) - return configs_path - - def _get_service_config_path(self, service): - domain = self._domain.encode(sys.getfilesystemencoding()) - configs_path = os.path.join( - self._basedir, get_provider_path( - domain, config='%s-service.json' % service)) - return configs_path - - def _get_ca_cert_path(self): - domain = self._domain.encode(sys.getfilesystemencoding()) - cert_path = os.path.join(self._basedir, get_ca_cert_path(domain)) - return cert_path - - def _get_ca_cert_uri(self): - try: - uri = self._provider_config.ca_cert_uri - uri = str(uri) - except Exception: - uri = None - return uri - - def _load_provider_json(self): - path = self._get_provider_json_path() - if not is_file(path): - log.msg("Cannot LOAD provider config path %s" % path) - return - - with open(path, 'r') as config: - self._provider_config = Record(**json.load(config)) - - api_uri = self._provider_config.api_uri - if api_uri: - parsed = urlparse(api_uri) - self._disco.netloc = parsed.netloc - - def _get_config_for_all_services(self, session): - services_dict = self._load_provider_configs() - configs_path = self._get_configs_path() - with open(configs_path) as jsonf: - services_dict = Record(**json.load(jsonf)).services - pending = [] - base = self._disco.get_base_uri() - for service in self._provider_config.services: - if service in self.SERVICES_MAP.keys(): - for subservice in self.SERVICES_MAP[service]: - uri = base + str(services_dict[subservice]) - path = self._get_service_config_path(subservice) - if session: - d = session.fetch_provider_configs(uri, path) - else: - d = self._fetch_provider_configs_unauthenticated( - uri, path) - pending.append(d) - return defer.gatherResults(pending) - - def _load_provider_configs(self): - configs_path = self._get_configs_path() - with open(configs_path) as jsonf: - services_dict = Record(**json.load(jsonf)).services - return services_dict - - def _fetch_provider_configs_unauthenticated(self, uri, path): - log.msg('Downloading config for %s...' % uri) - d = downloadPage(uri, path, method='GET') - return d - - def _http_request(self, *args, **kw): - # XXX pass if-modified-since header - return httpRequest(self._agent, *args, **kw) - - -class Record(object): - def __init__(self, **kw): - self.__dict__.update(kw) - - -class WebClientContextFactory(ClientContextFactory): - def getContext(self, hostname, port): - return ClientContextFactory.getContext(self) - - -if __name__ == '__main__': - - def print_done(): - print '>>> bootstrapping done!!!' - - provider = Provider('cdev.bitmask.net') - provider.callWhenReady(print_done) - reactor.run() diff --git a/bonafide/src/leap/bonafide/cred_srp.py b/bonafide/src/leap/bonafide/cred_srp.py deleted file mode 100644 index 9fcb97b..0000000 --- a/bonafide/src/leap/bonafide/cred_srp.py +++ /dev/null @@ -1,157 +0,0 @@ -# -*- coding: utf-8 -*- -# srp_cred.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/>. - -""" -Credential module for authenticating SRP requests against the LEAP platform. -""" - -# ----------------- DOC ------------------------------------------------------ -# See examples of cred modules: -# https://github.com/oubiwann-unsupported/txBrowserID/blob/master/browserid/checker.py -# http://stackoverflow.com/questions/19171686/book-twisted-network-programming-essentials-example-9-1-does-not-work -# ----------------- DOC ------------------------------------------------------ - -from zope.interface import implements, implementer, Interface, Attribute - -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 Session - - -@implementer(ICredentialsChecker) -class SRPCredentialsChecker(object): - - # TODO need to decide if the credentials that we pass here are per provider - # or not. - # I think it's better to have the credentials passed with the full user_id, - # and here split user/provider. - # XXX then we need to check if the provider is properly configured, to get - # the right api info AND the needed certificates. - # XXX might need to initialize credential checker with a ProviderAPI - - credentialInterfaces = (credentials.IUsernamePassword,) - - def requestAvatarId(self, credentials): - # TODO If we are already authenticated, we should just - # return the session object, somehow. - # XXX If not authenticated (ie, no cached credentials?) - # we pass credentials to srpauth.authenticate method - # another srpauth class should interface with the blocking srpauth - # library, and chain all the calls needed to do the handshake. - # Therefore: - # should keep reference to the srpauth instances somewhere - # TODO If we want to return an anonymous user (useful for getting the - # anon-vpn cert), we should return an empty tuple from here. - - return defer.maybeDeferred(_get_leap_session(credentials)).addCallback( - self._check_srp_auth) - - def _check_srp_auth(session, username): - if session.is_authenticated: - # is ok! --- should add it to some global cache? - return defer.succeed(username) - else: - return defer.fail(credError.UnauthorizedLogin( - "Bad username/password combination")) - - -def _get_leap_session(credentials): - session = Session(credentials) - d = session.authenticate() - d.addCallback(lambda _: session) - return d - - -class ILeapUserAvatar(Interface): - - # TODO add attributes for username, uuid, token, session_id - - def logout(): - """ - Clean up per-login resource allocated to this avatar. - """ - - -@implementer(ILeapUserAvatar) -class LeapUserAvatar(object): - - # TODO initialize with: username, uuid, token, session_id - # TODO initialize provider data (for api) - # TODO how does this relate to LeapSession? maybe we should get one passed? - - def logout(self): - - # TODO reset the (global?) srpauth object. - # https://leap.se/en/docs/design/bonafide#logout - # DELETE API_BASE/logout(.json) - pass - - -class LeapAuthRealm(object): - """ - The realm corresponds to an application domain and is in charge of avatars, - which are network-accessible business logic objects. - """ - - # TODO should be initialized with provider API objects. - - implements(portal.IRealm) - - def requestAvatar(self, avatarId, mind, *interfaces): - - if ILeapUserAvatar in interfaces: - # XXX how should we get the details for the requested avatar? - avatar = LeapUserAvatar() - return ILeapUserAvatar, avatar, avatar.logout - - raise NotImplementedError( - "This realm only supports the ILeapUserAvatar interface.") - - -if __name__ == '__main__': - - # from the browser-id implementation - #import sys - #def _done(res): - #print res - #reactor.stop() - #assertion = sys.argv[1] - #d = defer.Deferred() - #reactor.callLater(0, d.callback, "http://localhost:8081") - #d.addCallback(SRPCredentialsChecker) - #d.addCallback(lambda c: c.requestAvatarId(assertion)) - #d.addBoth(_done) - #reactor.run() - - # XXX move boilerplate to some bitmask-core template. - - leap_realm = LeapAuthRealm() - # XXX should pass a provider mapping to realm too? - - leap_portal = portal.Portal(leap_realm) - - # XXX should we add an offline credentials checker, that's able - # to unlock local soledad sqlcipher backend? - # XXX should pass a provider mapping to credentials checker too? - srp_checker = SRPCredentialsChecker() - leap_portal.registerChecker(srp_checker) - - # XXX tie this to some sample server... - reactor.listenTCP(8000, EchoFactory(leap_portal)) - reactor.run() diff --git a/bonafide/src/leap/bonafide/provider.py b/bonafide/src/leap/bonafide/provider.py deleted file mode 100644 index b44dae2..0000000 --- a/bonafide/src/leap/bonafide/provider.py +++ /dev/null @@ -1,186 +0,0 @@ -# -*- coding: utf-8 -*- -# provier.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/>. - -""" -LEAP Provider API. -""" - -from copy import deepcopy -import re -from urlparse import urlparse - - -""" -Maximum API version number supported by bonafide -""" -MAX_API_VERSION = 1 - - -class _MetaActionDispatcher(type): - - """ - A metaclass that will create dispatcher methods dynamically for each - action made available by the LEAP provider API. - - The new methods will be created according to the values contained in an - `_actions` dictionary, with the following format:: - - {'action_name': (uri_template, method)} - - where `uri_template` is a string that will be formatted with an arbitrary - number of keyword arguments. - - Any class that uses this one as its metaclass needs to implement two - private methods:: - - _get_uri(self, action_name, **extra_params) - _get_method(self, action_name) - - Beware that currently they cannot be inherited from bases. - """ - - def __new__(meta, name, bases, dct): - - def _generate_action_funs(dct): - _get_uri = dct['_get_uri'] - _get_method = dct['_get_method'] - newdct = deepcopy(dct) - actions = dct['_actions'] - - def create_uri_fun(action_name): - return lambda self, **kw: _get_uri( - self, action_name=action_name, **kw) - - def create_met_fun(action_name): - return lambda self: _get_method( - self, action_name=action_name) - - for action in actions: - uri, method = actions[action] - _action_uri = 'get_%s_uri' % action - _action_met = 'get_%s_method' % action - newdct[_action_uri] = create_uri_fun(action) - newdct[_action_met] = create_met_fun(action) - return newdct - - newdct = _generate_action_funs(dct) - return super(_MetaActionDispatcher, meta).__new__( - meta, name, bases, newdct) - - -class BaseProvider(object): - - def __init__(self, netloc, version=1): - parsed = urlparse(netloc) - if parsed.scheme != 'https': - raise ValueError( - 'ProviderApi needs to be passed a url with https scheme') - self.netloc = parsed.netloc - - self.version = version - if version > MAX_API_VERSION: - self.version = MAX_API_VERSION - - def get_hostname(self): - return urlparse(self._get_base_url()).hostname - - def _get_base_url(self): - return "https://{0}/{1}".format(self.netloc, self.version) - - -class Api(BaseProvider): - """ - An object that has all the information that a client needs to communicate - with the remote methods exposed by the web API of a LEAP provider. - - The actions are described in https://leap.se/bonafide - - By using the _MetaActionDispatcher as a metaclass, the _actions dict will - be translated dynamically into a set of instance methods that will allow - getting the uri and method for each action. - - The keyword arguments specified in the format string will automatically - raise a KeyError if the needed keyword arguments are not passed to the - dynamically created methods. - """ - - # 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 = { - 'signup': ('users', 'POST'), - 'update_user': ('users/{uid}', 'PUT'), - 'handshake': ('sessions', 'POST'), - 'authenticate': ('sessions/{login}', 'PUT'), - 'logout': ('logout', 'DELETE'), - 'vpn_cert': ('cert', 'POST'), - 'smtp_cert': ('smtp_cert', 'POST'), - } - - # Methods expected by the dispatcher metaclass - - def _get_uri(self, action_name, **extra_params): - resource, _ = self._actions.get(action_name) - uri = '{0}/{1}'.format( - bytes(self._get_base_url()), - bytes(resource)).format(**extra_params) - return uri - - def _get_method(self, action_name): - _, method = self._actions.get(action_name) - return method - - -class Discovery(BaseProvider): - """ - Discover basic information about a provider, including the provided - services. - """ - - __metaclass__ = _MetaActionDispatcher - _actions = { - 'provider_info': ('provider.json', 'GET'), - 'configs': ('1/configs.json', 'GET'), - } - - def _get_base_url(self): - return "https://{0}".format(self.netloc) - - def get_base_uri(self): - return self._get_base_url() - - # Methods expected by the dispatcher metaclass - - def _get_uri(self, action_name, **extra_params): - resource, _ = self._actions.get(action_name) - uri = '{0}/{1}'.format( - bytes(self._get_base_url()), - bytes(resource)).format(**extra_params) - return uri - - def _get_method(self, action_name): - _, method = self._actions.get(action_name) - return method - - -def validate_username(username): - accepted_characters = '^[a-z0-9\-\_\.]*$' - if not re.match(accepted_characters, username): - raise ValueError('Only lowercase letters, digits, . - and _ allowed.') diff --git a/bonafide/src/leap/bonafide/service.py b/bonafide/src/leap/bonafide/service.py deleted file mode 100644 index 14585ef..0000000 --- a/bonafide/src/leap/bonafide/service.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- 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/>. - -""" -Bonafide Service. -""" -import os -from collections import defaultdict - -from leap.common.config import get_path_prefix -from leap.common.service_hooks import HookableService -from leap.bonafide._protocol import BonafideProtocol - -from twisted.internet import defer -from twisted.python import log - - -_preffix = get_path_prefix() - - -class BonafideService(HookableService): - - def __init__(self, basedir=None): - if not basedir: - basedir = os.path.join(_preffix, 'leap') - self._basedir = os.path.expanduser(basedir) - self._bonafide = BonafideProtocol() - self.service_hooks = defaultdict(list) - - # XXX this is a quick hack to get a ref - # to the latest authenticated user. - self._active_user = None - - def startService(self): - log.msg('Starting Bonafide Service') - super(BonafideService, self).startService() - - # Commands - - def do_authenticate(self, username, password): - - def notify_passphrase_entry(username, password): - data = dict(username=username, password=password) - self.trigger_hook('on_passphrase_entry', **data) - - def notify_bonafide_auth(result): - if not result: - msg = "Authentication hook did not return anything" - log.msg(msg) - raise RuntimeError(msg) - - token, uuid = result - data = dict(username=username, token=token, uuid=uuid, - password=password) - self.trigger_hook('on_bonafide_auth', **data) - - self._active_user = username - return result - - # XXX I still have doubts from where it's best to trigger this. - # We probably should wait for BOTH deferreds and - # handle local and remote authentication success together - # (and fail if either one fails). Going with fire-and-forget for - # now, but needs needs improvement. - - notify_passphrase_entry(username, password) - - d = self._bonafide.do_authenticate(username, password) - d.addCallback(notify_bonafide_auth) - d.addCallback(lambda response: { - 'srp_token': response[0], 'uuid': response[1]}) - return d - - def do_signup(self, username, password): - d = self._bonafide.do_signup(username, password) - d.addCallback(lambda response: {'signup': 'ok', 'user': response}) - return d - - def do_logout(self, username): - if not username: - username = self._active_user - - def reset_active(passthrough): - self._active_user = None - return passthrough - - d = self._bonafide.do_logout(username) - d.addCallback(reset_active) - d.addCallback(lambda response: {'logout': 'ok'}) - return d - - def do_get_smtp_cert(self, username=None): - if not username: - username = self._active_user - if not username: - return defer.fail( - RuntimeError('No active user, cannot get SMTP cert.')) - - d = self._bonafide.do_get_smtp_cert(username) - d.addCallback(lambda response: (username, response)) - return d - - def do_get_active_user(self): - user = self._active_user or '<none>' - info = {'user': user} - return defer.succeed(info) diff --git a/bonafide/src/leap/bonafide/services/TODO b/bonafide/src/leap/bonafide/services/TODO deleted file mode 100644 index 97dc639..0000000 --- a/bonafide/src/leap/bonafide/services/TODO +++ /dev/null @@ -1 +0,0 @@ -# XXX this should be discoverable through the config. diff --git a/bonafide/src/leap/bonafide/services/__init__.py b/bonafide/src/leap/bonafide/services/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/bonafide/src/leap/bonafide/services/__init__.py +++ /dev/null diff --git a/bonafide/src/leap/bonafide/services/eip.py b/bonafide/src/leap/bonafide/services/eip.py deleted file mode 100644 index e69de29..0000000 --- a/bonafide/src/leap/bonafide/services/eip.py +++ /dev/null diff --git a/bonafide/src/leap/bonafide/services/mail.py b/bonafide/src/leap/bonafide/services/mail.py deleted file mode 100644 index e69de29..0000000 --- a/bonafide/src/leap/bonafide/services/mail.py +++ /dev/null diff --git a/bonafide/src/leap/bonafide/services/soledad.py b/bonafide/src/leap/bonafide/services/soledad.py deleted file mode 100644 index e69de29..0000000 --- a/bonafide/src/leap/bonafide/services/soledad.py +++ /dev/null diff --git a/bonafide/src/leap/bonafide/session.py b/bonafide/src/leap/bonafide/session.py deleted file mode 100644 index 4180041..0000000 --- a/bonafide/src/leap/bonafide/session.py +++ /dev/null @@ -1,219 +0,0 @@ -# -*- coding: utf-8 -*- -# session.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/>. -""" -LEAP Session management. -""" -from twisted.internet import defer, reactor -from twisted.python import log - -from leap.bonafide import _srp -from leap.bonafide import provider -from leap.bonafide._http import httpRequest, cookieAgentFactory - -OK = 'ok' - - -def _auth_required(func): - """ - Decorate a method so that it will not be called if the instance - attribute `is_authenticated` does not evaluate to True. - """ - def decorated(*args, **kwargs): - instance = args[0] - allowed = getattr(instance, 'is_authenticated') - if not allowed: - raise RuntimeError('This method requires authentication') - return func(*args, **kwargs) - return decorated - - -class Session(object): - - def __init__(self, credentials, api, provider_cert): - # TODO check if an anonymous credentials is passed. - # TODO move provider_cert to api object. - # On creation, it should be able to retrieve all the info it needs - # (calling bootstrap). - # TODO could get a "provider" object instead. - # this provider can have an api attribute, - # and a "autoconfig" attribute passed on initialization. - # TODO get a file-descriptor for password if not in credentials - # TODO merge self._request with config.Provider._http_request ? - - self.username = credentials.username - self.password = credentials.password - self._provider_cert = provider_cert - self._api = api - self._initialize_session() - - def _initialize_session(self): - self._agent = cookieAgentFactory(self._provider_cert) - username = self.username or '' - password = self.password or '' - self._srp_auth = _srp.SRPAuthMechanism(username, password) - self._srp_signup = _srp.SRPSignupMechanism() - self._token = None - self._uuid = None - - # Session - - @property - def token(self): - return self._token - - @property - def uuid(self): - return self._uuid - - @property - def is_authenticated(self): - return self._srp_auth.srp_user.authenticated() - - @defer.inlineCallbacks - def authenticate(self): - uri = self._api.get_handshake_uri() - met = self._api.get_handshake_method() - log.msg("%s to %s" % (met, uri)) - params = self._srp_auth.get_handshake_params() - - handshake = yield self._request(self._agent, uri, values=params, - method=met) - - self._srp_auth.process_handshake(handshake) - uri = self._api.get_authenticate_uri(login=self.username) - met = self._api.get_authenticate_method() - - log.msg("%s to %s" % (met, uri)) - params = self._srp_auth.get_authentication_params() - - auth = yield self._request(self._agent, uri, values=params, - method=met) - - uuid, token = self._srp_auth.process_authentication(auth) - self._srp_auth.verify_authentication() - - self._uuid = uuid - self._token = token - defer.returnValue(OK) - - @_auth_required - @defer.inlineCallbacks - def logout(self): - uri = self._api.get_logout_uri() - met = self._api.get_logout_method() - auth = yield self._request(self._agent, uri, method=met) - print 'AUTH', auth - print 'resetting user/pass' - self.username = None - self.password = None - self._initialize_session() - defer.returnValue(OK) - - # User certificates - - def get_vpn_cert(self): - # TODO pass it to the provider object so that it can save it in the - # right path. - uri = self._api.get_vpn_cert_uri() - met = self._api.get_vpn_cert_method() - return self._request(self._agent, uri, method=met) - - @_auth_required - def get_smtp_cert(self): - # TODO pass it to the provider object so that it can save it in the - # right path. - uri = self._api.get_smtp_cert_uri() - met = self._api.get_smtp_cert_method() - print met, "to", uri - return self._request(self._agent, uri, method=met) - - def _request(self, *args, **kw): - kw['token'] = self._token - return httpRequest(*args, **kw) - - # User management - - @defer.inlineCallbacks - def signup(self, username, password): - # XXX should check that it_IS_NOT_authenticated - provider.validate_username(username) - uri = self._api.get_signup_uri() - met = self._api.get_signup_method() - params = self._srp_signup.get_signup_params( - username, password) - - signup = yield self._request(self._agent, uri, values=params, - method=met) - registered_user = self._srp_signup.process_signup(signup) - self.username = username - self.password = password - defer.returnValue((OK, registered_user)) - - @_auth_required - def update_user_record(self): - # FIXME to be implemented - pass - - # Authentication-protected configuration - - @defer.inlineCallbacks - def fetch_provider_configs(self, uri, path): - config = yield self._request(self._agent, uri) - with open(path, 'w') as cf: - cf.write(config) - defer.returnValue('ok') - - -if __name__ == "__main__": - import os - import sys - from twisted.cred.credentials import UsernamePassword - - if len(sys.argv) != 4: - print "Usage:", sys.argv[0], "provider", "username", "password" - sys.exit() - _provider, username, password = sys.argv[1], sys.argv[2], sys.argv[3] - api = provider.Api('https://api.%s:4430' % _provider) - credentials = UsernamePassword(username, password) - cdev_pem = os.path.expanduser( - '~/.config/leap/providers/%s/keys/ca/cacert.pem' % _provider) - session = Session(credentials, api, cdev_pem) - - def print_result(result): - print result - return result - - def cbShutDown(ignored): - reactor.stop() - - def auth_eb(failure): - print "[ERROR!]", failure.getErrorMessage() - log.err(failure) - - d = session.authenticate() - d.addCallback(print_result) - d.addErrback(auth_eb) - - d.addCallback(lambda _: session.get_smtp_cert()) - #d.addCallback(lambda _: session.get_vpn_cert()) - d.addCallback(print_result) - d.addErrback(auth_eb) - - d.addCallback(lambda _: session.logout()) - d.addErrback(auth_eb) - d.addBoth(cbShutDown) - reactor.run() diff --git a/bonafide/src/leap/bonafide/ssh_service.py b/bonafide/src/leap/bonafide/ssh_service.py deleted file mode 100644 index 3777b10..0000000 --- a/bonafide/src/leap/bonafide/ssh_service.py +++ /dev/null @@ -1,2 +0,0 @@ -# TODO expose bonafide protocol through ssh, accessible through a hidden -# service diff --git a/bonafide/src/leap/bonafide/tests/__init__.py b/bonafide/src/leap/bonafide/tests/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/bonafide/src/leap/bonafide/tests/__init__.py +++ /dev/null |