commit 654147c3a92156c35fc1a8ecc0aac8e933f57e59 Author: Arturo Filastò arturo@filasto.net Date: Wed Nov 7 00:47:59 2012 +0100
Remove all references to the old API --- docs/source/index.rst | 40 ++- nettests/bridge_reachability/echo.py | 5 +- nettests/core/echo.py | 1 - ooni/__init__.py | 7 - ooni/custodiet.py | 421 ------------------------------- ooni/lib/Makefile | 36 --- ooni/nodes.py | 176 +++++++++++++ ooni/oonicli.py | 11 +- ooni/plugoo/__init__.py | 47 ---- ooni/plugoo/assets.py | 62 ----- ooni/plugoo/interface.py | 56 ---- ooni/plugoo/nodes.py | 176 ------------- ooni/plugoo/reports.py | 145 ----------- ooni/plugoo/tests.py | 142 ----------- ooni/plugoo/work.py | 148 ----------- ooni/protocols/http.py | 141 ----------- ooni/protocols/scapyproto.py | 55 ---- ooni/reporter.py | 20 +- ooni/runner.py | 61 +---- ooni/scaffolding.py | 78 ------ ooni/utils/legacy.py | 459 ---------------------------------- 21 files changed, 228 insertions(+), 2059 deletions(-)
diff --git a/docs/source/index.rst b/docs/source/index.rst index 5c96dd1..2497a09 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,26 +15,41 @@ about the various types, methods, and amounts of network tampering in the world. Getting started ***************
-If you choose to use virtualenv to setup your development environment you will -need to do the following:: +Requirements:
- virtualenv ENV - source ENV/bin/activate - pip install twisted Scapy pyyaml pyOpenSSL + * Git: http://git-scm.com/book/en/Getting-Started-Installing-Git + * Python >= 2.6: http://www.python.org/download/releases/ + * pip: http://www.pip-installer.org/en/latest/
-To get the latest version of scapy you will need mercurial. You can then install -it with:: +On debian based systems these can be installed with:
- pip install hg+http://hg.secdev.org/scapy + apt-get install git-core python python-pip python-dev
-On debian you can install all the dependecies with apt-get with this command:: +The python dependencies required for running ooniprobe are:
- apt-get install python-twisted python-twisted-names python-yaml python-scapy python-beautifulsoup + * Twisted + * Scapy + * txtorcon
-Once you have installed all the dependencies OONI tests can be run like so:: +They can be installed from the requirements.txt with:
- bin/ooniprobe path/to/test.py --cmd1 foo --cmd2 bar + pip install -r requirements.txt
+You are highly recommended to do so from inside of a virtual environment, since +pip does not download the packages via SSL and you will need to install it +system wide. + +This will require you to have installed virtualenv. + + apt-get install python-virtualenv + +To create a new virtual environment do + + virtualenv env + +Then install OONI with: + + pip install -r requirements.txt
Contents ******** @@ -45,7 +60,6 @@ Contents
oonib install - tutorial writing_tests api/* glossary diff --git a/nettests/bridge_reachability/echo.py b/nettests/bridge_reachability/echo.py index 611970e..0c20a3f 100644 --- a/nettests/bridge_reachability/echo.py +++ b/nettests/bridge_reachability/echo.py @@ -50,8 +50,7 @@ class EchoTest(ScapyTest): description = 'A simple ICMP-8 test to see if a host is reachable.' version = '0.0.1' inputFile = ['file', 'f', None, 'File of list of IPs to ping'] - requirements = None - #report = Storage() + requiresRoot = True
optParameters = [ ['interface', 'i', None, 'Network interface to use'], @@ -73,7 +72,7 @@ class EchoTest(ScapyTest): if self.localOptions: log.debug("%s: local_options found" % self.name) for key, value in self.localOptions.items(): - log.debug("%s: setting self.%s = %s" % (key, value)) + log.debug("setting self.%s = %s" % (key, value)) setattr(self, key, value)
## xxx is this now .subOptions? diff --git a/nettests/core/echo.py b/nettests/core/echo.py deleted file mode 120000 index d9926cd..0000000 --- a/nettests/core/echo.py +++ /dev/null @@ -1 +0,0 @@ -../../ooni/bridget/tests/echo.py \ No newline at end of file diff --git a/ooni/__init__.py b/ooni/__init__.py index 659d4af..0c9f297 100644 --- a/ooni/__init__.py +++ b/ooni/__init__.py @@ -9,13 +9,6 @@ from . import runner from . import templates from . import utils
-# XXX below are legacy related modules -#from . import ooniprobe -#from . import plugoo -#from . import plugins - __all__ = ['oconfig', 'inputunit', 'kit', 'lib', 'nettest', 'oonicli', 'reporter', 'runner', 'templates', 'utils'] - # XXX below are legacy related modules - #'ooniprobe', 'plugoo', 'plugins'] diff --git a/ooni/custodiet.py b/ooni/custodiet.py deleted file mode 100755 index 8cbcfce..0000000 --- a/ooni/custodiet.py +++ /dev/null @@ -1,421 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -# -# custodiet -# ********* -# -# "...quis custodiet ipsos custodes?" -# - Juvenal, Satires VI.347-348 (circa 2nd Century, C.E.) -# -# "'Hand me the Custodian,' Goodchild demands, inserting the waiflike -# robot into Bambara's opened navel. 'Providing conscience for those who -# have none.' Goodchild and the other Breen government agents disappear -# into the surrounding desert in a vehicle, kicking up cloud of white dust. -# Bambara awakens, and, patting the dust from his clothing, turns to -# greet a one-armed child. 'Hi, my name's Bambara; I'm a -# thirty-six-year-old Virgo and a former killer, who's hobbies include -# performing recreational autopsies, defecating, and drinking rum. I've -# recently been given a conscience, and would very much like to help you.' -# Cut to Bambara and the child, now with one of Bambara's arms, leaving -# a surgical clinic." -# - AeonFlux, "The Purge" (sometime in the late 90s) -# -# :copyright: (c) 2012 Isis Lovecruft -# :license: see LICENSE for more details. -# :version: 0.1.0-beta -# - -# ooniprobe.py imports -import sys -from signal import SIGTERM, signal -from pprint import pprint - -from twisted.python import usage -from twisted.internet import reactor -from twisted.plugin import getPlugins - -from zope.interface.verify import verifyObject -from zope.interface.exceptions import BrokenImplementation -from zope.interface.exceptions import BrokenMethodImplementation - -from ooni.bridget.tests import bridget -from ooni.bridget.utils import log, tests, work, reports -from ooni.bridget.utils.interface import ITest -from ooni.utils.logo import getlogo - -# runner.py imports -import os -import types -import time -import inspect -import yaml - -from twisted.internet import defer, reactor -from twisted.python import reflect, failure, usage -from twisted.python import log as tlog - -from twisted.trial import unittest -from twisted.trial.runner import TrialRunner, TestLoader -from twisted.trial.runner import isPackage, isTestCase, ErrorHolder -from twisted.trial.runner import filenameToModule, _importFromFile - -from ooni import nettest -from ooni.inputunit import InputUnitFactory -from ooni.nettest import InputTestSuite -from ooni.plugoo import tests as oonitests -from ooni.reporter import ReporterFactory -from ooni.utils import log, geodata, date -from ooni.utils.legacy import LegacyOONITest -from ooni.utils.legacy import start_legacy_test, adapt_legacy_test - - -__version__ = "0.1.0-beta" - - -#def retrieve_plugoo(): -# """ -# Get all the plugins that implement the ITest interface and get the data -# associated to them into a dict. -# """ -# interface = ITest -# d = {} -# error = False -# for p in getPlugins(interface, plugins): -# try: -# verifyObject(interface, p) -# d[p.shortName] = p -# except BrokenImplementation, bi: -# print "Plugin Broken" -# print bi -# error = True -# if error != False: -# print "Plugin Loaded!" -# return d -# -#plugoo = retrieve_plugoo() - -""" - -ai to watch over which tests to run - custodiet - - * runTest() or getPrefixMethodNames() to run the tests in order for each - test (esp. the tcp and icmp parts) to be oonicompat we should use the - test_icmp_ping API framework for those. - - * should handle calling - -tests to run: - echo - syn - fin - conn - tls - tor -need fakebridge - canary - -""" - -def runTest(test, options, global_options, reactor=reactor): - """ - Run an OONI probe test by name. - - @param test: a string specifying the test name as specified inside of - shortName. - - @param options: the local options to be passed to the test. - - @param global_options: the global options for OONI - """ - parallelism = int(global_options['parallelism']) - worker = work.Worker(parallelism, reactor=reactor) - test_class = plugoo[test].__class__ - report = reports.Report(test, global_options['output']) - - log_to_stdout = True - if global_options['quiet']: - log_to_stdout = False - - log.start(log_to_stdout, - global_options['log'], - global_options['verbosity']) - - resume = 0 - if not options: - options = {} - if 'resume' in options: - resume = options['resume'] - - test = test_class(options, global_options, report, reactor=reactor) - if test.tool: - test.runTool() - return True - - if test.ended: - print "Ending test" - return None - - wgen = work.WorkGenerator(test, - dict(options), - start=resume) - for x in wgen: - worker.push(x) - -class MainOptions(usage.Options): - tests = [bridget, ] - subCommands = [] - for test in tests: - print test - testopt = getattr(test, 'options') - subCommands.append([test, None, testopt, "Run the %s test" % test]) - - optFlags = [ - ['quiet', 'q', "Don't log to stdout"] - ] - - optParameters = [ - ['parallelism', 'n', 10, "Specify the number of parallel tests to run"], - #['target-node', 't', 'localhost:31415', 'Select target node'], - ['output', 'o', 'bridge.log', "Specify output report file"], - ['reportfile', 'o', 'bridge.log', "Specify output log file"], - ['verbosity', 'v', 1, "Specify the logging level"], - ] - - def opt_version(self): - """ - Display OONI version and exit. - """ - print "OONI version:", __version__ - sys.exit(0) - - def __str__(self): - """ - Hack to get the sweet ascii art into the help output and replace the - strings "Commands" with "Tests". - """ - return getlogo() + '\n' + self.getSynopsis() + '\n' + \ - self.getUsage(width=None).replace("Commands:", "Tests:") - - - -def isTestCase(thing): - try: - return issubclass(thing, unittest.TestCase) - except TypeError: - return False - -def isLegacyTest(obj): - """ - Returns True if the test in question is written using the OONITest legacy - class. - We do this for backward compatibility of the OONIProbe API. - """ - try: - if issubclass(obj, oonitests.OONITest) and not obj == oonitests.OONITest: - return True - else: - return False - except TypeError: - return False - -def processTest(obj, config): - """ - Process the parameters and :class:`twisted.python.usage.Options` of a - :class:`ooni.nettest.Nettest`. - - :param obj: - An uninstantiated old test, which should be a subclass of - :class:`ooni.plugoo.tests.OONITest`. - :param config: - A configured and instantiated :class:`twisted.python.usage.Options` - class. - """ - - inputFile = obj.inputFile - - if obj.optParameters or inputFile: - if not obj.optParameters: - obj.optParameters = [] - - if inputFile: - obj.optParameters.append(inputFile) - - class Options(usage.Options): - optParameters = obj.optParameters - - options = Options() - options.parseOptions(config['subArgs']) - obj.localOptions = options - - if inputFile: - obj.inputFile = options[inputFile[0]] - try: - tmp_obj = obj() - tmp_obj.getOptions() - except usage.UsageError: - options.opt_help() - - return obj - -def findTestClassesFromConfig(config): - """ - Takes as input the command line config parameters and returns the test - case classes. - If it detects that a certain test class is using the old OONIProbe format, - then it will adapt it to the new testing system. - - :param config: - A configured and instantiated :class:`twisted.python.usage.Options` - class. - :return: - A list of class objects found in a file or module given on the - commandline. - """ - - filename = config['test'] - classes = [] - - module = filenameToModule(filename) - for name, val in inspect.getmembers(module): - if isTestCase(val): - classes.append(processTest(val, config)) - elif isLegacyTest(val): - classes.append(adapt_legacy_test(val, config)) - return classes - -def makeTestCases(klass, tests, methodPrefix): - """ - Takes a class some tests and returns the test cases. methodPrefix is how - the test case functions should be prefixed with. - """ - - cases = [] - for test in tests: - cases.append(klass(methodPrefix+test)) - return cases - -def loadTestsAndOptions(classes, config): - """ - Takes a list of classes and returns their testcases and options. - Legacy tests will be adapted. - """ - - methodPrefix = 'test' - suiteFactory = InputTestSuite - options = [] - testCases = [] - names = [] - - _old_klass_type = LegacyOONITest - - for klass in classes: - if isinstance(klass, _old_klass_type): - try: - cases = start_legacy_test(klass) - #cases.callback() - if cases: - print cases - return [], [] - testCases.append(cases) - except Exception, e: - log.err(e) - else: - try: - opts = klass.local_options - options.append(opts) - except AttributeError, ae: - options.append([]) - log.err(ae) - elif not isinstance(klass, _old_klass_type): - tests = reflect.prefixedMethodNames(klass, methodPrefix) - if tests: - cases = makeTestCases(klass, tests, methodPrefix) - testCases.append(cases) - try: - k = klass() - opts = k.getOptions() - options.append(opts) - except AttributeError, ae: - options.append([]) - log.err(ae) - else: - try: - raise RuntimeError, "Class is some strange type!" - except RuntimeError, re: - log.err(re) - - return testCases, options - -class ORunner(object): - """ - This is a specialized runner used by the ooniprobe command line tool. - I am responsible for reading the inputs from the test files and splitting - them in input units. I also create all the report instances required to run - the tests. - """ - def __init__(self, cases, options=None, config=None, *arg, **kw): - self.baseSuite = InputTestSuite - self.cases = cases - self.options = options - - try: - assert len(options) != 0, "Length of options is zero!" - except AssertionError, ae: - self.inputs = [] - log.err(ae) - else: - try: - first = options.pop(0) - except: - first = {} - if 'inputs' in first: - self.inputs = options['inputs'] - else: - log.msg("Could not find inputs!") - log.msg("options[0] = %s" % first) - self.inputs = [None] - - try: - reportFile = open(config['reportfile'], 'a+') - except: - filename = 'report_'+date.timestamp()+'.yaml' - reportFile = open(filename, 'a+') - self.reporterFactory = ReporterFactory(reportFile, - testSuite=self.baseSuite(self.cases)) - - def runWithInputUnit(self, inputUnit): - idx = 0 - result = self.reporterFactory.create() - - for inputs in inputUnit: - result.reporterFactory = self.reporterFactory - - suite = self.baseSuite(self.cases) - suite.input = inputs - suite(result, idx) - - # XXX refactor all of this index bullshit to avoid having to pass - # this index around. Probably what I want to do is go and make - # changes to report to support the concept of having multiple runs - # of the same test. - # We currently need to do this addition in order to get the number - # of times the test cases that have run inside of the test suite. - idx += (suite._idx - idx) - - result.done() - - def run(self): - self.reporterFactory.options = self.options - for inputUnit in InputUnitFactory(self.inputs): - self.runWithInputUnit(inputUnit) - -if __name__ == "__main__": - config = Options() - config.parseOptions() - - if not config.subCommand: - config.opt_help() - signal(SIGTERM) - #sys.exit(1) - - runTest(config.subCommand, config.subOptions, config) - reactor.run() diff --git a/ooni/lib/Makefile b/ooni/lib/Makefile deleted file mode 100644 index c40b8d2..0000000 --- a/ooni/lib/Makefile +++ /dev/null @@ -1,36 +0,0 @@ -all: txtorcon - -txtraceroute: - echo "Processing dependency txtraceroute..." - git clone https://github.com/hellais/txtraceroute.git txtraceroute.git - mv txtraceroute.git/txtraceroute.py txtraceroute.py - rm -rf txtraceroute.git - -txtorcon: - echo "Processing dependency txtorcon..." - git clone https://github.com/meejah/txtorcon.git txtorcon.git - mv txtorcon.git/txtorcon txtorcon - rm -rf txtorcon.git - -clean: - rm -rf txtorcon -# rm -rf txtraceroute.py -# rm -rf txscapy.py - -cleanall: - rm -rf txtorcon - rm -rf txtraceroute.py - rm -rf txscapy.py - -txscapy: - echo "Processing dependency txscapy" - git clone https://github.com/hellais/txscapy.git txscapy.git - mv txscapy.git/txscapy.py txscapy.py - rm -rf txscapy.git - -#rfc3339: -# echo "Processing RFC3339 dependency" -# hg clone https://bitbucket.org/henry/rfc3339 rfc3339 -# mv rfc3339/rfc3339.py rfc3339.py -# rm -rf rfc3339 - diff --git a/ooni/nodes.py b/ooni/nodes.py new file mode 100644 index 0000000..155f183 --- /dev/null +++ b/ooni/nodes.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 +""" + nodes + ***** + + This contains all the code related to Nodes + both network and code execution. + + :copyright: (c) 2012 by Arturo Filastò, Isis Lovecruft + :license: see LICENSE for more details. + +""" + +import os +from binascii import hexlify + +try: + import paramiko +except: + print "Error: module paramiko is not installed." +from pprint import pprint +import sys +import socks +import xmlrpclib + +class Node(object): + def __init__(self, address, port): + self.address = address + self.port = port + +class LocalNode(object): + def __init__(self): + pass + +""" +[]: node = NetworkNode("192.168.0.112", 5555, "SOCKS5") +[]: node_socket = node.wrap_socket() +""" +class NetworkNode(Node): + def __init__(self, address, port, node_type="SOCKS5", auth_creds=None): + self.node = Node(address,port) + + # XXX support for multiple types + # node type (SOCKS proxy, HTTP proxy, GRE tunnel, ...) + self.node_type = node_type + # type-specific authentication credentials + self.auth_creds = auth_creds + + def _get_socksipy_socket(self, proxy_type, auth_creds): + import socks + s = socks.socksocket() + # auth_creds[0] -> username + # auth_creds[1] -> password + s.setproxy(proxy_type, self.node.address, self.node.port, + self.auth_creds[0], self.auth_creds[1]) + return s + + def _get_socket_wrapper(self): + if (self.node_type.startswith("SOCKS")): # SOCKS proxies + if (self.node_type != "SOCKS5"): + proxy_type = socks.PROXY_TYPE_SOCKS5 + elif (self.node_type != "SOCKS4"): + proxy_type = socks.PROXY_TYPE_SOCKS4 + else: + print "We don't know this proxy type." + sys.exit(1) + + return self._get_socksipy_socket(proxy_type) + elif (self.node_type == "HTTP"): # HTTP proxies + return self._get_socksipy_socket(PROXY_TYPE_HTTP) + else: # Unknown proxies + print "We don't know this proxy type." + sys.exit(1) + + def wrap_socket(self): + return self._get_socket_wrapper() + +class CodeExecNode(Node): + def __init__(self, address, port, node_type, auth_creds): + self.node = Node(address,port) + + # node type (SSH proxy, etc.) + self.node_type = node_type + # type-specific authentication credentials + self.auth_creds = auth_creds + + def add_unit(self): + pass + + def get_status(self): + pass + +class PlanetLab(CodeExecNode): + def __init__(self, address, auth_creds, ooni): + self.auth_creds = auth_creds + + self.config = ooni.utils.config + self.logger = ooni.logger + self.name = "PlanetLab" + + def _api_auth(self): + api_server = xmlrpclib.ServerProxy('https://www.planet-lab.org/PLCAPI/') + auth = {} + ## should be changed to separate node.conf file + auth['Username'] = self.config.main.pl_username + auth['AuthString'] = self.config.main.pl_password + auth['AuthMethod'] = "password" + authorized = api_server.AuthCheck(auth) + + if authorized: + print 'We are authorized!' + return auth + else: + print 'Authorization failed. Please check your settings for pl_username and pl_password in the ooni-probe.conf file.' + + def _search_for_nodes(self, node_filter=None): + api_server = xmlrpclib.ServerProxy('https://www.planet-lab.org/PLCAPI/', allow_none=True) + node_filter = {'hostname': '*.cert.org.cn'} + return_fields = ['hostname', 'site_id'] + all_nodes = api_server.GetNodes(self.api_auth(), node_filter, boot_state_filter) + pprint(all_nodes) + return all_nodes + + def _add_nodes_to_slice(self): + api_server = xmlrpclib.ServerProxy('https://www.planet-lab.org/PLCAPI/', allow_none=True) + all_nodes = self.search_for_nodes() + for node in all_nodes: + api_server.AddNode(self.api_auth(), node['site_id'], all_nodes) + print 'Adding nodes %s' % node['hostname'] + + def _auth_login(slicename, machinename): + """Attempt to authenticate to the given PL node, slicename and + machinename, using any of the private keys in ~/.ssh/ """ + + agent = paramiko.Agent() + agent_keys = agent.get_keys() + if len(agent_keys) == 0: + return + + for key in agent_keys: + print 'Trying ssh-agent key %s' % hexlify(key.get_fingerprint()), + try: + paramiko.transport.auth_publickey(machinename, slicename) + print 'Public key authentication to PlanetLab node %s successful.' % machinename, + return + except paramiko.SSHException: + print 'Public key authentication to PlanetLab node %s failed.' % machinename, + + def _get_command(): + pass + + def ssh_and_run_(slicename, machinename, command): + """Attempt to make a standard OpenSSH client to PL node, and run + commands from a .conf file.""" + + ## needs a way to specify 'ssh -l <slicename> <machinename>' + ## with public key authentication. + + command = PlanetLab.get_command() + + client = paramiko.SSHClient() + client.load_system_host_keys() + client.connect(machinename) + + stdin, stdout, stderr = client.exec_command(command) + + def send_files_to_node(directory, files): + """Attempt to rsync a tree to the PL node.""" + pass + + def add_unit(): + pass + + def get_status(): + pass diff --git a/ooni/oonicli.py b/ooni/oonicli.py index 1988652..6def1df 100644 --- a/ooni/oonicli.py +++ b/ooni/oonicli.py @@ -32,12 +32,12 @@ from ooni.utils import log
class Options(usage.Options, app.ReactorSelectionMixin): - synopsis = """%s [options] [[file|package|module|TestCase|testmethod]...] + synopsis = """%s [options] [path to test].py """ % (os.path.basename(sys.argv[0]),)
longdesc = ("ooniprobe loads and executes a suite or a set of suites of" - "network tests. These are loaded from modules, packages and" - "files listed on the command line") + " network tests. These are loaded from modules, packages and" + " files listed on the command line")
optFlags = [["help", "h"], ['debug-stacktraces', 'B', @@ -47,8 +47,6 @@ class Options(usage.Options, app.ReactorSelectionMixin): optParameters = [ ["reportfile", "o", None, "report file name"], ["logfile", "l", None, "log file name"], - ['temp-directory', None, '_ooni_temp', - 'Path to use as working directory for tests.'] ]
compData = usage.Completions( @@ -98,12 +96,11 @@ def run(): if config['debug-stacktraces']: defer.setDebugging(True)
- log.start(config['logfile']) - classes = runner.findTestClassesFromConfig(config) casesList, options = runner.loadTestsAndOptions(classes, config)
for idx, cases in enumerate(casesList): orunner = runner.ORunner(cases, options[idx], config) + log.start(config['logfile']) orunner.run()
diff --git a/ooni/plugoo/__init__.py b/ooni/plugoo/__init__.py deleted file mode 100644 index f3a49e9..0000000 --- a/ooni/plugoo/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: UTF-8 -""" - plugoo - ****** - - This contains all of the "goo" necessary for creating - ooni-probe plugoonies. - - :copyright: (c) 2012 by Arturo Filastò. - :license: see LICENSE for more details. - -""" - -__all__ = ['assets', 'nodes', 'reports', 'tests'] - -import os -from datetime import datetime -import yaml - -import logging -import itertools - -def gen_headers(self, options="common"): - """ - Returns a set of headers to be used when generating - HTTP requests. - - :options specify what rules should be used for - generating the headers. - "common": choose a very common header set (default) - "random": make the headers random - """ - if options == "common": - headers = [('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'), - ('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.3'), - ('Accept-Encoding', 'gzip,deflate,sdch'), - ('Accept-Language', 'en,en-US;q=0.8,it;q=0.6'), - ('Cache-Control', 'max-age=0') - ('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11')] - elif options == "random": - # XXX not implemented - return False - else: - print "Error, unrecognized header generation options.." - return False - - return headers diff --git a/ooni/plugoo/assets.py b/ooni/plugoo/assets.py deleted file mode 100644 index 205d60d..0000000 --- a/ooni/plugoo/assets.py +++ /dev/null @@ -1,62 +0,0 @@ -class Asset: - """ - This is an ooni-probe asset. It is a python - iterator object, allowing it to be efficiently looped. - To create your own custom asset your should subclass this - and override the next_asset method and the len method for - computing the length of the asset. - """ - def __init__(self, file=None, *args, **argv): - self.fh = None - if file: - self.name = file - self.fh = open(file, 'r') - self.eof = False - - def __iter__(self): - return self - - def len(self): - """ - Returns the length of the asset - """ - for i, l in enumerate(self.fh): - pass - # rewind the file - self.fh.seek(0) - return i + 1 - - def parse_line(self, line): - """ - Override this method if you need line - by line parsing of an Asset. - """ - return line.replace('\n','') - - def next_asset(self): - """ - Return the next asset. - """ - # XXX this is really written with my feet. - # clean me up please... - line = self.fh.readline() - if line: - parsed_line = self.parse_line(line) - if parsed_line: - return parsed_line - else: - self.fh.seek(0) - raise StopIteration - - def next(self): - try: - return self.next_asset() - except: - raise StopIteration - -class MissingAssetException(Exception): - """Raised when an Asset necessary for running the Test is missing.""" - def __init__(self, error_message): - print error_message - import sys - return sys.exit() diff --git a/ooni/plugoo/interface.py b/ooni/plugoo/interface.py deleted file mode 100644 index 6dc83a0..0000000 --- a/ooni/plugoo/interface.py +++ /dev/null @@ -1,56 +0,0 @@ -from zope.interface import implements, Interface, Attribute - -class ITest(Interface): - """ - This interface represents an OONI test. It fires a deferred on completion. - """ - - shortName = Attribute("""A short user facing description for this test""") - description = Attribute("""A string containing a longer description for the test""") - - requirements = Attribute("""What is required to run this this test, for example raw socket access or UDP or TCP""") - - options = Attribute("""These are the arguments to be passed to the test for it's execution""") - - blocking = Attribute("""True or False, stating if the test should be run in a thread or not.""") - - def control(experiment_result, args): - """ - @param experiment_result: The result returned by the experiment method. - - @param args: the keys of this dict are the names of the assets passed in - from load_assets. The value is one item of the asset. - - Must return a dict containing what should be written to the report. - Anything returned by control ends up inside of the YAMLOONI report. - """ - - def experiment(args): - """ - Perform all the operations that are necessary to running a test. - - @param args: the keys of this dict are the names of the assets passed in - from load_assets. The value is one item of the asset. - - Must return a dict containing the values to be passed to control. - """ - - def load_assets(): - """ - Load the assets that should be passed to the Test. These are the inputs - to the OONI test. - Must return a dict that has as keys the asset names and values the - asset contents. - If the test does not have any assets it should return an empty dict. - """ - - def end(): - """ - This can be called at any time to terminate the execution of all of - these test instances. - - What this means is that no more test instances with new parameters will - be created. A report will be written. - """ - - diff --git a/ooni/plugoo/nodes.py b/ooni/plugoo/nodes.py deleted file mode 100644 index 155f183..0000000 --- a/ooni/plugoo/nodes.py +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -""" - nodes - ***** - - This contains all the code related to Nodes - both network and code execution. - - :copyright: (c) 2012 by Arturo Filastò, Isis Lovecruft - :license: see LICENSE for more details. - -""" - -import os -from binascii import hexlify - -try: - import paramiko -except: - print "Error: module paramiko is not installed." -from pprint import pprint -import sys -import socks -import xmlrpclib - -class Node(object): - def __init__(self, address, port): - self.address = address - self.port = port - -class LocalNode(object): - def __init__(self): - pass - -""" -[]: node = NetworkNode("192.168.0.112", 5555, "SOCKS5") -[]: node_socket = node.wrap_socket() -""" -class NetworkNode(Node): - def __init__(self, address, port, node_type="SOCKS5", auth_creds=None): - self.node = Node(address,port) - - # XXX support for multiple types - # node type (SOCKS proxy, HTTP proxy, GRE tunnel, ...) - self.node_type = node_type - # type-specific authentication credentials - self.auth_creds = auth_creds - - def _get_socksipy_socket(self, proxy_type, auth_creds): - import socks - s = socks.socksocket() - # auth_creds[0] -> username - # auth_creds[1] -> password - s.setproxy(proxy_type, self.node.address, self.node.port, - self.auth_creds[0], self.auth_creds[1]) - return s - - def _get_socket_wrapper(self): - if (self.node_type.startswith("SOCKS")): # SOCKS proxies - if (self.node_type != "SOCKS5"): - proxy_type = socks.PROXY_TYPE_SOCKS5 - elif (self.node_type != "SOCKS4"): - proxy_type = socks.PROXY_TYPE_SOCKS4 - else: - print "We don't know this proxy type." - sys.exit(1) - - return self._get_socksipy_socket(proxy_type) - elif (self.node_type == "HTTP"): # HTTP proxies - return self._get_socksipy_socket(PROXY_TYPE_HTTP) - else: # Unknown proxies - print "We don't know this proxy type." - sys.exit(1) - - def wrap_socket(self): - return self._get_socket_wrapper() - -class CodeExecNode(Node): - def __init__(self, address, port, node_type, auth_creds): - self.node = Node(address,port) - - # node type (SSH proxy, etc.) - self.node_type = node_type - # type-specific authentication credentials - self.auth_creds = auth_creds - - def add_unit(self): - pass - - def get_status(self): - pass - -class PlanetLab(CodeExecNode): - def __init__(self, address, auth_creds, ooni): - self.auth_creds = auth_creds - - self.config = ooni.utils.config - self.logger = ooni.logger - self.name = "PlanetLab" - - def _api_auth(self): - api_server = xmlrpclib.ServerProxy('https://www.planet-lab.org/PLCAPI/') - auth = {} - ## should be changed to separate node.conf file - auth['Username'] = self.config.main.pl_username - auth['AuthString'] = self.config.main.pl_password - auth['AuthMethod'] = "password" - authorized = api_server.AuthCheck(auth) - - if authorized: - print 'We are authorized!' - return auth - else: - print 'Authorization failed. Please check your settings for pl_username and pl_password in the ooni-probe.conf file.' - - def _search_for_nodes(self, node_filter=None): - api_server = xmlrpclib.ServerProxy('https://www.planet-lab.org/PLCAPI/', allow_none=True) - node_filter = {'hostname': '*.cert.org.cn'} - return_fields = ['hostname', 'site_id'] - all_nodes = api_server.GetNodes(self.api_auth(), node_filter, boot_state_filter) - pprint(all_nodes) - return all_nodes - - def _add_nodes_to_slice(self): - api_server = xmlrpclib.ServerProxy('https://www.planet-lab.org/PLCAPI/', allow_none=True) - all_nodes = self.search_for_nodes() - for node in all_nodes: - api_server.AddNode(self.api_auth(), node['site_id'], all_nodes) - print 'Adding nodes %s' % node['hostname'] - - def _auth_login(slicename, machinename): - """Attempt to authenticate to the given PL node, slicename and - machinename, using any of the private keys in ~/.ssh/ """ - - agent = paramiko.Agent() - agent_keys = agent.get_keys() - if len(agent_keys) == 0: - return - - for key in agent_keys: - print 'Trying ssh-agent key %s' % hexlify(key.get_fingerprint()), - try: - paramiko.transport.auth_publickey(machinename, slicename) - print 'Public key authentication to PlanetLab node %s successful.' % machinename, - return - except paramiko.SSHException: - print 'Public key authentication to PlanetLab node %s failed.' % machinename, - - def _get_command(): - pass - - def ssh_and_run_(slicename, machinename, command): - """Attempt to make a standard OpenSSH client to PL node, and run - commands from a .conf file.""" - - ## needs a way to specify 'ssh -l <slicename> <machinename>' - ## with public key authentication. - - command = PlanetLab.get_command() - - client = paramiko.SSHClient() - client.load_system_host_keys() - client.connect(machinename) - - stdin, stdout, stderr = client.exec_command(command) - - def send_files_to_node(directory, files): - """Attempt to rsync a tree to the PL node.""" - pass - - def add_unit(): - pass - - def get_status(): - pass diff --git a/ooni/plugoo/reports.py b/ooni/plugoo/reports.py deleted file mode 100644 index 1bfdac0..0000000 --- a/ooni/plugoo/reports.py +++ /dev/null @@ -1,145 +0,0 @@ -from __future__ import with_statement - -import os -import yaml - -import itertools -from ooni.utils import log, date, net - -class Report: - """This is the ooni-probe reporting mechanism. It allows - reporting to multiple destinations and file formats. - - :scp the string of <host>:<port> of an ssh server - - :yaml the filename of a the yaml file to write - - :file the filename of a simple txt file to write - - :tcp the <host>:<port> of a TCP server that will just listen for - inbound connection and accept a stream of data (think of it - as a `nc -l -p <port> > filename.txt`) - """ - def __init__(self, testname=None, file="report.log", - scp=None, - tcp=None): - - self.testname = testname - self.file = file - self.tcp = tcp - self.scp = scp - #self.config = ooni.config.report - - #if self.config.timestamp: - # tmp = self.file.split('.') - # self.file = '.'.join(tmp[:-1]) + "-" + \ - # datetime.now().isoformat('-') + '.' + \ - # tmp[-1] - # print self.file - - self.scp = None - self.write_header() - - def write_header(self): - pretty_date = date.pretty_date() - header = "# OONI Probe Report for Test %s\n" % self.testname - header += "# %s\n\n" % pretty_date - self._write_to_report(header) - # XXX replace this with something proper - address = net.getClientAddress() - test_details = {'start_time': str(date.now()), - 'asn': address['asn'], - 'test_name': self.testname, - 'addr': address['ip']} - self(test_details) - - def _write_to_report(self, dump): - reports = [] - - if self.file: - reports.append("file") - - if self.tcp: - reports.append("tcp") - - if self.scp: - reports.append("scp") - - #XXX make this non blocking - for report in reports: - self.send_report(dump, report) - - def __call__(self, data): - """ - This should be invoked every time you wish to write some - data to the reporting system - """ - dump = yaml.dump([data]) - self._write_to_report(dump) - - def file_report(self, data): - """ - This reports to a file in YAML format - """ - with open(self.file, 'a+') as f: - f.write(data) - - def send_report(self, data, type): - """ - This sends the report using the - specified type. - """ - #print "Reporting %s to %s" % (data, type) - log.msg("Reporting to %s" % type) - getattr(self, type+"_report").__call__(data) - -class NewReport(object): - filename = 'report.log' - startTime = None - endTime = None - testName = None - ipAddr = None - asnAddr = None - - def _open(): - self.fp = open(self.filename, 'a+') - - @property - def header(): - pretty_date = date.pretty_date() - report_header = "# OONI Probe Report for Test %s\n" % self.testName - report_header += "# %s\n\n" % pretty_date - test_details = {'start_time': self.startTime, - 'asn': asnAddr, - 'test_name': self.testName, - 'addr': ipAddr} - report_header += yaml.dump([test_details]) - return report_header - - def create(): - """ - Create a new report by writing it's header. - """ - self.fp = open(self.filename, 'w+') - self.fp.write(self.header) - - def exists(): - """ - Returns False if the file does not exists. - """ - return os.path.exists(self.filename) - - def write(data): - """ - Write a report to the file. - - :data: python data structure to be written to report. - """ - if not self.exists(): - self.create() - else: - self._open() - yaml_encoded_data = yaml.dump([data]) - self.fp.write(yaml_encoded_data) - self.fp.close() - diff --git a/ooni/plugoo/tests.py b/ooni/plugoo/tests.py deleted file mode 100644 index 653dd67..0000000 --- a/ooni/plugoo/tests.py +++ /dev/null @@ -1,142 +0,0 @@ -import os -import yaml -from zope.interface import Interface, Attribute - -import logging -import itertools -from twisted.internet import reactor, defer, threads -## XXX why is this imported and not used? -from twisted.python import failure - -from ooni.utils import log, date -from ooni.plugoo import assets, work -from ooni.plugoo.reports import Report -from ooni.plugoo.interface import ITest - - -class OONITest(object): - """ - This is the base class for writing OONI Tests. - - It should be used in conjunction with the ITest Interface. It allows the - developer to benefit from OONIs reporting system and command line argument - parsing system. - """ - name = "oonitest" - # By default we set this to False, meaning that we don't block - blocking = False - reactor = reactor - tool = False - ended = False - - def __init__(self, local_options, global_options, report, ooninet=None, - reactor=reactor): - # These are the options that are read through the tests suboptions - self.local_options = local_options - # These are the options global to all of OONI - self.global_options = global_options - self.report = report - #self.ooninet = ooninet - self.reactor = reactor - self.result = {} - self.initialize() - self.assets = self.load_assets() - - def initialize(self): - """ - Override this method if you are interested in having some extra - behavior when your test class is instantiated. - """ - pass - - def load_assets(self): - """ - This method should be overriden by the test writer to provide the - logic for loading their assets. - """ - return {} - - def __repr__(self): - return "<OONITest %s %s %s>" % (self.local_options, - self.global_options, - self.assets) - - def end(self): - """ - State that the current test should finish. - """ - self.ended = True - - def finished(self, return_value): - """ - The Test has finished running, we must now calculate the test runtime - and add all time data to the report. - """ - #self.ooninet.report(result) - self.end_time = date.now() - result = self.result - result['start_time'] = str(self.start_time) - result['end_time'] = str(self.end_time) - result['run_time'] = str(self.end_time - self.start_time) - result['return_value'] = return_value - log.msg("FINISHED %s" % result) - self.report(result) - return result - - def _do_experiment(self, args): - """ - A wrapper around the launch of experiment. - If we are running a blocking test experiment will be run in a thread if - not we expect it to return a Deferred. - - @param args: the asset line(s) that we are working on. - - returns a deferred. - """ - if self.blocking: - self.d = threads.deferToThread(self.experiment, args) - else: - self.d = self.experiment(args) - - self.d.addCallback(self.control, args) - self.d.addCallback(self.finished) - self.d.addErrback(self.finished) - return self.d - - def control(self, result, args): - """ - Run the control. - - @param result: what was returned by experiment. - - @param args: the asset(s) lines that we are working on. - """ - log.msg("Doing control") - return result - - def experiment(self, args): - """ - Run the experiment. This sample implementation returns a deferred, - making it a non-blocking test. - - @param args: the asset(s) lines that we are working on. - """ - log.msg("Doing experiment") - d = defer.Deferred() - return d - - def startTest(self, args): - """ - This method is invoked by the worker to start the test with one line of - the asset file. - - @param args: the asset(s) lines that we are working on. - """ - self.start_time = date.now() - - if self.shortName: - log.msg("Starting test %s" % self.shortName) - else: - log.msg("Starting test %s" % self.__class__) - - return self._do_experiment(args) diff --git a/ooni/plugoo/work.py b/ooni/plugoo/work.py deleted file mode 100644 index db88fbf..0000000 --- a/ooni/plugoo/work.py +++ /dev/null @@ -1,148 +0,0 @@ -# -*- coding: UTF-8 -""" - work.py - ********** - - This contains all code related to generating - Units of Work and processing it. - - :copyright: (c) 2012 by Arturo Filastò. - :license: see LICENSE for more details. - -""" -import itertools -import yaml -from datetime import datetime - -from zope.interface import Interface, Attribute - -from twisted.python import failure -from twisted.internet import reactor, defer - -class Worker(object): - """ - This is the core of OONI. It takes as input Work Units and - runs them concurrently. - """ - def __init__(self, maxconcurrent=10, reactor=reactor): - """ - @param maxconcurrent: how many test instances should be run - concurrently. - """ - self.reactor = reactor - self.maxconcurrent = maxconcurrent - self._running = 0 - self._queued = [] - - def _run(self, r): - """ - Check if we should start another test because we are below maximum - concurrency. - - This function is called every time a test finishes running. - - @param r: the return value of a previous test. - """ - if self._running > 0: - self._running -= 1 - - if self._running < self.maxconcurrent and self._queued: - workunit, d = self._queued.pop(0) - asset, test, idx = workunit - while test.ended and workunit: - try: - workunit, d = self._queued.pop(0) - asset, test, idx = workunit - except: - workunit = None - - if not test.ended: - self._running += 1 - actuald = test.startTest(asset).addBoth(self._run) - - if isinstance(r, failure.Failure): - # XXX probably we should be doing something to retry test running - r.trap() - - if self._running == 0 and not self._queued: - self.reactor.stop() - - return r - - def push(self, workunit): - """ - Add a test to the test queue and run it if we are not maxed out on - concurrency. - - @param workunit: a tuple containing the (asset, test, idx), where asset - is the line of the asset(s) we are working on, test - is an instantiated test and idx is the index we are - currently at. - """ - if self._running < self.maxconcurrent: - asset, test, idx = workunit - if not test.ended: - self._running += 1 - return test.startTest(asset).addBoth(self._run) - - d = defer.Deferred() - self._queued.append((workunit, d)) - return d - -class WorkGenerator(object): - """ - Factory responsible for creating units of work. - - This shall be run on the machine running OONI-cli. The returned WorkUnits - can either be run locally or on a remote OONI Node or Network Node. - """ - size = 10 - - def __init__(self, test, arguments=None, start=None): - self.Test = test - - if self.Test.assets and self.Test.assets.values()[0]: - self.assetGenerator = itertools.product(*self.Test.assets.values()) - else: - self.assetGenerator = None - - self.assetNames = self.Test.assets.keys() - - self.idx = 0 - self.end = False - if start: - self.skip(start) - - def __iter__(self): - return self - - def skip(self, start): - """ - Skip the first x number of lines of the asset. - - @param start: int how many items we should skip. - """ - for j in xrange(0, start-1): - for i in xrange(0, self.size): - self.assetGenerator.next() - self.idx += 1 - - def next(self): - if self.end: - raise StopIteration - - if not self.assetGenerator: - self.end = True - return ({}, self.Test, self.idx) - - try: - asset = self.assetGenerator.next() - ret = {} - for i, v in enumerate(asset): - ret[self.assetNames[i]] = v - except StopIteration: - raise StopIteration - - self.idx += 1 - return (ret, self.Test, self.idx) - diff --git a/ooni/protocols/http.py b/ooni/protocols/http.py deleted file mode 100644 index 569c382..0000000 --- a/ooni/protocols/http.py +++ /dev/null @@ -1,141 +0,0 @@ -import random -from zope.interface import implements -from twisted.python import usage -from twisted.plugin import IPlugin -from twisted.internet import protocol, defer -from ooni.plugoo.tests import ITest, OONITest -from ooni.plugoo.assets import Asset -from ooni.utils import log - -useragents = [("Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6", "Firefox 2.0, Windows XP"), - ("Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)", "Internet Explorer 7, Windows Vista"), - ("Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30)", "Internet Explorer 7, Windows XP"), - ("Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322)", "Internet Explorer 6, Windows XP"), - ("Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.1; .NET CLR 1.1.4322)", "Internet Explorer 5, Windows XP"), - ("Opera/9.20 (Windows NT 6.0; U; en)", "Opera 9.2, Windows Vista"), - ("Opera/9.00 (Windows NT 5.1; U; en)", "Opera 9.0, Windows XP"), - ("Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 8.50", "Opera 8.5, Windows XP"), - ("Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 8.0", "Opera 8.0, Windows XP"), - ("Mozilla/4.0 (compatible; MSIE 6.0; MSIE 5.5; Windows NT 5.1) Opera 7.02 [en]", "Opera 7.02, Windows XP"), - ("Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.5) Gecko/20060127 Netscape/8.1", "Netscape 8.1, Windows XP")] - -class BodyReceiver(protocol.Protocol): - def __init__(self, finished): - self.finished = finished - self.data = "" - - def dataReceived(self, bytes): - self.data += bytes - - def connectionLost(self, reason): - self.finished.callback(self.data) - -from twisted.web.http_headers import Headers -class HTTPTest(OONITest): - """ - A utility class for dealing with HTTP based testing. It provides methods to - be overriden for dealing with HTTP based testing. - The main functions to look at are processResponseBody and - processResponseHeader that are invoked once the headers have been received - and once the request body has been received. - """ - randomize_ua = True - follow_redirects = False - - def initialize(self): - from twisted.web.client import Agent - import yaml - - self.agent = Agent(self.reactor) - if self.follow_redirects: - from twisted.web.client import RedirectAgent - self.agent = RedirectAgent(self.agent) - - self.request = {} - self.response = {} - - def _processResponseBody(self, data): - self.response['body'] = data - self.result['response'] = self.response - self.processResponseBody(data) - - def processResponseBody(self, data): - """ - This should handle all the response body smushing for getting it ready - to be passed onto the control. - - @param data: The content of the body returned. - """ - pass - - def processResponseHeaders(self, headers): - """ - This should take care of dealing with the returned HTTP headers. - - @param headers: The content of the returned headers. - """ - pass - - def processRedirect(self, location): - """ - Handle a redirection via a 3XX HTTP status code. - - @param location: the url that is being redirected to. - """ - pass - - def doRequest(self, url): - d = self.build_request(url) - def finished(data): - return data - - d.addCallback(self._cbResponse) - d.addCallback(finished) - return d - - def experiment(self, args): - log.msg("Running experiment") - url = self.local_options['url'] if 'url' not in args else args['url'] - - d = self.doRequest(url) - return d - - def _cbResponse(self, response): - self.response['headers'] = list(response.headers.getAllRawHeaders()) - self.response['code'] = response.code - self.response['length'] = response.length - self.response['version'] = response.length - - if str(self.response['code']).startswith('3'): - self.processRedirect(response.headers.getRawHeaders('Location')[0]) - self.processResponseHeaders(self.response['headers']) - #self.result['response'] = self.response - - finished = defer.Deferred() - response.deliverBody(BodyReceiver(finished)) - finished.addCallback(self._processResponseBody) - - def randomize_useragent(self): - user_agent = random.choice(useragents) - self.request['headers']['User-Agent'] = [user_agent] - - def build_request(self, url, method="GET", headers=None, body=None): - self.request['method'] = method - self.request['url'] = url - self.request['headers'] = headers if headers else {} - self.request['body'] = body - if self.randomize_ua: - self.randomize_useragent() - - self.result['request'] = self.request - self.result['url'] = url - return self.agent.request(self.request['method'], self.request['url'], - Headers(self.request['headers']), - self.request['body']) - - def load_assets(self): - if self.local_options: - return {'url': Asset(self.local_options['asset'])} - else: - return {} - diff --git a/ooni/protocols/scapyproto.py b/ooni/protocols/scapyproto.py deleted file mode 100644 index 4166146..0000000 --- a/ooni/protocols/scapyproto.py +++ /dev/null @@ -1,55 +0,0 @@ -import random -from zope.interface import implements -from twisted.python import usage -from twisted.plugin import IPlugin -from twisted.internet import protocol, defer -from ooni.plugoo.tests import ITest, OONITest -from ooni.plugoo.assets import Asset -from ooni.utils import log - -from ooni.lib.txscapy import txsr, txsend - -class ScapyTest(OONITest): - """ - A utility class for writing scapy driven OONI tests. - """ - - receive = True - timeout = None - pcapfile = 'scapytest.pcap' - def initialize(self, reactor=None): - - if not self.reactor: - from twisted.internet import reactor - self.reactor = reactor - - self.request = {} - self.response = {} - - def experiment(self, args): - log.msg("Running experiment") - if self.receive: - log.msg("Sending and receiving packets.") - d = txsr(self.build_packets(), pcapfile=self.pcapfile, - timeout=self.timeout) - else: - log.msg("Sending packets.") - d = txsend(self.build_packets()) - - def finished(data): - log.msg("Finished sending") - return data - - d.addCallback(finished) - return d - - def build_packets(self): - """ - Override this method to build scapy packets. - """ - from scapy.all import IP, TCP - return IP()/TCP() - - def load_assets(self): - return {} - diff --git a/ooni/reporter.py b/ooni/reporter.py index ad7956d..a02e5a9 100644 --- a/ooni/reporter.py +++ b/ooni/reporter.py @@ -273,18 +273,14 @@ class OONIReporter(OReporter):
idx = self.getTestIndex(test)
- self._tests[idx]['last_time'] = self._getTime() - self._tests[idx]['test_started'] - # This is here for allowing reporting of legacy tests. - # XXX In the future this should be removed. - try: - report = list(test.legacy_report) - log.debug("Set the report to be a list") - except: - # XXX I put a dict() here so that the object is re-instantiated and I - # actually end up with the report I want. This could either be a - # python bug or a yaml bug. - report = dict(test.report) - log.debug("Set the report to be a dict") + self._tests[idx]['last_time'] = self._getTime() - \ + self._tests[idx]['test_started'] + + # XXX I put a dict() here so that the object is re-instantiated and I + # actually end up with the report I want. This could either be a + # python bug or a yaml bug. + report = dict(test.report) + log.debug("Set the report to be a dict")
log.debug("Adding to report %s" % report) self._tests[idx]['report'] = report diff --git a/ooni/runner.py b/ooni/runner.py index fa50840..102ff29 100644 --- a/ooni/runner.py +++ b/ooni/runner.py @@ -20,25 +20,9 @@ from twisted.trial.runner import filenameToModule from ooni.inputunit import InputUnitFactory from ooni.nettest import InputTestSuite
-from ooni.plugoo import tests as oonitests - from ooni.reporter import ReporterFactory from ooni.utils import log, date
-from ooni.utils.legacy import LegacyOONITest -from ooni.utils.legacy import start_legacy_test, adapt_legacy_test - -def isLegacyTest(obj): - """ - Returns True if the test in question is written using the OONITest legacy - class. - We do this for backward compatibility of the OONIProbe API. - """ - try: - return issubclass(obj, oonitests.OONITest) and not obj == oonitests.OONITest - except TypeError: - return False - def processTest(obj, config): """ Process the parameters and :class:`twisted.python.usage.Options` of a @@ -115,9 +99,6 @@ def findTestClassesFromConfig(config): if isTestCase(val): log.debug("Detected TestCase %s" % val) classes.append(processTest(val, config)) - elif isLegacyTest(val): - log.debug("Detected Legacy Test %s" % val) - classes.append(adapt_legacy_test(val, config)) return classes
def makeTestCases(klass, tests, method_prefix): @@ -141,38 +122,18 @@ def loadTestsAndOptions(classes, config): options = [] test_cases = []
- _old_klass_type = LegacyOONITest - for klass in classes: - if isinstance(klass, _old_klass_type): - try: - cases = start_legacy_test(klass) - if cases: - log.debug("Processing cases") - log.debug(str(cases)) - return [], [] - test_cases.append(cases) - except Exception, e: - log.err(e) - else: - try: - opts = klass.local_options - options.append(opts) - except AttributeError, ae: - options.append([]) - log.err(ae) - else: - tests = reflect.prefixedMethodNames(klass, method_prefix) - if tests: - cases = makeTestCases(klass, tests, method_prefix) - test_cases.append(cases) - try: - k = klass() - opts = k.getOptions() - options.append(opts) - except AttributeError, ae: - options.append([]) - log.err(ae) + tests = reflect.prefixedMethodNames(klass, method_prefix) + if tests: + cases = makeTestCases(klass, tests, method_prefix) + test_cases.append(cases) + try: + k = klass() + opts = k.getOptions() + options.append(opts) + except AttributeError, ae: + options.append([]) + log.err(ae)
return test_cases, options
diff --git a/ooni/scaffolding.py b/ooni/scaffolding.py deleted file mode 100644 index dffe342..0000000 --- a/ooni/scaffolding.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python - -""" -This script should be used for creating the scaffolding for a test. -""" -from __future__ import print_function -import os -import sys -from ooni.utils import log - -print("!!!! This test writing strategy is now deprecated !!!") -print("visit: https://ooni.readthedocs.org/en/latest/writing_tests.html " - "for new instructions") -sys.exit(1) - -test_template = """""" -This is a self genrated test created by scaffolding.py. -you will need to fill it up with all your necessities. -Safe hacking :). -""" -from zope.interface import implements -from twisted.python import usage -from twisted.plugin import IPlugin -from ooni.plugoo.tests import ITest, OONITest -from ooni.plugoo.assets import Asset -from ooni.utils import log - -class %(testShortname)sArgs(usage.Options): - optParameters = [['asset', 'a', None, 'Asset file'], - ['resume', 'r', 0, 'Resume at this index']] - -class %(testShortname)sTest(OONITest): - implements(IPlugin, ITest) - - shortName = "%(testSNlower)s" - description = "%(testName)s" - requirements = None - options = %(testShortname)sArgs - blocking = True - - def control(self, experiment_result, args): - # What you return here ends up inside of the report. - log.msg("Running control") - return {} - - def experiment(self, args): - # What you return here gets handed as input to control - log.msg("Running experiment") - return {} - - def load_assets(self): - if self.local_options: - return {'asset': Asset(self.local_options['asset'])} - else: - return {} - -# We need to instantiate it otherwise getPlugins does not detect it -# XXX Find a way to load plugins without instantiating them. -%(testShortname)s = %(testShortname)sTest(None, None, None) -""" - -test_vars = {'testName': None, 'testShortname': None} -test_vars['testName'] = raw_input('Test Name: ') -test_vars['testShortname'] = raw_input("Test Short Name: ") -test_vars['testSNlower'] = test_vars['testShortname'].lower() - -fname = os.path.join('plugins', test_vars['testSNlower']+'.py') - -if os.path.exists(fname): - print('WARNING! File named "%s" already exists.' % fname) - if raw_input("Do you wish to continue (y/N)? ").lower() != 'y': - print("gotcha! Dying..") - sys.exit(0) - -fp = open(fname, 'w') -fp.write(test_template % test_vars) -fp.close() - diff --git a/ooni/utils/legacy.py b/ooni/utils/legacy.py deleted file mode 100755 index 3e21d97..0000000 --- a/ooni/utils/legacy.py +++ /dev/null @@ -1,459 +0,0 @@ -# -*- coding: UTF-8 -# -# Open Observatory of Network Interference -# **************************************** -# -# legacy.py -# --------- -# Utilities for working with legacy OONI tests, i.e. tests which were created -# before the transition to the new twisted.trial based API. -# -# "The Net interprets censorship as damage and routes around it." -# - John Gilmore; TIME magazine (6 December 1993) -# -# :authors: Isis Lovecruft, Arturo Filasto -# :license: see included LICENSE file -# :copyright: (c) 2012 Isis Lovecruft, Arturo Filasto, The Tor Project, Inc. -# :version: 0.1.0-pre-alpha -# -# This API has been deprecated and is merely for API purposes. -# - -__VERSION__="0.0.1-pre-alpha" - -import inspect -import os -import yaml - -from twisted.internet import defer, reactor -from twisted.python import usage -from twisted.python import log as tplog -from twisted.python.usage import Options as tpOptions - -from ooni import nettest -from ooni.plugoo.tests import OONITest -from ooni.plugoo import work, reports -from ooni.utils import log, date -from ooni.utils.logo import getlogo - - -def runTest(test, options, global_options, reactor=reactor): - """ - Run an OONI test by name. - - @param test: - a string specifying the test name as specified inside - of shortName. - @param options: - the local options to be passed to the test. - @param global_options: the global options for OONI. - """ - parallelism = int(global_options['parallelism']) - worker = work.Worker(parallelism, reactor=reactor) - test_class = test.__class__ - report = reports.Report(test, global_options['output']) - - log_to_stdout = True - if global_options['quiet']: - log_to_stdout = False - - log.start(log_to_stdout, - global_options['log'], - global_options['verbosity']) - - resume = 0 - if not options: - options = {} - if 'resume' in options: - resume = options['resume'] - - test = test_class(options, global_options, report, reactor=reactor) - if test.tool: - test.runTool() - return True - - if test.ended: - print "Ending test" - return None - - wgen = work.WorkGenerator(test, dict(options), start=resume) - for x in wgen: - worker.push(x) - - -class LegacyOptions(usage.Options): - """Deprecated.""" - def __init__(test_classes=[], sub_commands=[]): - self.test_classes = test_classes - self.sub_commands = sub_commands - for cls in self.test_classes: - sub_commands.append([cls, None, cls.options, - "Run the %s test" % cls]) - optParameters = [ - ['parallelism', 'n', 10, "Specify the number of parallel tests to run"], - ['output', 'o', 'report.log', "Specify output report file"], - ['log', 'l', 'oonicli.log', "Specify output log file"], - ['verbosity', 'v', 1, "Specify the logging level"]] - optFlags = [['quiet', 'q', "Don't log to stdout"]] - - def opt_version(self): - """ - This API has been deprecated; please use the new alpha-release API. - See /nettests in the top directory, as well as the /docs folder for - further information. - """ - print "OONI version: %s\n\n%s" % (__VERSION__, __doc__) - sys.exit(0) - - def __str__(self): - """ - Hack to get the sweet ascii art into the help output and replace the - strings "Commands" with "Tests". - """ - return getlogo() + '\n' + self.getSynopsis() + '\n' + \ - self.getUsage(width=None).replace("Commands:", "Tests:") - -def run_ooniprobe_py(*args): - log.start() - if not args: - args = "--help" - old_api = usage.Options() - try: - old_api.parseOptions() - except: - log.msg("Use of this API is deprecated. Please use /bin/ooniprobe.") - runTest(old_api.sub_command, old_api.subOptions, old_api) - reactor.run() - -class LegacyReporter(object): - """ - Backwards compatibility class for creating a report object for results - from a :class:`ooni.runner.LegacyTest`. A - :class:`ooni.runner.LegacyReporter` object will eventually get wrapped in - a list when :mod:`ooni.oonicli` calls - :meth:`ooni.reporter.OONIReporter.stopTest`. - - :param report_target: - The type of object to write results to, by default a list. - """ - def __init__(self, report_target=[]): - self.report_target = report_target - if isinstance(self.report_target, dict): - self._type = dict - elif isinstance(self.report_target, list): - self._type = list - else: - self._type = type(self.report_target) - - def __call__(self, info): - if self._type is dict: - self.report_target.update(info) - elif self._type is list: - self.report_target.append(info) - else: - log.debug("ADD A NEW REPORT_TARGET TYPE!!") - -class LegacyOONITest(nettest.NetTestCase): - """ - Converts an old test, which should be a subclass of - :class:`ooni.plugoo.tests.OONITest`, to an :mod:`ooni.oonicli` - compatible class. - - :param obj: - An uninstantiated old test, which should be a subclass of - :class:`ooni.plugoo.tests.OONITest`. - :param config: - A configured and instantiated :class:`twisted.python.usage.Options` - class. - :meth start_legacy_test: - Handler for calling :meth:`ooni.plugoo.tests.OONITest.startTest`. - """ - - ## we need __bases__ because inspect.getmro() as well as - ## zope.interface.implements() both expect it: - from ooni.plugoo.tests import OONITest - __bases__ = (OONITest, ) - - def __getattr__(self, name): - """ - Override of builtin getattr for :class:`ooni.runner.LegacyTest` so - that method calls to a LegacyTest instance or its parent class - OONITest do not return unhandled errors, but rather report that the - method is unknown. - """ - def __unknown_method__(*a): - log.msg("Call to unknown method %s.%s" % (self.originalTest, name)) - if a: - log.msg("Unknown method %s parameters: %s" % str(a)) - return __unknown_method__ - - def find_missing_options(self): - """ - In the case that our test is actually a class within a module named - after itself, i.e. 'ooni.plugins.bridget.bridget', we want dynamic - method discover so that we can search for the test's Options class. - - Example: - Let's say we want the Options class, which is at - ``ooni.plugins.bridget.bridget.options``. But in this case, our - original_test variable isn't going to have an attribute named - 'options', because original_test is actually the *first* occurence of - 'bridget'. - - In other words, our original_test is actually the module, so we need - to find the test, which is done with: - - getattr(original_test.__class__, test_class) - - After that, we've got our test stored as something like - ``ooni.plugins.bridget.bridget`` and we need to find 'options' as an - attribute under that, which is what - - options_finder = inspect.attrgetter('options') - - is used for. And the namespace stuff is just used for debugging edge - cases where we totally can't find the options. - - :ivar original_class: - The original subclass of OONITest, except that in this case, - because our test is a module, what we have here is - 'ooni.plugins.bridget.BridgeTest', while we actually need - something like 'ooni.plugins.bridget.bridget.BridgeTest' instead. - :ivar class_string: - The :ivar:`original_class` converted to a string. - :ivar from_module: - The parent module of :ivar:`original_class`, i.e. - `ooni.plugins.bridget`. - :ivar test_class: - The last part of :ivar:`from_module`, ie. 'bridget'. - :ivar options_finder: - An instance of :meth:`inspect.attrgetter` which searches for - methods within a test class named 'options'. - """ - original_test = self.originalTest - original_class = original_test.__class__ - class_string = str(original_class) - from_module = inspect.getmodule(original_class) - test_class = class_string.rsplit('.', 1)[1] - options_finder = inspect.attrgetter('options') - - if self.was_named is False or self.name != test_class: - log.msg("Discovered legacy test named %s ..." % test_class) - setattr(self, 'name', test_class) - - try: - namespace = globals()[class_string] - log.debug("orginal namespace: %s" % namespace) - except KeyError, keyerr: - log.debug(keyerr) - - options = tpOptions - try: - options = options_finder(getattr(original_class, test_class)) - except AttributeError: - self.__getattr__(test_class) - except Exception, e: - log.err(e) - finally: - return options() - - def __init__(self, obj, config): - """ - xxx fill me in - - :param obj: - An uninstantiated old test, which should be a subclass of - :class:`ooni.plugoo.tests.OONITest`. - :param config: - A configured and instantiated - :class:`twisted.python.usage.Options` class. - :attr originalTest: - :attr subArgs: - :attr name: - :ivar was_named: - :attr subOptions: - """ - super(LegacyOONITest, self).__init__() - self.originalTest = obj - self.start_time = date.now() - self.name = 'LegacyOONITest' - self.was_named = False - try: - self.name = self.originalTest.shortName - self.was_named = True - except AttributeError: - if self.originalTest.name and self.originalTest.name != 'oonitest': - self.name = self.originalTest.name - self.was_named = True - - if 'subArgs' in config: - self.subArgs = config['subArgs'] - else: - self.subArgs = (None, ) - log.msg("No suboptions to test %s found; continuing..."% self.name) - - try: - self.subOptions = self.originalTest.options() - except AttributeError: - if self.was_named is False: - self.subOptions = self.find_missing_options() - else: - self.subOptions = None - log.msg("That test appears to have a name, but no options!") - - if self.subOptions is not None: - if len(self.subArgs) > 0: - self.subOptions.parseOptions(self.subArgs) - self.local_options = self.subOptions - else: - print self.subOptions - - if 'reportfile' in config: - self.reporter_file = config['reportfile'] - else: - filename = str(self.name) + "-" + str(date.timestamp()) + ".yaml" - self.reporter_file = os.path.join(os.getcwd(), filename) - self.reporter = [] - self.report = LegacyReporter(report_target=self.reporter) - - self.legacy_test = self.originalTest(None, self.local_options, - None, self.report) - setattr(self.legacy_test, 'name', self.name) - setattr(self.legacy_test, 'start_time', self.start_time) - - self.inputs = {} - for keys, values in self.legacy_test.assets.items(): - self.inputs[keys] = values - setattr(self.legacy_test, 'inputs', self.inputs) - - @defer.inlineCallbacks - def run_with_args(self, args): - """ - Handler for calling :meth:`ooni.plugoo.tests.OONITest.startTest` with - each :param:`args` that, in the old framework, would have been - generated one line at a time by - :class:`ooni.plugoo.assets.Asset`. This function is wrapped with - :meth:`twisted.internet.defer.inlineCallbacks` so that the result of - each call to :meth:`ooni.plugoo.tests.OONITest.experiment` is returned - immediately as :ivar:`returned`. - """ - result = yield self.legacy_test.startTest(args) - defer.returnValue(result) - -def adapt_legacy_test(obj, config): - """ - Wrapper function for taking a legacy OONITest class and converting it into - a :class:`LegacyTest`, which is a variant of the new - :class:`ooni.nettest.TestCase` and is compatible with - :mod:`ooni.oonicli`. This allows for backward compatibility of old OONI - tests. - - :param obj: - An uninstantiated old test, which should be a subclass of - :class:`ooni.plugoo.tests.OONITest`. - :param config: - A configured and instantiated :class:`twisted.python.usage.Options` - class. - :return: - A :class:`LegacyOONITest`. - """ - return LegacyOONITest(obj, config) - -def report_legacy_test_to_file(legacy_test, file=None): - """ - xxx this function current does not get used, and could easily be handled - by ooni.runner.loadTestsAndOptions, or some other function in - ooni.runner. - - xxx fill me in - """ - reporter_file = legacy_test.reporter_file - - if file is not None: - base = os.path.dirname(os.path.abspath(file)) - if base.endswith("ooni") or base == os.getcwd(): - reporter_file = file - else: - log.msg("Writing to %s not allowed, using default file %s." - % (base, reporter_file)) - - yams = yaml.safe_dump(legacy_test.reporter) - with open(reporter_file, 'a') as rosemary: - rosemary.write(yams) - rosemary.flush() - log.msg("Finished reporting.") - -def log_legacy_test_results(result, legacy_test, args): - """ - Callback function for deferreds in :func:`start_legacy_test` which - handles updating the legacy_test's :class:`legacy_test.report`. - - :param result: - The possible result of a deferred which has been returned from - :meth:`ooni.plugoo.test.OONITest.experiment` and - :meth:`ooni.plugoo.test.OONITest.control`. - :param legacy_test: - The :class:`LegacyOONITest` which we're processing. - :param args: - The current inputs which we're giving to legacy_test.startTest(). - :return: - The :param:`legacy_test`. - """ - if result: - legacy_test.report({args: result}) - log.debug("Legacy test %s with args:\n%s\nreturned result:\n%s" - % (legacy_test.name, args, result)) - else: - legacy_test.report({args: None}) - log.debug("No results return for %s with args:\n%s" - % (legacy_test.name, args)) - return legacy_test - -def start_legacy_test(legacy_test): - """ - This is the main function which should be used to call a legacy test, it - handles parsing the deprecated :class:`ooni.plugoo.assets.Asset` items as - inputs, and calls back to a custom, backwards-compatible Reporter. - - For each input to the legacy_test, this function creates a - :class:`twisted.internet.defer.Deferred` which has already received its - :meth:`callback`. The end result is a - :class:`twisted.internet.defer.gatherResults` of all the outcomes of - :param:`legacy_test` for each of the inputs. - - :param legacy_test: - A :class:`LegacyOONITest` to process. - :ivar results: - A list of :class:`twisted.internet.defer.Deferred`s which gets - processed as a :class:`twisted.internet.defer.DeferredList`. - :ivar current_input: - The input we are current working on, i.e. what would have been 'args' - (as in, 'experiment(args)') in the old design. - :return: - A :class:`twisted.internet.defer.gatherResults`. - """ - results = [] - current_input = {} - - if len(legacy_test.inputs) > 0: - for keys, values in legacy_test.inputs: - for value in values: - current_input[keys] = value - log.debug("Running %s with args: %s" - % (legacy_test.name, current_input)) - d = legacy_test.run_with_args(current_input) - d.addCallback(log_legacy_test_results, legacy_test, - current_input) - d.addErrback(tplog.err) - results.append(d) - else: - current_input['zero_input_test'] = True - log.debug("Running %s with current input: %s" - % (legacy_test.name, current_input)) - d = legacy_test.run_with_args(current_input) - d.addCallback(log_legacy_test_results, legacy_test, current_input) - d.addErrback(tplog.err) - results.append(d) - - dlist = defer.gatherResults(results) - return dlist