[tor-commits] [ooni-probe/master] Add support for starting tests from the HTTP API

art at torproject.org art at torproject.org
Mon Sep 19 12:14:24 UTC 2016


commit b2118234e7b85ad4381ef2caddb9ec37e131fba4
Author: Arturo Filastò <arturo at filasto.net>
Date:   Fri Apr 1 19:48:14 2016 +0200

    Add support for starting tests from the HTTP API
---
 ooni/director.py      |   2 +-
 ooni/web/resources.py | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++
 ooni/web/root.py      |  40 ++++++++++++
 ooni/webui.py         |  57 +++++++++++++++++
 4 files changed, 270 insertions(+), 1 deletion(-)

diff --git a/ooni/director.py b/ooni/director.py
index d6503f2..9897ffd 100644
--- a/ooni/director.py
+++ b/ooni/director.py
@@ -134,7 +134,7 @@ class Director(object):
             elif config.tor.control_port and config.tor_state is None:
                 yield connect_to_control_port()
 
-        if config.global_options['no-geoip']:
+        if config.global_options.get('no-geoip'):
             aux = [False]
             if config.global_options.get('annotations') is not None:
                 annotations = [k.lower() for k in config.global_options['annotations'].keys()]
diff --git a/ooni/web/resources.py b/ooni/web/resources.py
new file mode 100644
index 0000000..336fdc0
--- /dev/null
+++ b/ooni/web/resources.py
@@ -0,0 +1,172 @@
+import json
+from twisted.web import resource
+from twisted.python import usage
+
+from ooni import errors
+from ooni.nettest import NetTestLoader
+
+
+class WuiResource(resource.Resource):
+    isLeaf = True
+    XSRF_HEADER = 'X-XSRF-TOKEN'
+    # XXX set this to true when stable version
+    XSRF_PROTECTION = False
+
+    def __init__(self, director):
+        self.director = director
+        resource.Resource.__init__(self)
+
+    def check_xsrf(self, request):
+        if self.XSRF_PROTECTION is False:
+            return True
+        if request.requestHeaders.hasHeader(self.XSRF_HEADER):
+            return True
+        return False
+
+    def render(self, request):
+        if not self.check_xsrf(request):
+            obj = {
+                'error_code': 400,
+                'error_message': ('Missing cross site request forgery '
+                                  'header \'{}\''.format(self.XSRF_HEADER))
+            }
+            request.setResponseCode(403)
+            return self.render_json(obj, request)
+        obj = resource.Resource.render(self, request)
+        return self.render_json(obj, request)
+
+    def render_json(self, obj, request):
+        json_string = json.dumps(obj) + "\n"
+        request.setHeader('Content-Type', 'application/json')
+        request.setHeader('Content-Length', len(json_string))
+        return json_string
+
+
+class DecksGenerate(WuiResource):
+    def render_GET(self, request):
+        return {"generate": "deck"}
+
+
+class DecksStart(WuiResource):
+    def __init__(self, director, deck_name):
+        WuiResource.__init__(self, director)
+        self.deck_name = deck_name
+
+    def render_GET(self, request):
+        return {"start": self.deck_name}
+
+
+class DecksStop(WuiResource):
+    def __init__(self, director, deck_id):
+        WuiResource.__init__(self, director)
+        self.deck_id = deck_id
+
+    def render_GET(self, request):
+        return {"stop": self.deck_id}
+
+
+class DecksStatus(WuiResource):
+    def __init__(self, director, deck_name):
+        WuiResource.__init__(self, director)
+        self.deck_name = deck_name
+
+    def render_GET(self, request):
+        return {"deck": self.deck_name}
+
+
+class DecksList(WuiResource):
+    def render_GET(self, request):
+        return {"deck": "list"}
+
+
+def getNetTestLoader(test_options, test_file):
+    """
+    Args:
+        test_options: (dict) containing as keys the option names.
+
+        test_file: (string) the path to the test_file to be run.
+    Returns:
+        an instance of :class:`ooni.nettest.NetTestLoader` with the specified
+        test_file and the specified options.
+        """
+    options = []
+    for k, v in test_options.items():
+        options.append('--'+k)
+        options.append(v)
+
+    net_test_loader = NetTestLoader(options,
+            test_file=test_file)
+    return net_test_loader
+
+class TestsStart(WuiResource):
+    def __init__(self, director, test_name):
+        WuiResource.__init__(self, director)
+        self.test_name = test_name
+
+    def render_POST(self, request):
+        try:
+            net_test = self.director.netTests[self.test_name]
+        except KeyError:
+            request.setResponseCode(500)
+            return {
+                'error_code': 500,
+                'error_message': 'Could not find the specified test'
+            }
+        test_options = json.load(request.content)
+        net_test_loader = getNetTestLoader(test_options, net_test['path'])
+        try:
+            net_test_loader.checkOptions()
+            # XXX we actually want to generate the report_filename in a smart
+            # way so that we can know where it is located and learn the results
+            # of the measurement.
+            report_filename = None
+            self.director.startNetTest(net_test_loader, report_filename)
+        except errors.MissingRequiredOption, option_name:
+            request.setResponseCode(500)
+            return {
+                'error_code': 501,
+                'error_message': ('Missing required option: '
+                                  '\'{}\''.format(option_name))
+            }
+        except usage.UsageError:
+            request.setResponseCode(500)
+            return {
+                'error_code': 502,
+                'error_message': 'Error in parsing options'
+            }
+        except errors.InsufficientPrivileges:
+            request.setResponseCode(500)
+            return {
+                'error_code': 503,
+                'error_message': 'Insufficient priviledges'
+            }
+
+        return {"deck": "list"}
+
+
+class TestsStop(WuiResource):
+    def __init__(self, director, test_id):
+        WuiResource.__init__(self, director)
+        self.test_id = test_id
+
+    def render_GET(self, request):
+        return {"deck": "list"}
+
+
+class TestsStatus(WuiResource):
+    def __init__(self, director, test_id):
+        WuiResource.__init__(self, director)
+        self.test_id = test_id
+
+    def render_GET(self, request):
+        return {"deck": "list"}
+
+
+class TestsList(WuiResource):
+    def render_GET(self, request):
+        return self.director.netTests
+
+
+class Results(WuiResource):
+    def render_GET(self, request):
+        return {"result": "bar"}
diff --git a/ooni/web/root.py b/ooni/web/root.py
new file mode 100644
index 0000000..7ceba4a
--- /dev/null
+++ b/ooni/web/root.py
@@ -0,0 +1,40 @@
+import os
+import re
+from twisted.web import resource, static
+
+from .resources import DecksGenerate, DecksStart, DecksStop
+from .resources import DecksStatus, DecksList, TestsStart
+from .resources import TestsStop, TestsStatus, TestsList
+from .resources import Results
+
+
+class OONIProbeWebRoot(resource.Resource):
+    routes = [
+        ('^/decks/generate$', DecksGenerate),
+        ('^/decks/(.*)/start$', DecksStart),
+        ('^/decks/(.*)/stop$', DecksStop),
+        ('^/decks/(.*)$', DecksStatus),
+        ('^/decks$', DecksList),
+        ('^/tests/(.*)/start$', TestsStart),
+        ('^/tests/(.*)/stop$', TestsStop),
+        ('^/tests/(.*)$', TestsStatus),
+        ('^/tests$', TestsList),
+        ('^/results$', Results)
+    ]
+
+    def __init__(self, config, director):
+        resource.Resource.__init__(self)
+
+        self._director = director
+        self._config = config
+        self._route_map = map(lambda x: (re.compile(x[0]), x[1]), self.routes)
+
+        wui_directory = os.path.join(self._config.data_directory, 'ui', 'app')
+        self._static = static.File(wui_directory)
+
+    def getChild(self, path, request):
+        for route, r in self._route_map:
+            match = route.search(request.path)
+            if match:
+                return r(self._director, *match.groups())
+        return self._static.getChild(path, request)
diff --git a/ooni/webui.py b/ooni/webui.py
new file mode 100644
index 0000000..e2fe450
--- /dev/null
+++ b/ooni/webui.py
@@ -0,0 +1,57 @@
+import os
+
+from twisted.scripts import twistd
+from twisted.python import usage
+from twisted.internet import reactor
+from twisted.web import server
+from twisted.application import service
+
+from ooni.web.root import OONIProbeWebRoot
+from ooni.settings import config
+from ooni.director import Director
+from ooni.utils import log
+
+class WebUI(service.MultiService):
+    portNum = 8822
+    def startService(self):
+        service.MultiService.startService(self)
+        config.set_paths()
+        config.initialize_ooni_home()
+        config.read_config_file()
+        def _started(res):
+            log.msg("Director started")
+            root = server.Site(OONIProbeWebRoot(config, director))
+            self._port = reactor.listenTCP(self.portNum, root)
+        director = Director()
+        d = director.start()
+        d.addCallback(_started)
+        d.addErrback(self._startupFailed)
+
+    def _startupFailed(self, err):
+        log.err("Failed to start the director")
+        log.exception(err)
+        os.abort()
+
+    def stopService(self):
+        if self._port:
+            self._port.stopListening()
+
+class StartOoniprobeWebUIPlugin:
+    tapname = "ooniprobe"
+    def makeService(self, so):
+        return WebUI()
+
+class MyTwistdConfig(twistd.ServerOptions):
+    subCommands = [("StartOoniprobeWebUI", None, usage.Options, "ooniprobe web ui")]
+
+def start():
+    twistd_args = ["--nodaemon"]
+    twistd_config = MyTwistdConfig()
+    twistd_args.append("StartOoniprobeWebUI")
+    try:
+        twistd_config.parseOptions(twistd_args)
+    except usage.error, ue:
+        print("ooniprobe: usage error from twistd: {}\n".format(ue))
+    twistd_config.loadedPlugins = {"StartOoniprobeWebUI": StartOoniprobeWebUIPlugin()}
+    twistd.runApp(twistd_config)
+    return 0





More information about the tor-commits mailing list