summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKali Kaneko (leap communications) <kali@leap.se>2015-12-18 02:00:58 -0400
committerKali Kaneko (leap communications) <kali@leap.se>2015-12-18 02:00:58 -0400
commit462e71f0c44cd5369c95d06402b3234e396550e5 (patch)
tree0ec092690910d1e0bc9d5dea553a3cf1ba5ed06a
parente8c3d389a734350661cdd1fcf6db607f318e75e8 (diff)
wip: provider bootstrap, autodiscoveryheads/kali/master
-rw-r--r--src/leap/bonafide/config.py145
-rw-r--r--src/leap/bonafide/provider.py77
2 files changed, 188 insertions, 34 deletions
diff --git a/src/leap/bonafide/config.py b/src/leap/bonafide/config.py
index 9490f55..c3a4fbe 100644
--- a/src/leap/bonafide/config.py
+++ b/src/leap/bonafide/config.py
@@ -18,10 +18,17 @@
Configuration for a LEAP provider.
"""
import datetime
+import json
import os
import sys
+from twisted.internet import 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
@@ -45,8 +52,15 @@ def get_provider_path(domain):
: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")
+ # TODO sanitize domain
+ leap_assert(domain is not None, 'get_provider_path: We need a domain')
+ return os.path.join('providers', domain, 'provider.json')
+
+
+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):
@@ -99,28 +113,67 @@ def make_address(user, provider):
:param provider: the provider domain
:type provider: basestring
"""
- return "%s@%s" % (user, provider)
+ return '%s@%s' % (user, provider)
def get_username_and_provider(full_id):
return full_id.split('@')
+class WebClientContextFactory(ClientContextFactory):
+ def getContext(self, hostname, port):
+ return ClientContextFactory.getContext(self)
+
+
class ProviderConfig(object):
- # TODO add file config for enabled services
- def __init__(self, domain):
- self._api_base = None
+ # TODO add validation
+ # TODO split this class: ProviderBootstrap, ProviderConfig
+
+ def __init__(self, domain, autoconf=True, basedir='~/.config/leap',
+ check_certificate=True):
self._domain = domain
+ self._basedir = os.path.expanduser(basedir)
+ self._disco = Discovery('https://%s' % domain)
+ self._provider_config = {}
+
+ if not check_certificate:
+ # XXX we should do this only for the FIRST provider download.
+ # For the rest, we should pass the ca cert to the agent.
+ self.contextFactory = WebClientContextFactory()
+ else:
+ self.contextFactory = None
+ self._agent = Agent(reactor, self.contextFactory)
+
+ self._load_provider_config()
+ # TODO if loaded, setup _get_api_uri on the DISCOVERY
+
+ if not self.is_configured() and autoconf:
+ print 'provider %s not configured: downloading files...' % domain
+ self.bootstrap()
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
+ if not is_file(provider_json):
+ return False
+ if not is_file(self._get_ca_cert_path()):
+ return False
+ return True
+
+ def bootstrap(self):
+ provider_json = self._get_provider_json_path()
+ if not is_file(provider_json):
+ self.download_provider_info()
+ if not is_file(self._get_ca_cert_path()):
+ self.download_ca_cert()
+ self.validate_ca_cert()
+ self.download_services_config()
+
+ def has_valid_certificate(self):
+ pass
- def download_provider_info(self):
+ def 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.
@@ -128,25 +181,81 @@ class ProviderConfig(object):
# 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):
+ print 'PROVIDER JSON', provider_json
+ if is_file(provider_json) and not replace:
raise RuntimeError('File 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()
+
+ def print_info(res):
+ print "RES:", res
+
+ d = downloadPage(uri, provider_json, method=met)
+ d.addCallback(print_info)
+ d.addCallback(lambda _: self._load_provider_config())
+ d.addErrback(log.err)
+ return d
+
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 download_ca_cert(self):
+ uri = self._get_ca_cert_uri()
+ path = self._get_ca_cert_path()
+ mkdir_p(os.path.split(path)[0])
+ d = downloadPage(uri, path)
+ d.addErrback(log.err)
+ return d
+
+ def validate_ca_cert(self):
+ # XXX 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):
+ return self._provider_config.get('ca_cert_fingerprint', None)
+
+ def download_services_config(self):
+ pass
def _get_provider_json_path(self):
domain = self._domain.encode(sys.getfilesystemencoding())
- provider_json = os.path.join(get_path_prefix(), get_provider_path(domain))
+ provider_json = os.path.join(self._basedir, get_provider_path(domain))
return provider_json
+ 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):
+ uri = self._provider_config.get('ca_cert_uri', None)
+ if uri:
+ uri = str(uri)
+ return uri
+
+ def _load_provider_config(self):
+ path = self._get_provider_json_path()
+ if not is_file(path):
+ return
+ with open(path, 'r') as config:
+ self._provider_config = json.load(config)
+
+ def _http_request(self, *args, **kw):
+ # XXX pass if-modified-since header
+ return httpRequest(self._agent, *args, **kw)
+
+ def _get_api_uri(self):
+ pass
+
+
if __name__ == '__main__':
- config = ProviderConfig('cdev.bitmask.net')
- config.is_configured()
- config.download_provider_info()
+ config = ProviderConfig('cdev.bitmask.net', check_certificate=False)
+ reactor.run()
diff --git a/src/leap/bonafide/provider.py b/src/leap/bonafide/provider.py
index 1d0b5b7..60272da 100644
--- a/src/leap/bonafide/provider.py
+++ b/src/leap/bonafide/provider.py
@@ -38,11 +38,13 @@ class _MetaActionDispatcher(type):
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::
+ 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):
@@ -74,17 +76,34 @@ class _MetaActionDispatcher(type):
meta, name, bases, newdct)
-class Api(object):
+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
+
+ 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
+ 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.
@@ -106,19 +125,45 @@ class Api(object):
'smtp_cert': ('smtp_cert', 'POST'),
}
- 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
+ # Methods expected by the dispatcher metaclass
- def get_hostname(self):
- return urlparse(self._get_base_url()).hostname
+ 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': ('configs.json', 'GET'),
+ }
+
+ api_uri = None
+ api_port = None
def _get_base_url(self):
- return "https://{0}/{1}".format(self.netloc, self.version)
+ if self.api_uri:
+ base = self.api_uri
+ else:
+ base = self.netloc
+
+ uri = "https://{0}".format(base)
+ if self.api_port:
+ uri = uri + ':%s' % self.api_port
+ return uri
# Methods expected by the dispatcher metaclass