[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [ooni-probe/master] Add support for initialization of ooniprobe
commit 27a299c5c7d195860e63aa0b1d316f3255e4857d
Author: Arturo Filastò <arturo@xxxxxxxxxxx>
Date: Wed Aug 3 13:05:31 2016 +0200
Add support for initialization of ooniprobe
---
data/decks/web.yaml | 1 +
ooni/agent/scheduler.py | 25 +++---
ooni/scripts/ooniprobe.py | 4 +-
ooni/settings.py | 191 ++++++++++++++++++++++++++++++++++++++----
ooni/tests/__init__.py | 7 --
ooni/ui/cli.py | 52 +++++++++++-
ooni/ui/web/client/index.html | 2 +-
ooni/ui/web/server.py | 82 +++++++++++++++++-
ooni/utils/__init__.py | 1 -
9 files changed, 329 insertions(+), 36 deletions(-)
diff --git a/data/decks/web.yaml b/data/decks/web.yaml
index a81b8f8..c7b9bdc 100644
--- a/data/decks/web.yaml
+++ b/data/decks/web.yaml
@@ -2,6 +2,7 @@
name: Web related ooniprobe tests
description: This deck runs HTTP Header Field Manipulation, HTTP Invalid
Request and the Web Connectivity test
+schedule: "@daily"
tasks:
- name: Runs the HTTP Header Field Manipulation test
ooni:
diff --git a/ooni/agent/scheduler.py b/ooni/agent/scheduler.py
index a6d689f..1f51bd4 100644
--- a/ooni/agent/scheduler.py
+++ b/ooni/agent/scheduler.py
@@ -22,9 +22,11 @@ class ScheduledTask(object):
schedule = None
identifier = None
- def __init__(self, schedule=None):
+ def __init__(self, schedule=None, identifier=None):
if schedule is not None:
self.schedule = schedule
+ if identifier is not None:
+ self.identifier = identifier
assert self.identifier is not None, "self.identifier must be set"
assert self.schedule is not None, "self.schedule must be set"
@@ -120,23 +122,23 @@ class DeleteOldReports(ScheduledTask):
measurement_path.child(measurement['id']).remove()
-class RunDecks(ScheduledTask):
+class RunDeck(ScheduledTask):
"""
This will run the decks that have been configured on the system as the
decks to run by default.
"""
- schedule = '@daily'
- identifier = 'run-decks'
- def __init__(self, director, schedule=None):
- super(RunDecks, self).__init__(schedule)
+ def __init__(self, director, deck_id, schedule):
+ self.deck_id = deck_id
self.director = director
+ identifier = 'run-deck-' + deck_id
+ super(RunDeck, self).__init__(schedule, identifier)
@defer.inlineCallbacks
def task(self):
- for deck_id, deck in deck_store.list_enabled():
- yield deck.setup()
- yield deck.run(self.director)
+ deck = deck_store.get(self.deck_id)
+ yield deck.setup()
+ yield deck.run(self.director)
class SendHeartBeat(ScheduledTask):
"""
@@ -215,7 +217,10 @@ class SchedulerService(service.MultiService):
self.schedule(UpdateInputsAndResources())
self.schedule(UploadReports())
self.schedule(DeleteOldReports())
- self.schedule(RunDecks(self.director))
+ for deck_id, deck in deck_store.list_enabled():
+ if deck.schedule is None:
+ continue
+ self.schedule(RunDeck(self.director, deck_id, deck.schedule))
self._looping_call.start(self.interval)
diff --git a/ooni/scripts/ooniprobe.py b/ooni/scripts/ooniprobe.py
index f5d5b59..430252a 100644
--- a/ooni/scripts/ooniprobe.py
+++ b/ooni/scripts/ooniprobe.py
@@ -6,12 +6,14 @@ from twisted.internet import task, defer
def ooniprobe(reactor):
from ooni.ui.cli import runWithDaemonDirector, runWithDirector
- from ooni.ui.cli import setupGlobalOptions
+ from ooni.ui.cli import setupGlobalOptions, initializeOoniprobe
global_options = setupGlobalOptions(logging=True, start_tor=True,
check_incoherences=True)
if global_options['queue']:
return runWithDaemonDirector(global_options)
+ elif global_options['initialize']:
+ return initializeOoniprobe(global_options)
elif global_options['web-ui']:
from ooni.scripts.ooniprobe_agent import WEB_UI_URL
from ooni.scripts.ooniprobe_agent import status_agent, start_agent
diff --git a/ooni/settings.py b/ooni/settings.py
index 2161560..8bb3340 100644
--- a/ooni/settings.py
+++ b/ooni/settings.py
@@ -13,6 +13,125 @@ from ooni.utils.net import ConnectAndCloseProtocol, connectProtocol
from ooni.utils import Storage, log, get_ooni_root
from ooni import errors
+
+CONFIG_FILE_TEMPLATE = """\
+# This is the configuration file for OONIProbe
+# This file follows the YAML markup format: http://yaml.org/spec/1.2/spec.html
+# Keep in mind that indentation matters.
+
+basic:
+ # Where OONIProbe should be writing it's log file
+ logfile: {logfile}
+ loglevel: WARNING
+privacy:
+ # Should we include the IP address of the probe in the report?
+ includeip: {include_ip}
+ # Should we include the ASN of the probe in the report?
+ includeasn: {include_asn}
+ # Should we include the country as reported by GeoIP in the report?
+ includecountry: {include_country}
+ # Should we collect a full packet capture on the client?
+ #includepcap: false
+reports:
+ # Should we place a unique ID inside of every report
+ #unique_id: true
+ # This is a prefix for each packet capture file (.pcap) per test:
+ #pcap: null
+ #collector: null
+ # Should we be uploading reports to the collector by default?
+ upload: {should_upload}
+advanced:
+ #debug: false
+ # enable if auto detection fails
+ #tor_binary: /usr/sbin/tor
+ #obfsproxy_binary: /usr/bin/obfsproxy
+ # For auto detection
+ # interface: auto
+ # Of specify a specific interface
+ # interface: wlan0
+ # If you do not specify start_tor, you will have to have Tor running and
+ # explicitly set the control port and SOCKS port
+ #start_tor: true
+ # After how many seconds we should give up on a particular measurement
+ #measurement_timeout: 120
+ # After how many retries we should give up on a measurement
+ #measurement_retries: 2
+ # How many measurements to perform concurrently
+ #measurement_concurrency: 4
+ # After how may seconds we should give up reporting
+ #reporting_timeout: 360
+ # After how many retries to give up on reporting
+ #reporting_retries: 5
+ # How many reports to perform concurrently
+ #reporting_concurrency: 7
+ # If we should support communicating to plaintext backends (via HTTP)
+ # insecure_backend: false
+ # The preferred backend type, can be one of onion, https or cloudfront
+ preferred_backend: {preferred_backend}
+tor:
+ #socks_port: 8801
+ #control_port: 8802
+ # Specify the absolute path to the Tor bridges to use for testing
+ #bridges: bridges.list
+ # Specify path of the tor datadirectory.
+ # This should be set to something to avoid having Tor download each time
+ # the descriptors and consensus data.
+ #data_dir: ~/.tor/
+ #
+ # This is the timeout after which we consider to to not have
+ # bootstrapped properly.
+ #timeout: 200
+ torrc:
+ #HTTPProxy: host:port
+ #HTTPProxyAuthenticator: user:password
+ #HTTPSProxy: host:port
+ #HTTPSProxyAuthenticator: user:password
+ #UseBridges: 1
+ #Bridge:
+ #- "meek_lite 0.0.2.0:1 url=https://meek-reflect.appspot.com/ front=www.google.com"
+ #- "meek_lite 0.0.2.0:2 url=https://d2zfqthxsdq309.cloudfront.net/ front=a0.awsstatic.com"
+ #- "meek_lite 0.0.2.0:3 url=https://az786092.vo.msecnd.net/ front=ajax.aspnetcdn.com"
+ #ClientTransportPlugin: "meek_lite exec /usr/bin/obfs4proxy"
+"""
+
+defaults = {
+ "basic": {
+ "loglevel": "WARNING",
+ "logfile": "ooniprobe.log"
+ },
+ "privacy": {
+ "includeip": False,
+ "includeasn": True,
+ "includecountry": True,
+ "includepcap": False
+ },
+ "reports": {
+ "unique_id": True,
+ "pcap": None,
+ "collector": None,
+ "upload": True
+ },
+ "advanced": {
+ "debug": False,
+ "tor_binary": None,
+ "obfsproxy_binary": None,
+ "interface": "auto",
+ "start_tor": True,
+ "measurement_timeout": 120,
+ "measurement_retries": 2,
+ "measurement_concurrency": 4,
+ "reporting_timeout": 360,
+ "reporting_retries": 5,
+ "reporting_concurrency": 7,
+ "insecure_backend": False,
+ "preferred_backend": "onion"
+ },
+ "tor": {
+ "timeout": 200,
+ "torrc": {}
+ }
+}
+
class OConfig(object):
_custom_home = None
@@ -39,6 +158,17 @@ class OConfig(object):
return settings.get(category, option)
return None
+ def is_initialized(self):
+ # When this is false it means that the user has not gone
+ # through the steps of acquiring informed consent and
+ # initializing this ooniprobe installation.
+ initialized_path = os.path.join(self.running_path, 'initialized')
+ return os.path.exists(initialized_path)
+
+ def set_initialized(self):
+ initialized_path = os.path.join(self.running_path, 'initialized')
+ with open(initialized_path, 'w+'): pass
+
@property
def var_lib_path(self):
if hasattr(sys, 'real_prefix'):
@@ -149,7 +279,8 @@ class OConfig(object):
config_file = self.global_options['configfile']
self.config_file = expanduser(config_file)
else:
- self.config_file = os.path.join(self.ooni_home, 'ooniprobe.conf')
+ self.config_file = os.path.join(self.running_path,
+ 'ooniprobe.conf')
if 'logfile' in self.basic:
self.basic.logfile = expanduser(
@@ -183,6 +314,33 @@ class OConfig(object):
if exc.errno != 17:
raise
+ def create_config_file(self, include_ip=False, include_asn=True,
+ include_country=True, should_upload=True,
+ preferred_backend="onion"):
+ def _bool_to_yaml(value):
+ if value is True:
+ return 'true'
+ elif value is False:
+ return 'false'
+ else:
+ return 'null'
+ # Convert the boolean value to their YAML string representation
+ include_ip = _bool_to_yaml(include_ip )
+ include_asn = _bool_to_yaml(include_asn)
+ include_country = _bool_to_yaml(include_country)
+ should_upload = _bool_to_yaml(should_upload)
+
+ logfile = os.path.join(self.running_path, 'ooniprobe.log')
+ with open(self.config_file, 'w+') as out_file:
+ out_file.write(
+ CONFIG_FILE_TEMPLATE.format(logfile=logfile,
+ include_ip=include_ip,
+ include_asn=include_asn,
+ include_country=include_country,
+ should_upload=should_upload,
+ preferred_backend=preferred_backend)
+ )
+ self.read_config_file()
def _create_config_file(self):
target_config_file = self.config_file
@@ -200,19 +358,24 @@ class OConfig(object):
w.write(line)
def read_config_file(self, check_incoherences=False):
- if not os.path.isfile(self.config_file):
- print "Configuration file does not exist."
- self._create_config_file()
- self.read_config_file()
-
- with open(self.config_file) as f:
- config_file_contents = '\n'.join(f.readlines())
- configuration = yaml.safe_load(config_file_contents)
-
- for setting in configuration.keys():
- if setting in dir(self) and configuration[setting] is not None:
- for k, v in configuration[setting].items():
- getattr(self, setting)[k] = v
+ #if not os.path.isfile(self.config_file):
+ # print "Configuration file does not exist."
+ # self._create_config_file()
+ # self.read_config_file()
+
+ configuration = {}
+ if os.path.isfile(self.config_file):
+ with open(self.config_file) as f:
+ config_file_contents = '\n'.join(f.readlines())
+ configuration = yaml.safe_load(config_file_contents)
+
+ for category in defaults.keys():
+ for k, v in defaults[category].items():
+ try:
+ value = configuration.get(category, {})[k]
+ except KeyError:
+ value = v
+ getattr(self, category)[k] = value
self.set_paths()
if check_incoherences:
diff --git a/ooni/tests/__init__.py b/ooni/tests/__init__.py
index e7fd48b..b5dbab4 100644
--- a/ooni/tests/__init__.py
+++ b/ooni/tests/__init__.py
@@ -1,11 +1,4 @@
import socket
-from ooni.settings import config
-
-config.initialize_ooni_home('ooni_home')
-config.read_config_file()
-config.logging = False
-config.advanced.debug = False
-
def is_internet_connected():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
diff --git a/ooni/ui/cli.py b/ooni/ui/cli.py
index 3eccf9a..8cd3358 100644
--- a/ooni/ui/cli.py
+++ b/ooni/ui/cli.py
@@ -30,7 +30,9 @@ class Options(usage.Options):
["list", "s", "List the currently installed ooniprobe "
"nettests"],
["verbose", "v", "Show more verbose information"],
- ["web-ui", "w", "Start the web UI"]
+ ["web-ui", "w", "Start the web UI"],
+ ["initialize", "z", "Initialize ooniprobe to begin running "
+ "it"],
]
optParameters = [
@@ -178,10 +180,58 @@ def director_startup_other_failures(failure):
log.err("An unhandled exception occurred while starting the director!")
log.exception(failure)
+
+def initializeOoniprobe(global_options):
+ # XXX print here the informed consent documentation.
+ answer = raw_input('Should we upload measurements to a collector? (Y/n) ')
+ should_upload = True
+ if answer.lower().startswith("n"):
+ should_upload = False
+
+ answer = raw_input('Should we include your IP in measurements? (y/N) ')
+ include_ip = False
+ if answer.lower().startswith("y"):
+ include_ip = True
+
+ answer = raw_input('Should we include your ASN (your network) in '
+ 'measurements? (Y/n) ')
+ include_asn = False
+ if answer.lower().startswith("n"):
+ include_asn = True
+
+ answer = raw_input('Should we include your Country in '
+ 'measurements? (Y/n) ')
+ include_country = False
+ if answer.lower().startswith("n"):
+ include_country = True
+
+ answer = raw_input('How would you like reports to be uploaded? (onion, '
+ 'https, cloudfronted) ')
+
+ preferred_backend = 'onion'
+ if answer.lower().startswith("https"):
+ preferred_backend = 'https'
+ elif answer.lower().startswith("cloudfronted"):
+ preferred_backend = 'cloudfronted'
+
+ config.create_config_file(include_ip=include_ip,
+ include_asn=include_asn,
+ include_country=include_country,
+ should_upload=should_upload,
+ preferred_backend=preferred_backend)
+ config.set_initialized()
+
def setupGlobalOptions(logging, start_tor, check_incoherences):
global_options = parseOptions()
config.global_options = global_options
+
+ if not config.is_initialized():
+ log.err("You first need to agree to the informed consent and setup "
+ "ooniprobe to run it.")
+ global_options['initialize'] = True
+ return
+
config.set_paths()
config.initialize_ooni_home()
try:
diff --git a/ooni/ui/web/client/index.html b/ooni/ui/web/client/index.html
index e363ba0..6a7c149 100644
--- a/ooni/ui/web/client/index.html
+++ b/ooni/ui/web/client/index.html
@@ -13,5 +13,5 @@
<app>
Loading...
</app>
- <script type="text/javascript" src="app.bundle.js?9d3ccb3bc67af5ed4453"></script></body>
+ <script type="text/javascript" src="app.bundle.js?9c4ed560c98eaf61a836"></script></body>
</html>
diff --git a/ooni/ui/web/server.py b/ooni/ui/web/server.py
index a03daa4..ed2193e 100644
--- a/ooni/ui/web/server.py
+++ b/ooni/ui/web/server.py
@@ -71,6 +71,39 @@ def xsrf_protect(check=True):
return deco
+def _requires_value(value, attrs=[]):
+
+ def deco(f):
+
+ @wraps(f)
+ def wrapper(instance, request, *a, **kw):
+ for attr in attrs:
+ attr_value = getattr(instance, attr)
+ if attr_value is not value:
+ raise WebUIError(400, "{0} must be {1}".format(attr,
+ value))
+ return f(instance, request, *a, **kw)
+
+ return wrapper
+
+ return deco
+
+def requires_true(attrs=[]):
+ """
+ This decorator is used to require that a certain set of class attributes are
+ set to True.
+ Otherwise it will trigger a WebUIError.
+ """
+ return _requires_value(True, attrs)
+
+def requires_false(attrs=[]):
+ """
+ This decorator is used to require that a certain set of class attributes are
+ set to False.
+ Otherwise it will trigger a WebUIError.
+ """
+ return _requires_value(False, attrs)
+
class LongPoller(object):
def __init__(self, timeout, _reactor=reactor):
@@ -128,6 +161,7 @@ class WebUIAPI(object):
for _ in range(30)])
self._director_started = False
+ self._is_initialized = config.is_initialized()
self.status_poller = LongPoller(
self._long_polling_timeout, _reactor)
@@ -139,6 +173,10 @@ class WebUIAPI(object):
self.status_poller.start()
self.director.subscribe(self.handle_director_event)
+ if self._is_initialized:
+ self.start_director()
+
+ def start_director(self):
d = self.director.start()
d.addCallback(self.director_started)
@@ -151,7 +189,8 @@ class WebUIAPI(object):
"software_name": "ooniprobe",
"asn": probe_ip.geodata['asn'],
"country_code": probe_ip.geodata['countrycode'],
- "director_started": self._director_started
+ "director_started": self._director_started,
+ "initialized": self._is_initialized
}
def handle_director_event(self, event):
@@ -208,8 +247,36 @@ class WebUIAPI(object):
d.addCallback(got_status_update)
return d
+ @app.route('/api/initialize', methods=["POST"])
+ @xsrf_protect(check=True)
+ @requires_false(attrs=['_is_initialized'])
+ def api_initialize(self, request):
+ try:
+ initial_configuration = json.load(request.content)
+ except ValueError:
+ raise WebUIError(400, 'Invalid JSON message recevied')
+
+ required_keys = ['include_ip', 'include_asn', 'include_country',
+ 'should_upload', 'preferred_backend']
+ options = {}
+ for required_key in required_keys:
+ try:
+ options[required_key] = initial_configuration[required_key]
+ except KeyError:
+ raise WebUIError(400, 'Missing required key {0}'.format(
+ required_key))
+ config.create_config_file(**options)
+ config.set_initialized()
+
+ self._is_initialized = True
+
+ self.status_poller.notify()
+ self.start_director()
+ return self.render_json({"result": "ok"}, request)
+
@app.route('/api/deck/<string:deck_id>/start', methods=["POST"])
@xsrf_protect(check=True)
+ @requires_true(attrs=['_director_started', '_is_initialized'])
def api_deck_start(self, request, deck_id):
try:
deck = self.director.deck_store.get(deck_id)
@@ -225,6 +292,7 @@ class WebUIAPI(object):
@app.route('/api/deck', methods=["GET"])
@xsrf_protect(check=False)
+ @requires_true(attrs=['_director_started', '_is_initialized'])
def api_deck_list(self, request):
deck_list = {
'available': {},
@@ -246,6 +314,7 @@ class WebUIAPI(object):
@app.route('/api/deck/<string:deck_id>/enable', methods=["POST"])
@xsrf_protect(check=True)
+ @requires_true(attrs=['_director_started', '_is_initialized'])
def api_deck_enable(self, request, deck_id):
try:
self.director.deck_store.enable(deck_id)
@@ -256,6 +325,7 @@ class WebUIAPI(object):
@app.route('/api/deck/<string:deck_id>/disable', methods=["POST"])
@xsrf_protect(check=True)
+ @requires_true(attrs=['_director_started', '_is_initialized'])
def api_deck_disable(self, request, deck_id):
try:
self.director.deck_store.disable(deck_id)
@@ -276,6 +346,7 @@ class WebUIAPI(object):
@app.route('/api/nettest/<string:test_name>/start', methods=["POST"])
@xsrf_protect(check=True)
+ @requires_true(attrs=['_director_started', '_is_initialized'])
def api_nettest_start(self, request, test_name):
try:
_ = self.director.netTests[test_name]
@@ -321,11 +392,13 @@ class WebUIAPI(object):
@app.route('/api/nettest', methods=["GET"])
@xsrf_protect(check=False)
+ @requires_true(attrs=['_director_started', '_is_initialized'])
def api_nettest_list(self, request):
return self.render_json(self.director.netTests, request)
@app.route('/api/input', methods=["GET"])
@xsrf_protect(check=False)
+ @requires_true(attrs=['_is_initialized'])
def api_input_list(self, request):
input_store_list = self.director.input_store.list()
for key, value in input_store_list.items():
@@ -334,6 +407,7 @@ class WebUIAPI(object):
@app.route('/api/input/<string:input_id>/content', methods=["GET"])
@xsrf_protect(check=False)
+ @requires_true(attrs=['_is_initialized'])
def api_input_content(self, request, input_id):
content = self.director.input_store.getContent(input_id)
request.setHeader('Content-Type', 'text/plain')
@@ -342,6 +416,7 @@ class WebUIAPI(object):
@app.route('/api/input/<string:input_id>', methods=["GET"])
@xsrf_protect(check=False)
+ @requires_true(attrs=['_is_initialized'])
def api_input_details(self, request, input_id):
return self.render_json(
self.director.input_store.get(input_id), request
@@ -349,12 +424,14 @@ class WebUIAPI(object):
@app.route('/api/measurement', methods=["GET"])
@xsrf_protect(check=False)
+ @requires_true(attrs=['_is_initialized'])
def api_measurement_list(self, request):
measurements = list_measurements()
return self.render_json({"measurements": measurements}, request)
@app.route('/api/measurement/<string:measurement_id>', methods=["GET"])
@xsrf_protect(check=False)
+ @requires_true(attrs=['_is_initialized'])
def api_measurement_summary(self, request, measurement_id):
try:
measurement = get_measurement(measurement_id)
@@ -373,6 +450,7 @@ class WebUIAPI(object):
@app.route('/api/measurement/<string:measurement_id>', methods=["DELETE"])
@xsrf_protect(check=True)
+ @requires_true(attrs=['_is_initialized'])
def api_measurement_delete(self, request, measurement_id):
try:
measurement = get_measurement(measurement_id)
@@ -394,6 +472,7 @@ class WebUIAPI(object):
@app.route('/api/measurement/<string:measurement_id>/keep', methods=["POST"])
@xsrf_protect(check=True)
+ @requires_true(attrs=['_is_initialized'])
def api_measurement_keep(self, request, measurement_id):
try:
measurement_dir = self.measurement_path.child(measurement_id)
@@ -409,6 +488,7 @@ class WebUIAPI(object):
@app.route('/api/measurement/<string:measurement_id>/<int:idx>',
methods=["GET"])
@xsrf_protect(check=False)
+ @requires_true(attrs=['_is_initialized'])
def api_measurement_view(self, request, measurement_id, idx):
try:
measurement_dir = self.measurement_path.child(measurement_id)
diff --git a/ooni/utils/__init__.py b/ooni/utils/__init__.py
index 35d419d..d672ca8 100644
--- a/ooni/utils/__init__.py
+++ b/ooni/utils/__init__.py
@@ -56,7 +56,6 @@ class Storage(dict):
for (k, v) in value.items():
self[k] = v
-
def checkForRoot():
if os.getuid() != 0:
raise errors.InsufficientPrivileges
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits