From 7c588e919e959f32b33235b4a44da257d8f4a964 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Wed, 7 Dec 2016 20:46:02 +0100 Subject: [refactor] move web service to its own submodule --- src/leap/bitmask/core/web/README | 14 --- src/leap/bitmask/core/web/__init__.py | 12 +++ src/leap/bitmask/core/web/_auth.py | 86 +++++++++++++++ src/leap/bitmask/core/web/api.py | 32 ++++++ src/leap/bitmask/core/web/index.html | 131 ----------------------- src/leap/bitmask/core/web/service.py | 152 +++++++++++++++++++++++++++ src/leap/bitmask/core/web/static/README | 14 +++ src/leap/bitmask/core/web/static/__init__.py | 0 src/leap/bitmask/core/web/static/index.html | 131 +++++++++++++++++++++++ 9 files changed, 427 insertions(+), 145 deletions(-) delete mode 100644 src/leap/bitmask/core/web/README create mode 100644 src/leap/bitmask/core/web/_auth.py create mode 100644 src/leap/bitmask/core/web/api.py delete mode 100644 src/leap/bitmask/core/web/index.html create mode 100644 src/leap/bitmask/core/web/service.py create mode 100644 src/leap/bitmask/core/web/static/README create mode 100644 src/leap/bitmask/core/web/static/__init__.py create mode 100644 src/leap/bitmask/core/web/static/index.html (limited to 'src/leap/bitmask/core/web') diff --git a/src/leap/bitmask/core/web/README b/src/leap/bitmask/core/web/README deleted file mode 100644 index 2b99926..0000000 --- a/src/leap/bitmask/core/web/README +++ /dev/null @@ -1,14 +0,0 @@ -This is a simple html based console that uses the bitmask.js library, which -uses the REST api exposed by the HTTPRequestDispatcher. This html should be -served when leap.bitmask_js package is not found on the import path from where -bitmask.core is executed. - -The development of bitmask_js is in the ui/ folder in this bitmask-dev repo. - -A pre-compiled version of the whole html+js ui can be found in the -leap.bitmask_js package. To have it served from the same endpoint than the REST -api, just install leap.bitmask_js in your environment. - -This 'web console' remains here to be able to develop against the REST api -without the need of installing the full-fledged bitmask_js package. However, -it's only going to work when running from the git repository. 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/index.html b/src/leap/bitmask/core/web/index.html deleted file mode 100644 index 9951a9b..0000000 --- a/src/leap/bitmask/core/web/index.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - Bitmask.js example - - - - -

Bitmask Control Panel

- -
-

Provider:

-
- - - - -
-

Email address:

-

Password:

-
- - - -

-   
-
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 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/static/README b/src/leap/bitmask/core/web/static/README
new file mode 100644
index 0000000..2b99926
--- /dev/null
+++ b/src/leap/bitmask/core/web/static/README
@@ -0,0 +1,14 @@
+This is a simple html based console that uses the bitmask.js library, which
+uses the REST api exposed by the HTTPRequestDispatcher. This html should be
+served when leap.bitmask_js package is not found on the import path from where
+bitmask.core is executed. 
+
+The development of bitmask_js is in the ui/ folder in this bitmask-dev repo.
+
+A pre-compiled version of the whole html+js ui can be found in the
+leap.bitmask_js package. To have it served from the same endpoint than the REST
+api, just install leap.bitmask_js in your environment.
+
+This 'web console' remains here to be able to develop against the REST api
+without the need of installing the full-fledged bitmask_js package. However,
+it's only going to work when running from the git repository.
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
diff --git a/src/leap/bitmask/core/web/static/index.html b/src/leap/bitmask/core/web/static/index.html
new file mode 100644
index 0000000..9951a9b
--- /dev/null
+++ b/src/leap/bitmask/core/web/static/index.html
@@ -0,0 +1,131 @@
+
+
+   
+      Bitmask.js example
+      
+      
+   
+   
+      

Bitmask Control Panel

+ +
+

Provider:

+
+ + + + +
+

Email address:

+

Password:

+
+ + + +

+   
+
-- 
cgit v1.2.3