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.py | 268 --------------------------- src/leap/bitmask/core/service.py | 4 +- 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 +++++++++++++ 11 files changed, 429 insertions(+), 415 deletions(-) delete mode 100644 src/leap/bitmask/core/_web.py 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 diff --git a/src/leap/bitmask/core/_web.py b/src/leap/bitmask/core/_web.py deleted file mode 100644 index 11a7be1..0000000 --- a/src/leap/bitmask/core/_web.py +++ /dev/null @@ -1,268 +0,0 @@ -# -*- coding: utf-8 -*- -# _web.py -# Copyright (C) 2016 LEAP Encryption Access 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 json -import os -import pkg_resources - -from twisted.application import service - -from twisted.internet import endpoints -from twisted.cred import portal, checkers, credentials, error as credError -from twisted.internet import reactor, defer -from twisted.logger import Logger -from twisted.web.guard import HTTPAuthSessionWrapper, BasicCredentialFactory -from twisted.web.resource import IResource, Resource -from twisted.web.server import Site, NOT_DONE_YET -from twisted.web.static import File - -from zope.interface import implementer - -from leap.bitmask.util import here -from leap.bitmask.core.dispatcher import CommandDispatcher - -try: - import leap.bitmask_js - HAS_WEB_UI = True -except ImportError: - HAS_WEB_UI = False - -try: - import txtorcon -except Exception: - pass - -log = Logger() - - -class TokenCredentialFactory(BasicCredentialFactory): - scheme = 'token' - - -@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() - - -@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 - - -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')) - 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} - - -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() - - -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/service.py b/src/leap/bitmask/core/service.py index 705dcb5..9682c18 100644 --- a/src/leap/bitmask/core/service.py +++ b/src/leap/bitmask/core/service.py @@ -29,9 +29,9 @@ from twisted.logger import Logger from leap.bitmask import __version__ from leap.bitmask.core import configurable from leap.bitmask.core import _zmq -from leap.bitmask.core import _web from leap.bitmask.core import flags from leap.bitmask.core import _session +from leap.bitmask.core.web.service import HTTPDispatcherService from leap.common.events import server as event_server # from leap.vpn import EIPService @@ -175,7 +175,7 @@ class BitmaskBackend(configurable.ConfigurableService): zs.setServiceParent(self) def _init_web(self, onion=False): - service = _web.HTTPDispatcherService + service = HTTPDispatcherService self._maybe_init_service('web', service, self, onion=onion) def _init_websockets(self): 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