summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/core/web
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/bitmask/core/web')
-rw-r--r--src/leap/bitmask/core/web/__init__.py12
-rw-r--r--src/leap/bitmask/core/web/_auth.py86
-rw-r--r--src/leap/bitmask/core/web/api.py32
-rw-r--r--src/leap/bitmask/core/web/service.py152
-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__.py0
-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 e69de29..ed8cc52 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 0000000..6a5e362
--- /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 0000000..e8bd21e
--- /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 0000000..2437d2d
--- /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 2b99926..2b99926 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 0000000..e69de29
--- /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 9951a9b..9951a9b 100644
--- a/src/leap/bitmask/core/web/index.html
+++ b/src/leap/bitmask/core/web/static/index.html