diff options
Diffstat (limited to 'src/leap/bitmask/core/web')
-rw-r--r-- | src/leap/bitmask/core/web/__init__.py | 12 | ||||
-rw-r--r-- | src/leap/bitmask/core/web/_auth.py | 86 | ||||
-rw-r--r-- | src/leap/bitmask/core/web/api.py | 32 | ||||
-rw-r--r-- | src/leap/bitmask/core/web/service.py | 152 | ||||
-rw-r--r-- | src/leap/bitmask/core/web/static/README (renamed from src/leap/bitmask/core/web/README) | 0 | ||||
-rw-r--r-- | src/leap/bitmask/core/web/static/__init__.py | 0 | ||||
-rw-r--r-- | src/leap/bitmask/core/web/static/index.html (renamed from src/leap/bitmask/core/web/index.html) | 0 |
7 files changed, 282 insertions, 0 deletions
diff --git a/src/leap/bitmask/core/web/__init__.py b/src/leap/bitmask/core/web/__init__.py index e69de29b..ed8cc527 100644 --- a/src/leap/bitmask/core/web/__init__.py +++ b/src/leap/bitmask/core/web/__init__.py @@ -0,0 +1,12 @@ +try: + import leap.bitmask_js + assert leap.bitmask_js + HAS_WEB_UI = True +except ImportError: + HAS_WEB_UI = False + +try: + import txtorcon + assert txtorcon +except Exception: + pass diff --git a/src/leap/bitmask/core/web/_auth.py b/src/leap/bitmask/core/web/_auth.py new file mode 100644 index 00000000..6a5e3621 --- /dev/null +++ b/src/leap/bitmask/core/web/_auth.py @@ -0,0 +1,86 @@ +from zope.interface import implementer + +from twisted.cred import portal, checkers, credentials, error as credError +from twisted.internet import defer +from twisted.web.guard import HTTPAuthSessionWrapper, BasicCredentialFactory +from twisted.web.resource import IResource + + +class TokenCredentialFactory(BasicCredentialFactory): + scheme = 'token' + + +@implementer(IResource) +class WhitelistHTTPAuthSessionWrapper(HTTPAuthSessionWrapper): + + """ + Wrap a portal, enforcing supported header-based authentication schemes. + It doesn't apply the enforcement to routes included in a whitelist. + """ + + # TODO extend this to inspect the data -- so that we pass a tuple + # with the action + + whitelist = (None,) + + def __init__(self, *args, **kw): + self.whitelist = kw.pop('whitelist', tuple()) + super(WhitelistHTTPAuthSessionWrapper, self).__init__( + *args, **kw) + + def getChildWithDefault(self, path, request): + if request.path in self.whitelist: + return self + return HTTPAuthSessionWrapper.getChildWithDefault(self, path, request) + + def render(self, request): + if request.path in self.whitelist: + _res = self._portal.realm.resource + return _res.render(request) + return HTTPAuthSessionWrapper.render(self, request) + + +def protectedResourceFactory(resource, session_tokens, whitelist): + realm = HttpPasswordRealm(resource) + checker = TokenDictChecker(session_tokens) + resource_portal = portal.Portal(realm, [checker]) + credentialFactory = TokenCredentialFactory('localhost') + protected_resource = WhitelistHTTPAuthSessionWrapper( + resource_portal, [credentialFactory], + whitelist=whitelist) + return protected_resource + + +@implementer(checkers.ICredentialsChecker) +class TokenDictChecker: + + credentialInterfaces = (credentials.IUsernamePassword, + credentials.IUsernameHashedPassword) + + def __init__(self, tokens): + self.tokens = tokens + + def requestAvatarId(self, credentials): + username = credentials.username + if username in self.tokens: + if credentials.checkPassword(self.tokens[username]): + return defer.succeed(username) + else: + return defer.fail( + credError.UnauthorizedLogin("Bad session token")) + else: + return defer.fail( + credError.UnauthorizedLogin("No such user")) + + +@implementer(portal.IRealm) +class HttpPasswordRealm(object): + + def __init__(self, resource): + self.resource = resource + + def requestAvatar(self, user, mind, *interfaces): + # the resource is passed on regardless of user + if IResource in interfaces: + return (IResource, self.resource, lambda: None) + raise NotImplementedError() diff --git a/src/leap/bitmask/core/web/api.py b/src/leap/bitmask/core/web/api.py new file mode 100644 index 00000000..e8bd21e4 --- /dev/null +++ b/src/leap/bitmask/core/web/api.py @@ -0,0 +1,32 @@ +import json +from twisted.web.server import NOT_DONE_YET + +from twisted.web.resource import Resource + + +class Api(Resource): + + isLeaf = True + + def __init__(self, dispatcher): + Resource.__init__(self) + self.dispatcher = dispatcher + + def render_POST(self, request): + command = request.uri.split('/')[2:] + params = request.content.getvalue() + if params: + # json.loads returns unicode strings and the rest of the code + # expects strings. This 'str(param)' conversion can be removed + # if we move to python3 + for param in json.loads(params): + command.append(str(param)) + + d = self.dispatcher.dispatch(command) + d.addCallback(self._write_response, request) + return NOT_DONE_YET + + def _write_response(self, response, request): + request.setHeader('Content-Type', 'application/json') + request.write(response) + request.finish() diff --git a/src/leap/bitmask/core/web/service.py b/src/leap/bitmask/core/web/service.py new file mode 100644 index 00000000..2437d2d6 --- /dev/null +++ b/src/leap/bitmask/core/web/service.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +# service.py +# Copyright (C) 2016 LEAP Encryption Acess Project +# +# 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/>. + +""" +HTTP REST Dispatcher Service. +""" + +import os +import pkg_resources + +from twisted.application import service +from twisted.logger import Logger +from twisted.internet import endpoints +from twisted.internet import reactor +from twisted.web.server import Site +from twisted.web.static import File + +from leap.bitmask.core.dispatcher import CommandDispatcher +from leap.bitmask.core.web import HAS_WEB_UI +from leap.bitmask.core.web.api import Api +from leap.bitmask.core.web._auth import protectedResourceFactory +from leap.bitmask.util import here + +try: + import txtorcon +except ImportError: + pass + + +log = Logger() + + +class HTTPDispatcherService(service.Service): + + """ + A Dispatcher for BitmaskCore exposing a REST API. + + The API itself is served under the API/ route. + + If the package ``leap.bitmask_js`` is found in the import path, we'll serve + the whole JS UI in the root resource too (under the ``public`` path). + + If that package cannot be found, we'll serve just the javascript wrapper + around the REST API. + """ + + API_WHITELIST = ( + '/API/bonafide/user', + ) + + def __init__(self, core, port=7070, debug=False, onion=False): + self._core = core + self.port = port + self.debug = debug + self.onion = onion + self.uri = '' + + def startService(self): + # TODO refactor this, too long---------------------------------------- + if HAS_WEB_UI: + webdir = os.path.abspath( + pkg_resources.resource_filename('leap.bitmask_js', 'public')) + log.debug('webdir: %s' % webdir) + else: + log.warn('bitmask_js not found, serving bitmask.core ui') + webdir = os.path.abspath( + pkg_resources.resource_filename( + 'leap.bitmask.core.web', 'static')) + jspath = os.path.join( + here(), '..', '..', '..', + 'ui', 'app', 'lib', 'bitmask.js') + jsapi = File(os.path.abspath(jspath)) + + api = Api(CommandDispatcher(self._core)) + protected_api = protectedResourceFactory( + api, self._core.tokens, self.API_WHITELIST) + + root = File(webdir) + root.putChild(u'API', protected_api) + if not HAS_WEB_UI: + root.putChild('bitmask.js', jsapi) + + factory = Site(root) + self.site = factory + + if self.onion and _has_txtorcon(): + self._start_onion_service(factory) + else: + interface = '127.0.0.1' + endpoint = endpoints.TCP4ServerEndpoint( + reactor, self.port, interface=interface) + self.uri = 'https://%s:%s' % (interface, self.port) + endpoint.listen(factory) + + # TODO this should be set in a callback to the listen call + self.running = True + + def _start_onion_service(self, factory): + + def progress(percent, tag, message): + bar = int(percent / 10) + log.debug('[%s%s] %s' % ('#' * bar, '.' * (10 - bar), message)) + + def setup_complete(port): + port = txtorcon.IHiddenService(port) + self.uri = "http://%s" % (port.getHost().onion_uri) + log.info('I have set up a hidden service, advertised at: %s' + % self.uri) + log.info('locally listening on %s' % port.local_address.getHost()) + + def setup_failed(args): + log.error('onion service setup FAILED: %r' % args) + + endpoint = endpoints.serverFromString(reactor, 'onion:80') + txtorcon.IProgressProvider(endpoint).add_progress_listener(progress) + d = endpoint.listen(factory) + d.addCallback(setup_complete) + d.addErrback(setup_failed) + return d + + def stopService(self): + self.site.stopFactory() + self.listener.stopListening() + self.running = False + + def do_status(self): + status = 'running' if self.running else 'disabled' + return {'web': status, 'uri': self.uri} + + +def _has_txtorcon(): + try: + import txtorcon + txtorcon + except ImportError: + log.error('onion is enabled, but could not find txtorcon') + return False + return True diff --git a/src/leap/bitmask/core/web/README b/src/leap/bitmask/core/web/static/README index 2b99926c..2b99926c 100644 --- a/src/leap/bitmask/core/web/README +++ b/src/leap/bitmask/core/web/static/README diff --git a/src/leap/bitmask/core/web/static/__init__.py b/src/leap/bitmask/core/web/static/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/bitmask/core/web/static/__init__.py diff --git a/src/leap/bitmask/core/web/index.html b/src/leap/bitmask/core/web/static/index.html index 9951a9b2..9951a9b2 100644 --- a/src/leap/bitmask/core/web/index.html +++ b/src/leap/bitmask/core/web/static/index.html |