[tor-commits] [ooni-probe/master] * Refactor all the bridget tests into a better dir structure

isis at torproject.org isis at torproject.org
Sat Nov 3 01:24:44 UTC 2012


commit 04a61965d73cb96745e898c5bfd4cf097a090803
Author: Isis Lovecruft <isis at torproject.org>
Date:   Tue Oct 23 10:44:46 2012 +0000

    * Refactor all the bridget tests into a better dir structure
---
 bin/canary                      |   27 ++
 ooni/bridget/__init__.py        |   14 +
 ooni/bridget/custodiet.py       |  421 ++++++++++++++++++++++++
 ooni/bridget/tests/__init__.py  |   14 +
 ooni/bridget/tests/bridget.py   |  499 ++++++++++++++++++++++++++++
 ooni/bridget/utils/__init__.py  |    1 +
 ooni/bridget/utils/inputs.py    |  174 ++++++++++
 ooni/bridget/utils/interface.py |   54 +++
 ooni/bridget/utils/log.py       |   98 ++++++
 ooni/bridget/utils/nodes.py     |  176 ++++++++++
 ooni/bridget/utils/onion.py     |  686 +++++++++++++++++++++++++++++++++++++++
 ooni/bridget/utils/reports.py   |  144 ++++++++
 ooni/bridget/utils/tests.py     |  141 ++++++++
 ooni/bridget/utils/work.py      |  147 +++++++++
 ooni/plugins/bridget.py         |  500 ----------------------------
 15 files changed, 2596 insertions(+), 500 deletions(-)

diff --git a/bin/canary b/bin/canary
new file mode 100755
index 0000000..1473ae4
--- /dev/null
+++ b/bin/canary
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+###############################################################################
+#
+# canary
+# -----------------
+# Test Tor bridge reachability.
+#
+# :authors: Isis Lovecruft
+# :copyright: 2012 Isis Lovecruft, The Tor Project
+# :licence: see included LICENSE file
+# :version: 0.2.0-beta
+###############################################################################
+
+import os, sys
+import copy_reg
+
+# Hack to set the proper sys.path. Overcomes the export PYTHONPATH pain.
+sys.path[:] = map(os.path.abspath, sys.path)
+sys.path.insert(0, os.path.abspath(os.getcwd()))
+
+# This is a hack to overcome a bug in python
+from ooni.utils.hacks import patched_reduce_ex
+copy_reg._reduce_ex = patched_reduce_ex
+
+from ooni.bridget import spelunker
+spelunker.descend()
diff --git a/ooni/bridget/__init__.py b/ooni/bridget/__init__.py
new file mode 100644
index 0000000..4648d77
--- /dev/null
+++ b/ooni/bridget/__init__.py
@@ -0,0 +1,14 @@
+#-*- coding: utf-8 -*-
+
+#import os, sys
+#import copy_reg
+
+## Hack to set the proper sys.path. Overcomes the export PYTHONPATH pain.
+#sys.path[:] = map(os.path.abspath, sys.path)
+#sys.path.insert(0, os.path.abspath(os.getcwd()))
+
+## This is a hack to overcome a bug in python
+#from ooni.utils.hacks import patched_reduce_ex
+#copy_reg._reduce_ex = patched_reduce_ex
+
+__all__ = ['custodiet']
diff --git a/ooni/bridget/custodiet.py b/ooni/bridget/custodiet.py
new file mode 100755
index 0000000..8cbcfce
--- /dev/null
+++ b/ooni/bridget/custodiet.py
@@ -0,0 +1,421 @@
+#!/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/bridget/tests/__init__.py b/ooni/bridget/tests/__init__.py
new file mode 100644
index 0000000..9ecc88d
--- /dev/null
+++ b/ooni/bridget/tests/__init__.py
@@ -0,0 +1,14 @@
+# -*- coding: UTF-8
+#
+#    bridget/tests/__init__.py
+#    *************************
+#
+#    "...quis custodiet ipsos custodes?"
+#               - Juvenal, Satires VI.347-348 (circa 2nd Century, C.E.)
+#
+#    :copyright: (c) 2012 Isis Lovecruft
+#    :license: see LICENSE for more details.
+#    :version: 0.1.0-beta
+#
+
+all = ['bridget']
diff --git a/ooni/bridget/tests/bridget.py b/ooni/bridget/tests/bridget.py
new file mode 100644
index 0000000..a334747
--- /dev/null
+++ b/ooni/bridget/tests/bridget.py
@@ -0,0 +1,499 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+#
+#  +-----------+
+#  |  BRIDGET  |
+#  |        +--------------------------------------------+
+#  +--------| Use a Tor process to test making a Tor     |
+#           | connection to a list of bridges or relays. |
+#           +--------------------------------------------+
+#
+# :authors: Isis Lovecruft, Arturo Filasto
+# :licence: see included LICENSE
+# :version: 0.1.0-alpha
+
+from __future__           import with_statement
+from functools            import partial
+from random               import randint
+
+import os
+import sys
+
+from twisted.python       import usage
+from twisted.plugin       import IPlugin
+from twisted.internet     import defer, error, reactor
+from zope.interface       import implements
+
+from ooni.utils           import log, date
+from ooni.utils.config    import ValueChecker
+
+from ooni.plugoo.tests    import ITest, OONITest
+from ooni.plugoo.assets   import Asset, MissingAssetException
+from ooni.utils.onion     import TxtorconImportError
+from ooni.utils.onion     import PTNoBridgesException, PTNotFoundException
+
+try:
+    from ooni.utils.onion     import parse_data_dir
+except:
+    log.msg("Please go to /ooni/lib and do 'make txtorcon' to run this test!")
+
+class RandomPortException(Exception):
+    """Raised when using a random port conflicts with configured ports."""
+    def __init__(self):
+        log.msg("Unable to use random and specific ports simultaneously")
+        return sys.exit()
+
+class BridgetArgs(usage.Options):
+    """Commandline options."""
+    allowed = "Port to use for Tor's %s, must be between 1024 and 65535."
+    sock_check = ValueChecker(allowed % "SocksPort").port_check
+    ctrl_check = ValueChecker(allowed % "ControlPort").port_check
+
+    optParameters = [
+        ['bridges', 'b', None,
+         'File listing bridge IP:ORPorts to test'],
+        ['relays', 'f', None,
+         'File listing relay IPs to test'],
+        ['socks', 's', 9049, None, sock_check],
+        ['control', 'c', 9052, None, ctrl_check],
+        ['torpath', 'p', None,
+         'Path to the Tor binary to use'],
+        ['datadir', 'd', None,
+         'Tor DataDirectory to use'],
+        ['transport', 't', None,
+         'Tor ClientTransportPlugin'],
+        ['resume', 'r', 0,
+         'Resume at this index']]
+    optFlags = [['random', 'x', 'Use random ControlPort and SocksPort']]
+
+    def postOptions(self):
+        if not self['bridges'] and not self['relays']:
+            raise MissingAssetException(
+                "Bridget can't run without bridges or relays to test!")
+        if self['transport']:
+            ValueChecker.uid_check(
+                "Can't run bridget as root with pluggable transports!")
+            if not self['bridges']:
+                raise PTNoBridgesException
+        if self['socks'] or self['control']:
+            if self['random']:
+                raise RandomPortException
+        if self['datadir']:
+            ValueChecker.dir_check(self['datadir'])
+        if self['torpath']:
+            ValueChecker.file_check(self['torpath'])
+
+class BridgetAsset(Asset):
+    """Class for parsing bridget Assets ignoring commented out lines."""
+    def __init__(self, file=None):
+        self = Asset.__init__(self, file)
+
+    def parse_line(self, line):
+        if line.startswith('#'):
+            return
+        else:
+            return line.replace('\n','')
+
+class BridgetTest(OONITest):
+    """
+    XXX fill me in
+
+    :ivar config:
+        An :class:`ooni.lib.txtorcon.TorConfig` instance.
+    :ivar relays:
+        A list of all provided relays to test.
+    :ivar bridges:
+        A list of all provided bridges to test.
+    :ivar socks_port:
+        Integer for Tor's SocksPort.
+    :ivar control_port:
+        Integer for Tor's ControlPort.
+    :ivar transport:
+        String defining the Tor's ClientTransportPlugin, for testing
+        a bridge's pluggable transport functionality.
+    :ivar tor_binary:
+        Path to the Tor binary to use, e.g. \'/usr/sbin/tor\'
+    """
+    implements(IPlugin, ITest)
+
+    shortName    = "bridget"
+    description  = "Use a Tor process to test connecting to bridges or relays"
+    requirements = None
+    options      = BridgetArgs
+    blocking     = False
+
+    def initialize(self):
+        """
+        Extra initialization steps. We only want one child Tor process
+        running, so we need to deal with most of the TorConfig() only once,
+        before the experiment runs.
+        """
+        self.socks_port      = 9049
+        self.control_port    = 9052
+        self.circuit_timeout = 90
+        self.tor_binary      = '/usr/sbin/tor'
+        self.data_directory  = None
+
+        def __make_asset_list__(opt, lst):
+            log.msg("Loading information from %s ..." % opt)
+            with open(opt) as opt_file:
+                for line in opt_file.readlines():
+                    if line.startswith('#'):
+                        continue
+                    else:
+                        lst.append(line.replace('\n',''))
+
+        def __count_remaining__(which):
+            total, reach, unreach = map(lambda x: which[x],
+                                        ['all', 'reachable', 'unreachable'])
+            count = len(total) - reach() - unreach()
+            return count
+
+        ## XXX should we do report['bridges_up'].append(self.bridges['current'])
+        self.bridges = {}
+        self.bridges['all'], self.bridges['up'], self.bridges['down'] = \
+            ([] for i in range(3))
+        self.bridges['reachable']   = lambda: len(self.bridges['up'])
+        self.bridges['unreachable'] = lambda: len(self.bridges['down'])
+        self.bridges['remaining']   = lambda: __count_remaining__(self.bridges)
+        self.bridges['current']     = None
+        self.bridges['pt_type']     = None
+        self.bridges['use_pt']      = False
+
+        self.relays = {}
+        self.relays['all'], self.relays['up'], self.relays['down'] = \
+            ([] for i in range(3))
+        self.relays['reachable']   = lambda: len(self.relays['up'])
+        self.relays['unreachable'] = lambda: len(self.relays['down'])
+        self.relays['remaining']   = lambda: __count_remaining__(self.relays)
+        self.relays['current']     = None
+
+        if self.local_options:
+            try:
+                from ooni.lib.txtorcon import TorConfig
+            except ImportError:
+                raise TxtorconImportError
+            else:
+                self.config = TorConfig()
+            finally:
+                options = self.local_options
+
+            if options['bridges']:
+                self.config.UseBridges = 1
+                __make_asset_list__(options['bridges'], self.bridges['all'])
+            if options['relays']:
+                ## first hop must be in TorState().guards
+                self.config.EntryNodes = ','.join(relay_list)
+                __make_asset_list__(options['relays'], self.relays['all'])
+            if options['socks']:
+                self.socks_port = options['socks']
+            if options['control']:
+                self.control_port = options['control']
+            if options['random']:
+                log.msg("Using randomized ControlPort and SocksPort ...")
+                self.socks_port   = randint(1024, 2**16)
+                self.control_port = randint(1024, 2**16)
+            if options['torpath']:
+                self.tor_binary = options['torpath']
+            if options['datadir']:
+                self.data_directory = parse_data_dir(options['datadir'])
+            if options['transport']:
+                ## ClientTransportPlugin transport exec pathtobinary [options]
+                ## XXX we need a better way to deal with all PTs
+                log.msg("Using ClientTransportPlugin %s" % options['transport'])
+                self.bridges['use_pt'] = True
+                [self.bridges['pt_type'], pt_exec] = \
+                    options['transport'].split(' ', 1)
+
+                if self.bridges['pt_type'] == "obfs2":
+                    self.config.ClientTransportPlugin = \
+                        self.bridges['pt_type'] + " " + pt_exec
+                else:
+                    raise PTNotFoundException
+
+            self.config.SocksPort            = self.socks_port
+            self.config.ControlPort          = self.control_port
+            self.config.CookieAuthentication = 1
+
+    def __load_assets__(self):
+        """
+        Load bridges and/or relays from files given in user options. Bridges
+        should be given in the form IP:ORport. We don't want to load these as
+        assets, because it's inefficient to start a Tor process for each one.
+
+        We cannot use the Asset model, because that model calls
+        self.experiment() with the current Assets, which would be one relay
+        and one bridge, then it gives the defer.Deferred returned from
+        self.experiment() to self.control(), which means that, for each
+        (bridge, relay) pair, experiment gets called again, which instantiates
+        an additional Tor process that attempts to bind to the same
+        ports. Thus, additionally instantiated Tor processes return with
+        RuntimeErrors, which break the final defer.chainDeferred.callback(),
+        sending it into the errback chain.
+        """
+        assets = {}
+        if self.local_options:
+            if self.local_options['bridges']:
+                assets.update({'bridge':
+                               BridgetAsset(self.local_options['bridges'])})
+            if self.local_options['relays']:
+                assets.update({'relay':
+                               BridgetAsset(self.local_options['relays'])})
+        return assets
+
+    def experiment(self, args):
+        """
+        if bridges:
+            1. configure first bridge line
+            2a. configure data_dir, if it doesn't exist
+            2b. write torrc to a tempfile in data_dir
+            3. start tor                              } if any of these
+            4. remove bridges which are public relays } fail, add current
+            5. SIGHUP for each bridge                 } bridge to unreach-
+                                                      } able bridges.
+        if relays:
+            1a. configure the data_dir, if it doesn't exist
+            1b. write torrc to a tempfile in data_dir
+            2. start tor
+            3. remove any of our relays which are already part of current
+               circuits
+            4a. attach CustomCircuit() to self.state
+            4b. RELAY_EXTEND for each relay } if this fails, add
+                                            } current relay to list
+                                            } of unreachable relays
+            5.
+        if bridges and relays:
+            1. configure first bridge line
+            2a. configure data_dir if it doesn't exist
+            2b. write torrc to a tempfile in data_dir
+            3. start tor
+            4. remove bridges which are public relays
+            5. remove any of our relays which are already part of current
+               circuits
+            6a. attach CustomCircuit() to self.state
+            6b. for each bridge, build three circuits, with three
+                relays each
+            6c. RELAY_EXTEND for each relay } if this fails, add
+                                            } current relay to list
+                                            } of unreachable relays
+
+        :param args:
+            The :class:`BridgetAsset` line currently being used. Except that it
+            in Bridget it doesn't, so it should be ignored and avoided.
+        """
+        try:
+            from ooni.utils         import process
+            from ooni.utils.onion   import remove_public_relays, start_tor
+            from ooni.utils.onion   import start_tor_filter_nodes
+            from ooni.utils.onion   import setup_fail, setup_done
+            from ooni.utils.onion   import CustomCircuit
+            from ooni.utils.timer   import deferred_timeout, TimeoutError
+            from ooni.lib.txtorcon  import TorConfig, TorState
+        except ImportError:
+            raise TxtorconImportError
+        except TxtorconImportError, tie:
+            log.err(tie)
+            sys.exit()
+
+        def reconfigure_done(state, bridges):
+            """
+            Append :ivar:`bridges['current']` to the list
+            :ivar:`bridges['up'].
+            """
+            log.msg("Reconfiguring with 'Bridge %s' successful"
+                    % bridges['current'])
+            bridges['up'].append(bridges['current'])
+            return state
+
+        def reconfigure_fail(state, bridges):
+            """
+            Append :ivar:`bridges['current']` to the list
+            :ivar:`bridges['down'].
+            """
+            log.msg("Reconfiguring TorConfig with parameters %s failed"
+                    % state)
+            bridges['down'].append(bridges['current'])
+            return state
+
+        @defer.inlineCallbacks
+        def reconfigure_bridge(state, bridges):
+            """
+            Rewrite the Bridge line in our torrc. If use of pluggable
+            transports was specified, rewrite the line as:
+                Bridge <transport_type> <IP>:<ORPort>
+            Otherwise, rewrite in the standard form:
+                Bridge <IP>:<ORPort>
+
+            :param state:
+                A fully bootstrapped instance of
+                :class:`ooni.lib.txtorcon.TorState`.
+            :param bridges:
+                A dictionary of bridges containing the following keys:
+
+                bridges['remaining'] :: A function returning and int for the
+                                        number of remaining bridges to test.
+                bridges['current']   :: A string containing the <IP>:<ORPort>
+                                        of the current bridge.
+                bridges['use_pt']    :: A boolean, True if we're testing
+                                        bridges with a pluggable transport;
+                                        False otherwise.
+                bridges['pt_type']   :: If :ivar:`bridges['use_pt'] is True,
+                                        this is a string containing the type
+                                        of pluggable transport to test.
+            :return:
+                :param:`state`
+            """
+            log.msg("Current Bridge: %s" % bridges['current'])
+            log.msg("We now have %d bridges remaining to test..."
+                    % bridges['remaining']())
+            try:
+                if bridges['use_pt'] is False:
+                    controller_response = yield state.protocol.set_conf(
+                        'Bridge', bridges['current'])
+                elif bridges['use_pt'] and bridges['pt_type'] is not None:
+                    controller_reponse = yield state.protocol.set_conf(
+                        'Bridge', bridges['pt_type'] +' '+ bridges['current'])
+                else:
+                    raise PTNotFoundException
+
+                if controller_response == 'OK':
+                    finish = yield reconfigure_done(state, bridges)
+                else:
+                    log.err("SETCONF for %s responded with error:\n %s"
+                            % (bridges['current'], controller_response))
+                    finish = yield reconfigure_fail(state, bridges)
+
+                defer.returnValue(finish)
+
+            except Exception, e:
+                log.err("Reconfiguring torrc with Bridge line %s failed:\n%s"
+                        % (bridges['current'], e))
+                defer.returnValue(None)
+
+        def attacher_extend_circuit(attacher, deferred, router):
+            ## XXX todo write me
+            ## state.attacher.extend_circuit
+            raise NotImplemented
+            #attacher.extend_circuit
+
+        def state_attach(state, path):
+            log.msg("Setting up custom circuit builder...")
+            attacher = CustomCircuit(state)
+            state.set_attacher(attacher, reactor)
+            state.add_circuit_listener(attacher)
+            return state
+
+            ## OLD
+            #for circ in state.circuits.values():
+            #    for relay in circ.path:
+            #        try:
+            #            relay_list.remove(relay)
+            #        except KeyError:
+            #            continue
+            ## XXX how do we attach to circuits with bridges?
+            d = defer.Deferred()
+            attacher.request_circuit_build(d)
+            return d
+
+        def state_attach_fail(state):
+            log.err("Attaching custom circuit builder failed: %s" % state)
+
+        log.msg("Bridget: initiating test ... ")  ## Start the experiment
+
+        ## if we've at least one bridge, and our config has no 'Bridge' line
+        if self.bridges['remaining']() >= 1 \
+                and not 'Bridge' in self.config.config:
+
+            ## configure our first bridge line
+            self.bridges['current'] = self.bridges['all'][0]
+            self.config.Bridge = self.bridges['current']
+                                                  ## avoid starting several
+            self.config.save()                    ## processes
+            assert self.config.config.has_key('Bridge'), "No Bridge Line"
+
+            ## start tor and remove bridges which are public relays
+            from ooni.utils.onion import start_tor_filter_nodes
+            state = start_tor_filter_nodes(reactor, self.config,
+                                           self.control_port, self.tor_binary,
+                                           self.data_directory, self.bridges)
+            #controller = defer.Deferred()
+            #controller.addCallback(singleton_semaphore, tor)
+            #controller.addErrback(setup_fail)
+            #bootstrap = defer.gatherResults([controller, filter_bridges],
+            #                                consumeErrors=True)
+
+            if state is not None:
+                log.debug("state:\n%s" % state)
+                log.debug("Current callbacks on TorState():\n%s"
+                          % state.callbacks)
+
+        ## if we've got more bridges
+        if self.bridges['remaining']() >= 2:
+            #all = []
+            for bridge in self.bridges['all'][1:]:
+                self.bridges['current'] = bridge
+                #new = defer.Deferred()
+                #new.addCallback(reconfigure_bridge, state, self.bridges)
+                #all.append(new)
+            #check_remaining = defer.DeferredList(all, consumeErrors=True)
+            #state.chainDeferred(check_remaining)
+                state.addCallback(reconfigure_bridge, self.bridges)
+
+        if self.relays['remaining']() > 0:
+            while self.relays['remaining']() >= 3:
+                #path = list(self.relays.pop() for i in range(3))
+                #log.msg("Trying path %s" % '->'.join(map(lambda node:
+                #                                         node, path)))
+                self.relays['current'] = self.relays['all'].pop()
+                for circ in state.circuits.values():
+                    for node in circ.path:
+                        if node == self.relays['current']:
+                            self.relays['up'].append(self.relays['current'])
+                    if len(circ.path) < 3:
+                        try:
+                            ext = attacher_extend_circuit(state.attacher, circ,
+                                                          self.relays['current'])
+                            ext.addCallback(attacher_extend_circuit_done,
+                                            state.attacher, circ,
+                                            self.relays['current'])
+                        except Exception, e:
+                            log.err("Extend circuit failed: %s" % e)
+                    else:
+                        continue
+
+        #state.callback(all)
+        #self.reactor.run()
+        return state
+
+    def startTest(self, args):
+        """
+        Local override of :meth:`OONITest.startTest` to bypass calling
+        self.control.
+
+        :param args:
+            The current line of :class:`Asset`, not used but kept for
+            compatibility reasons.
+        :return:
+            A fired deferred which callbacks :meth:`experiment` and
+            :meth:`OONITest.finished`.
+        """
+        self.start_time = date.now()
+        self.d = self.experiment(args)
+        self.d.addErrback(log.err)
+        self.d.addCallbacks(self.finished, log.err)
+        return self.d
+
+## So that getPlugins() can register the Test:
+#bridget = BridgetTest(None, None, None)
+
+## ISIS' NOTES
+## -----------
+## TODO:
+##       x  cleanup documentation
+##       x  add DataDirectory option
+##       x  check if bridges are public relays
+##       o  take bridge_desc file as input, also be able to give same
+##          format as output
+##       x  Add asynchronous timeout for deferred, so that we don't wait
+##       o  Add assychronous timout for deferred, so that we don't wait
+##          forever for bridges that don't work.
diff --git a/ooni/bridget/utils/__init__.py b/ooni/bridget/utils/__init__.py
new file mode 100644
index 0000000..92893d6
--- /dev/null
+++ b/ooni/bridget/utils/__init__.py
@@ -0,0 +1 @@
+all = ['inputs', 'log', 'onion', 'tests', 'interface', 'nodes', 'reports', 'work']
diff --git a/ooni/bridget/utils/inputs.py b/ooni/bridget/utils/inputs.py
new file mode 100644
index 0000000..fe058cc
--- /dev/null
+++ b/ooni/bridget/utils/inputs.py
@@ -0,0 +1,174 @@
+#-*- coding: utf-8 -*-
+#
+#    inputs.py
+#    *********
+#
+#    "...quis custodiet ipsos custodes?"
+#               - Juvenal, Satires VI.347-348 (circa 2nd Century, C.E.)
+#
+#    :copyright: (c) 2012 Isis Lovecruft
+#    :license: see LICENSE for more details.
+#    :version: 0.1.0-beta
+#
+
+#from types        import FunctionType, FileType
+import types
+
+from ooni.bridget import log
+from ooni.utils   import date, Storage
+
+class InputFile:
+    """
+    This is a class describing a file used to store Tor bridge or relays
+    inputs. It is a python iterator object, allowing it to be efficiently
+    looped.
+
+    This class should not be used directly, but rather its subclasses,
+    BridgeFile and RelayFile should be used instead.
+    """
+
+    def __init__(self, file, **kw):
+        """
+        ## This is an InputAsset file, created because you tried to pass a
+        ## non-existent filename to a test.
+        ##
+        ## To use this file, place one input to be tested per line. Each
+        ## test takes different inputs. Lines which are commented out with
+        ## a '#' are not used.
+        """
+        self.file    = file
+        self.eof     = False
+        self.all     = Storage()
+
+        for key, value in input_dict:
+            self.all[key] = value
+
+        try:
+            self.handler = open(self.file, 'r')
+        except IOError:
+            with open(self.file, 'w') as explain:
+                for line in self.__init__.__doc__:
+                    explain.writeline(line)
+            self.handler = open(self.file, 'r')
+        try:
+            assert isinstance(self.handler, file), "That's not a file!"
+        except AssertionError, ae:
+            log.err(ae)
+
+    # def __handler__(self):
+    #     """
+    #     Attempt to open InputFile.file and check that it is actually a file.
+    #     If it's not, create it and add an explaination for how InputFile files
+    #     should be used.
+
+    #     :return:
+    #         A :type:`file` which has been opened in read-only mode.
+    #     """
+    #     try:
+    #         handler = open(self.file, 'r')
+    #     except IOError, ioerror:             ## not the hacker <(A)3
+    #         log.err(ioerror)
+    #         explanation = (
+    #         with open(self.file, 'w') as explain:
+    #             for line in explanation:
+    #                 explain.writeline(line)
+    #         handler = open(self.file, 'r')
+    #     try:
+    #         assert isinstance(handler, file), "That's not a file!"
+    #     except AssertionError, ae:
+    #         log.err(ae)
+    #     else:
+    #         return handler
+
+    def __iter__(next, StopIteration):
+        """
+        Returns the next input from the file.
+        """
+        #return self.next()
+        return self
+
+    def len(self):
+        """
+        Returns the number of the lines in the InputFile.
+        """
+        with open(self.file, 'r') as input_file:
+            lines = input_file.readlines()
+            for number, line in enumerate(lines):
+                self.input_dict[number] = line
+        return number + 1
+
+    def next(self):
+        try:
+            return self.next_input()
+        except:
+            raise StopIteration
+
+    def next_input(self):
+        """
+        Return the next input.
+        """
+        line = self.handler.readline()
+        if line:
+            parsed_line = self.parse_line(line)
+            if parsed_line:
+                return parsed_line
+        else:
+            self.fh.seek(0)
+            raise StopIteration
+
+    def default_parser(self, line):
+        """
+        xxx fill me in
+        """
+        if not line.startswith('#'):
+            return line.replace('\n', '')
+        else:
+            return False
+
+    def parse_line(self, line):
+        """
+        Override this method if you need line by line parsing of an Asset.
+
+        The default parsing action is to ignore lines which are commented out
+        with a '#', and to strip the newline character from the end of the
+        line.
+
+        If the line was commented out return an empty string instead.
+
+        If a subclass Foo incorporates another class Bar, when Bar is not
+        also a subclass of InputFile, and Bar.parse_line() exists, then
+        do not overwrite Bar's parse_line method.
+        """
+        assert not hasattr(super(InputFile, self), 'parse_line')
+
+        if self.parser is None:
+            if not line.startswith('#'):
+                return line.replace('\n', '')
+            else:
+                return ''
+        else:
+            try:
+                assert isinstance(self.parser, FunctionType),"Not a function!"
+            except AssertionError, ae:
+                log.err(ae)
+            else:
+                return self.parser(line)
+
+class BridgeFile(InputFile):
+    """
+    xxx fill me in
+    """
+    def __init__(self, **kw):
+        super(BridgeFile, self).__init__(**kw)
+
+class MissingInputException(Exception):
+    """
+
+    Raised when an :class:`InputFile` necessary for running the Test is
+    missing.
+
+    """
+    def __init__(self, error_message):
+        print error_message
+        import sys
+        return sys.exit()
diff --git a/ooni/bridget/utils/interface.py b/ooni/bridget/utils/interface.py
new file mode 100644
index 0000000..aa55436
--- /dev/null
+++ b/ooni/bridget/utils/interface.py
@@ -0,0 +1,54 @@
+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/bridget/utils/log.py b/ooni/bridget/utils/log.py
new file mode 100644
index 0000000..eef50d8
--- /dev/null
+++ b/ooni/bridget/utils/log.py
@@ -0,0 +1,98 @@
+"""
+OONI logging facility.
+"""
+from sys                    import stderr, stdout
+
+from twisted.python         import log, util
+from twisted.python.failure import Failure
+
+def _get_log_level(level):
+    english = ['debug', 'info', 'warn', 'err', 'crit']
+
+    levels = dict(zip(range(len(english)), english))
+    number = dict(zip(english, range(len(english))))
+
+    if not level:
+        return number['info']
+    else:
+        ve = "Unknown log level: %s\n" % level
+        ve += "Allowed levels: %s\n" % [word for word in english]
+
+        if type(level) is int:
+            if 0 <= level <= 4:
+                return level
+        elif type(level) is str:
+            if number.has_key(level.lower()):
+                return number[level]
+            else:
+                raise ValueError, ve
+        else:
+            raise ValueError, ve
+
+class OONITestFailure(Failure):
+    """
+    For handling Exceptions asynchronously.
+
+    Can be given an Exception as an argument, else will use the
+    most recent Exception from the current stack frame.
+    """
+    def __init__(self, exception=None, _type=None,
+                 _traceback=None, _capture=False):
+        Failure.__init__(self, exc_type=_type,
+                         exc_tb=_traceback, captureVars=_capture)
+
+class OONILogObserver(log.FileLogObserver):
+    """
+    Supports logging level verbosity.
+    """
+    def __init__(self, logfile, verb=None):
+        log.FileLogObserver.__init__(self, logfile)
+        self.level = _get_log_level(verb) if verb is not None else 1
+        assert type(self.level) is int
+
+    def emit(self, eventDict):
+        if 'logLevel' in eventDict:
+            msgLvl = _get_log_level(eventDict['logLevel'])
+            assert type(msgLvl) is int
+            ## only log our level and higher
+            if self.level <= msgLvl:
+                text = log.textFromEventDict(eventDict)
+            else:
+                text = None
+        else:
+            text = log.textFromEventDict(eventDict)
+
+        if text is None:
+            return
+
+        timeStr = self.formatTime(eventDict['time'])
+        fmtDict = {'system': eventDict['system'],
+                   'text': text.replace('\n','\n\t')}
+        msgStr  = log._safeFormat("[%(system)s] %(text)s\n", fmtDict)
+
+        util.untilConcludes(self.write, timeStr + " " + msgStr)
+        util.untilConcludes(self.flush)
+
+def start(logfile=None, verbosity=None):
+    if log.defaultObserver:
+        verbosity = _get_log_level(verbosity)
+
+        ## Always log to file, keep level at info
+        file = open(logfile, 'a') if logfile else stderr
+        OONILogObserver(file, "info").start()
+
+        log.msg("Starting OONI...")
+
+def debug(message, level="debug", **kw):
+    print "[%s] %s" % (level, message)
+    ## If we want debug messages in the logfile:
+    #log.msg(message, logLevel=level, **kw)
+
+def msg(message, level="info", **kw):
+    log.msg(message, logLevel=level, **kw)
+
+def err(message, level="err", **kw):
+    log.err(logLevel=level, **kw)
+
+def fail(message, exception, level="crit", **kw):
+    log.failure(message, OONITestFailure(exception, **kw), logLevel=level)
diff --git a/ooni/bridget/utils/nodes.py b/ooni/bridget/utils/nodes.py
new file mode 100644
index 0000000..155f183
--- /dev/null
+++ b/ooni/bridget/utils/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/bridget/utils/onion.py b/ooni/bridget/utils/onion.py
new file mode 100644
index 0000000..9d4cae7
--- /dev/null
+++ b/ooni/bridget/utils/onion.py
@@ -0,0 +1,686 @@
+#
+# onion.py
+# ----------
+# Utilities for working with Tor.
+#
+# This code is largely taken from txtorcon and its documentation, and as such
+# any and all credit should go to Meejah. Minor adjustments have been made to
+# use OONI's logging system, and to build custom circuits without actually
+# attaching streams.
+#
+# :author: Meejah, Isis Lovecruft
+# :license: see included LICENSE file
+# :copyright: copyright (c) 2012 The Tor Project, Inc.
+# :version: 0.1.0-alpha
+#
+# XXX TODO add report keys for onion methods
+
+import random
+import sys
+
+from twisted.internet  import defer
+from zope.interface    import implements
+
+from ooni.lib.txtorcon import CircuitListenerMixin, IStreamAttacher
+from ooni.lib.txtorcon import TorState, TorConfig
+from ooni.utils        import log
+from ooni.utils.timer  import deferred_timeout, TimeoutError
+
+def parse_data_dir(data_dir):
+    """
+    Parse a string that a has been given as a DataDirectory and determine
+    its absolute path on the filesystem.
+
+    :param data_dir:
+        A directory for Tor's DataDirectory, to be parsed.
+    :return:
+        The absolute path of :param:data_dir.
+    """
+    from os import path, getcwd
+    import sys
+
+    try:
+        assert isinstance(data_dir, str), \
+            "Parameter type(data_dir) must be str"
+    except AssertionError, ae:
+        log.err(ae)
+
+    if data_dir.startswith('~'):
+        data_dir = path.expanduser(data_dir)
+    elif data_dir.startswith('/'):
+        data_dir = path.join(getcwd(), data_dir)
+    elif data_dir.startswith('./'):
+        data_dir = path.abspath(data_dir)
+    else:
+        data_dir = path.join(getcwd(), data_dir)
+
+    try:
+        assert path.isdir(data_dir), "Could not find %s" % data_dir
+    except AssertionError, ae:
+        log.err(ae)
+        sys.exit()
+    else:
+        return data_dir
+
+def write_torrc(conf, data_dir=None):
+    """
+    Create a torrc in our data_dir. If we don't yet have a data_dir, create a
+    temporary one. Any temporary files or folders are added to delete_list.
+
+    :param conf:
+        A :class:`ooni.lib.txtorcon.TorConfig` object, with all configuration
+        values saved.
+    :param data_dir:
+        The Tor DataDirectory to use.
+    :return: torrc, data_dir, delete_list
+    """
+    try:
+        from os       import write, close
+        from tempfile import mkstemp, mkdtemp
+    except ImportError, ie:
+        log.err(ie)
+
+    delete_list = []
+
+    if data_dir is None:
+        data_dir = mkdtemp(prefix='bridget-tordata')
+        delete_list.append(data_dir)
+    conf.DataDirectory = data_dir
+
+    (fd, torrc) = mkstemp(dir=data_dir)
+    delete_list.append(torrc)
+    write(fd, conf.create_torrc())
+    close(fd)
+
+    return torrc, data_dir, delete_list
+
+def delete_files_or_dirs(delete_list):
+    """
+    Given a list of files or directories to delete, delete all and suppress
+    all errors.
+
+    :param delete_list:
+        A list of files or directories to delete.
+    """
+    try:
+        from os     import unlink
+        from shutil import rmtree
+    except ImportError, ie:
+        log.err(ie)
+
+    for temp in delete_list:
+        try:
+            unlink(temp)
+        except OSError:
+            rmtree(temp, ignore_errors=True)
+
+def remove_node_from_list(node, list):
+    for item in list:              ## bridges don't match completely
+        if item.startswith(node):  ## due to the :<port>.
+            try:
+                log.msg("Removing %s because it is a public relay" % node)
+                list.remove(item)
+            except ValueError, ve:
+                log.err(ve)
+
+def remove_public_relays(state, bridges):
+    """
+    Remove bridges from our bridge list which are also listed as public
+    relays. This must be called after Tor has fully bootstrapped and we have a
+    :class:`ooni.lib.txtorcon.TorState` with the
+    :attr:`ooni.lib.txtorcon.TorState.routers` attribute assigned.
+
+    XXX Does state.router.values() have all of the relays in the consensus, or
+    just the ones we know about so far?
+
+    XXX FIXME: There is a problem in that Tor needs a Bridge line to already be
+    configured in order to bootstrap. However, after bootstrapping, we grab the
+    microdescriptors of all the relays and check if any of our bridges are
+    listed as public relays. Because of this, the first bridge does not get
+    checked for being a relay.
+    """
+    IPs = map(lambda addr: addr.split(':',1)[0], bridges['all'])
+    both = set(state.routers.values()).intersection(IPs)
+
+    if len(both) > 0:
+        try:
+            updated = map(lambda node: remove_node_from_list(node), both)
+            log.debug("Bridges in both: %s" % both)
+            log.debug("Updated = %s" % updated)
+            #if not updated:
+            #    defer.returnValue(state)
+            #else:
+            #    defer.returnValue(state)
+            return state
+        except Exception, e:
+            log.err("Removing public relays %s from bridge list failed:\n%s"
+                    % (both, e))
+
+def setup_done(proto):
+    log.msg("Setup Complete")
+    state = TorState(proto.tor_protocol)
+    state.post_bootstrap.addCallback(state_complete)
+    state.post_bootstrap.addErrback(setup_fail)
+
+def setup_fail(proto):
+    log.msg("Setup Failed:\n%s" % proto)
+    return proto
+    #reactor.stop()
+
+def state_complete(state):
+    """Called when we've got a TorState."""
+    log.msg("We've completely booted up a Tor version %s at PID %d"
+            % (state.protocol.version, state.tor_pid))
+    log.msg("This Tor has the following %d Circuits:"
+            % len(state.circuits))
+    for circ in state.circuits.values():
+        log.msg("%s" % circ)
+    return state
+
+def updates(_progress, _tag, _summary):
+    """Log updates on the Tor bootstrapping process."""
+    log.msg("%d%%: %s" % (_progress, _summary))
+
+def bootstrap(ctrl):
+    """
+    Bootstrap Tor from an instance of
+    :class:`ooni.lib.txtorcon.TorControlProtocol`.
+    """
+    conf = TorConfig(ctrl)
+    conf.post_bootstrap.addCallback(setup_done).addErrback(setup_fail)
+    log.msg("Tor process connected, bootstrapping ...")
+
+def start_tor(reactor, config, control_port, tor_binary, data_dir,
+              report=None, progress=updates,
+              process_cb=None, process_eb=None):
+    """
+    Use a txtorcon.TorConfig() instance, config, to write a torrc to a
+    tempfile in our DataDirectory, data_dir. If data_dir is None, a temp
+    directory will be created. Finally, create a TCP4ClientEndpoint at our
+    control_port, and connect it to our reactor and a spawned Tor
+    process. Compare with :meth:`txtorcon.launch_tor` for differences.
+
+    :param reactor:
+        An instance of class:`twisted.internet.reactor`.
+    :param config:
+        An instance of class:`txtorcon.TorConfig` with all torrc options
+        already configured. ivar:`config.ControlPort`,
+        ivar:`config.SocksPort`, ivar:`config.CookieAuthentication`, should
+        already be set, as well as ivar:`config.UseBridges` and
+        ivar:`config.Bridge` if bridges are to be used.
+        ivar:`txtorcon.DataDirectory` does not need to be set.
+    :param control_port:
+        The port number to use for Tor's ControlPort.
+    :param tor_binary:
+        The full path to the Tor binary to use.
+    :param data_dir:
+        The directory to use as Tor's DataDirectory.
+    :param report:
+        The class:`ooni.plugoo.reports.Report` instance.
+    :param progress:
+        A non-blocking function to handle bootstrapping updates, which takes
+        three parameters: _progress, _tag, and _summary.
+    :param process_cb:
+        The function to callback to after
+        class:`ooni.lib.txtorcon.TorProcessProtocol` returns with the fully
+        bootstrapped Tor process.
+    :param process_eb:
+        The function to errback to if
+        class:`ooni.lib.txtorcon.TorProcessProtocol` fails.
+    :return:
+        The result of the callback of a
+        class:`ooni.lib.txtorcon.TorProcessProtocol` which callbacks with a
+        class:`txtorcon.TorControlProtocol` as .protocol.
+    """
+    try:
+        from functools                  import partial
+        from twisted.internet.endpoints import TCP4ClientEndpoint
+        from ooni.lib.txtorcon          import TorProtocolFactory
+        from ooni.lib.txtorcon          import TorProcessProtocol
+    except ImportError, ie:
+        log.err(ie)
+
+    ## TODO: add option to specify an already existing torrc, which
+    ##       will require prior parsing to enforce necessary lines
+    (torrc, data_dir, to_delete) = write_torrc(config, data_dir)
+
+    log.msg("Starting Tor ...")
+    log.msg("Using the following as our torrc:\n%s" % config.create_torrc())
+    if report is None:
+        report = {'torrc': config.create_torrc()}
+    else:
+        report.update({'torrc': config.create_torrc()})
+
+    end_point = TCP4ClientEndpoint(reactor, 'localhost', control_port)
+    connection_creator = partial(end_point.connect, TorProtocolFactory())
+    process_protocol = TorProcessProtocol(connection_creator, progress)
+    process_protocol.to_delete = to_delete
+
+    if process_cb is not None and process_eb is not None:
+        process_protocol.connected_cb.addCallbacks(process_cb, process_eb)
+
+    reactor.addSystemEventTrigger('before', 'shutdown',
+                                  partial(delete_files_or_dirs, to_delete))
+    try:
+        transport = reactor.spawnProcess(process_protocol,
+                                         tor_binary,
+                                         args=(tor_binary,'-f',torrc),
+                                         env={'HOME': data_dir},
+                                         path=data_dir)
+        transport.closeStdin()
+    except RuntimeError, e:
+       log.err("Starting Tor failed:")
+       process_protocol.connected_cb.errback(e)
+    except NotImplementedError, e:
+       url = "http://starship.python.net/crew/mhammond/win32/Downloads.html"
+       log.msg("Running bridget on Windows requires pywin32: %s" % url)
+       process_protocol.connected_cb.errback(e)
+
+    return process_protocol.connected_cb
+
+ at defer.inlineCallbacks
+def start_tor_filter_nodes(reactor, config, control_port, tor_binary,
+                              data_dir, bridges):
+    """
+    Bootstrap a Tor process and return a fully-setup
+    :class:`ooni.lib.txtorcon.TorState`. Then search for our bridges
+    to test in the list of known public relays,
+    :ivar:`ooni.lib.txtorcon.TorState.routers`, and remove any bridges
+    which are known public relays.
+
+    :param reactor:
+        The :class:`twisted.internet.reactor`.
+    :param config:
+        An instance of :class:`ooni.lib.txtorcon.TorConfig`.
+    :param control_port:
+        The port to use for Tor's ControlPort. If already configured in
+        the TorConfig instance, this can be given as
+        TorConfig.config.ControlPort.
+    :param tor_binary:
+        The full path to the Tor binary to execute.
+    :param data_dir:
+        The full path to the directory to use as Tor's DataDirectory.
+    :param bridges:
+        A dictionary which has a key 'all' which is a list of bridges to
+        test connecting to, e.g.:
+             bridges['all'] = ['1.1.1.1:443', '22.22.22.22:9001']
+    :return:
+        A fully initialized :class:`ooni.lib.txtorcon.TorState`.
+    """
+    setup = yield start_tor(reactor, config, control_port,
+                            tor_binary, data_dir,
+                            process_cb=setup_done, process_eb=setup_fail)
+    filter_nodes = yield remove_public_relays(setup, bridges)
+    defer.returnValue(filter_nodes)
+
+ at defer.inlineCallbacks
+def start_tor_with_timer(reactor, config, control_port, tor_binary, data_dir,
+                         bridges, timeout):
+    """
+    Start bootstrapping a Tor process wrapped with an instance of the class
+    decorator :func:`ooni.utils.timer.deferred_timeout` and complete callbacks
+    to either :func:`setup_done` or :func:`setup_fail`. Return a fully-setup
+    :class:`ooni.lib.txtorcon.TorState`. Then search for our bridges to test
+    in the list of known public relays,
+    :ivar:`ooni.lib.txtorcon.TorState.routers`, and remove any bridges which
+    are listed as known public relays.
+
+    :param reactor:
+        The :class:`twisted.internet.reactor`.
+    :param config:
+        An instance of :class:`ooni.lib.txtorcon.TorConfig`.
+    :param control_port:
+        The port to use for Tor's ControlPort. If already configured in
+        the TorConfig instance, this can be given as
+        TorConfig.config.ControlPort.
+    :param tor_binary:
+        The full path to the Tor binary to execute.
+    :param data_dir:
+        The full path to the directory to use as Tor's DataDirectory.
+    :param bridges:
+        A dictionary which has a key 'all' which is a list of bridges to
+        test connecting to, e.g.:
+             bridges['all'] = ['1.1.1.1:443', '22.22.22.22:9001']
+    :param timeout:
+        The number of seconds to attempt to bootstrap the Tor process before
+        raising a :class:`ooni.utils.timer.TimeoutError`.
+    :return:
+        If the timeout limit is not exceeded, return a fully initialized
+        :class:`ooni.lib.txtorcon.TorState`, else return None.
+    """
+    error_msg = "Bootstrapping has exceeded the timeout limit..."
+    with_timeout = deferred_timeout(timeout, e=error_msg)(start_tor)
+    try:
+        setup = yield with_timeout(reactor, config, control_port, tor_binary,
+                                   data_dir, process_cb=setup_done,
+                                   process_eb=setup_fail)
+    except TimeoutError, te:
+        log.err(te)
+        defer.returnValue(None)
+    #except Exception, e:
+    #    log.err(e)
+    #    defer.returnValue(None)
+    else:
+        state = yield remove_public_relays(setup, bridges)
+        defer.returnValue(state)
+
+ at defer.inlineCallbacks
+def start_tor_filter_nodes_with_timer(reactor, config, control_port,
+                                      tor_binary, data_dir, bridges, timeout):
+    """
+    Start bootstrapping a Tor process wrapped with an instance of the class
+    decorator :func:`ooni.utils.timer.deferred_timeout` and complete callbacks
+    to either :func:`setup_done` or :func:`setup_fail`. Then, filter our list
+    of bridges to remove known public relays by calling back to
+    :func:`remove_public_relays`. Return a fully-setup
+    :class:`ooni.lib.txtorcon.TorState`. Then search for our bridges to test
+    in the list of known public relays,
+    :ivar:`ooni.lib.txtorcon.TorState.routers`, and remove any bridges which
+    are listed as known public relays.
+
+    :param reactor:
+        The :class:`twisted.internet.reactor`.
+    :param config:
+        An instance of :class:`ooni.lib.txtorcon.TorConfig`.
+    :param control_port:
+        The port to use for Tor's ControlPort. If already configured in
+        the TorConfig instance, this can be given as
+        TorConfig.config.ControlPort.
+    :param tor_binary:
+        The full path to the Tor binary to execute.
+    :param data_dir:
+        The full path to the directory to use as Tor's DataDirectory.
+    :param bridges:
+        A dictionary which has a key 'all' which is a list of bridges to
+        test connecting to, e.g.:
+             bridges['all'] = ['1.1.1.1:443', '22.22.22.22:9001']
+    :param timeout:
+        The number of seconds to attempt to bootstrap the Tor process before
+        raising a :class:`ooni.utils.timer.TimeoutError`.
+    :return:
+        If the timeout limit is not exceeded, return a fully initialized
+        :class:`ooni.lib.txtorcon.TorState`, else return None.
+    """
+    error_msg = "Bootstrapping has exceeded the timeout limit..."
+    with_timeout = deferred_timeout(timeout, e=error_msg)(start_tor_filter_nodes)
+    try:
+        state = yield with_timeout(reactor, config, control_port,
+                                   tor_binary, data_dir, bridges)
+    except TimeoutError, te:
+        log.err(te)
+        defer.returnValue(None)
+    #except Exception, e:
+    #    log.err(e)
+    #    defer.returnValue(None)
+    else:
+        defer.returnValue(state)
+
+class CustomCircuit(CircuitListenerMixin):
+    """
+    Utility class for controlling circuit building. See
+    'attach_streams_by_country.py' in the txtorcon documentation.
+
+    :param state:
+        A fully bootstrapped instance of :class:`ooni.lib.txtorcon.TorState`.
+    :param relays:
+        A dictionary containing a key 'all', which is a list of relays to
+        test connecting to.
+    :ivar waiting_circuits:
+        The list of circuits which we are waiting to attach to. You shouldn't
+        need to touch this.
+    """
+    implements(IStreamAttacher)
+
+    def __init__(self, state, relays=None):
+        self.state = state
+        self.waiting_circuits = []
+        self.relays = relays
+
+    def waiting_on(self, circuit):
+        """
+        Whether or not we are waiting on the given circuit before attaching to
+        it.
+
+        :param circuit:
+            An item from :ivar:`ooni.lib.txtorcon.TorState.circuits`.
+        :return:
+            True if we are waiting on the circuit, False if not waiting.
+        """
+        for (circid, d) in self.waiting_circuits:
+            if circuit.id == circid:
+                return True
+        return False
+
+    def circuit_extend(self, circuit, router):
+        "ICircuitListener"
+        if circuit.purpose != 'GENERAL':
+            return
+        if self.waiting_on(circuit):
+            log.msg("Circuit %d (%s)" % (circuit.id, router.id_hex))
+
+    def circuit_built(self, circuit):
+        "ICircuitListener"
+        if circuit.purpose != 'GENERAL':
+            return
+        log.msg("Circuit %s built ..." % circuit.id)
+        log.msg("Full path of %s: %s" % (circuit.id, circuit.path))
+        for (circid, d) in self.waiting_circuits:
+            if circid == circuit.id:
+                self.waiting_circuits.remove((circid, d))
+                d.callback(circuit)
+
+    def circuit_failed(self, circuit, reason):
+        """
+        If building a circuit has failed, try to remove it from our list of
+        :ivar:`waiting_circuits`, else request to build it.
+
+        :param circuit:
+            An item from :ivar:`ooni.lib.txtorcon.TorState.circuits`.
+        :param reason:
+            A :class:`twisted.python.fail.Failure` instance.
+        :return:
+            None
+        """
+        if self.waiting_on(circuit):
+            log.msg("Circuit %s failed for reason %s" % (circuit.id, reason))
+            circid, d = None, None
+            for c in self.waiting_circuits:
+                if c[0] == circuit.id:
+                    circid, d = c
+            if d is None:
+                raise Exception("Expected to find circuit.")
+
+            self.waiting_circuits.remove((circid, d))
+            log.msg("Trying to build a circuit for %s" % circid)
+            self.request_circuit_build(d)
+
+    def check_circuit_route(self, router):
+        """
+        Check if a relay is a hop in one of our already built circuits.
+
+        :param router:
+            An item from the list
+            :func:`ooni.lib.txtorcon.TorState.routers.values()`.
+        """
+        for circ in self.state.circuits.values():
+            if router in circ.path:
+            #router.update() ## XXX can i use without args? no.
+                TorInfo.dump(self)
+
+    def request_circuit_build(self, deferred, path=None):
+        """
+        Request a custom circuit.
+
+        :param deferred:
+            A :class:`twisted.internet.defer.Deferred` for this circuit.
+        :param path:
+            A list of router ids to build a circuit from. The length of this
+            list must be at least three.
+        """
+        if path is None:
+
+            pick   = self.relays['all'].pop
+            n      = self.state.entry_guards.values()
+            choose = random.choice
+
+            first, middle, last = (None for i in range(3))
+
+            if self.relays['remaining']() >= 3:
+                first, middle, last = (pick() for i in range(3))
+            elif self.relays['remaining']() < 3:
+                first = choose(n)
+                middle = pick()
+                if self.relays['remaining'] == 2:
+                    middle, last = (pick() for i in range(2))
+                elif self.relay['remaining'] == 1:
+                    middle = pick()
+                    last = choose(n)
+                else:
+                    log.msg("Qu'est-que fuque?")
+            else:
+                middle, last = (random.choice(self.state.routers.values())
+                                for i in range(2))
+
+            path = [first, middle, last]
+
+        else:
+            assert isinstance(path, list), \
+                "Circuit path must be a list of relays!"
+            assert len(path) >= 3, \
+                "Circuit path must be at least three hops!"
+
+        log.msg("Requesting a circuit: %s"
+                % '->'.join(map(lambda node: node, path)))
+
+        class AppendWaiting:
+            def __init__(self, attacher, deferred):
+                self.attacher = attacher
+                self.d        = deferred
+            def __call__(self, circ):
+                """
+                Return from build_circuit is a Circuit, however,
+                we want to wait until it is built before we can
+                issue an attach on it and callback to the Deferred
+                we issue here.
+                """
+                log.msg("Circuit %s is in progress ..." % circ.id)
+                self.attacher.waiting_circuits.append((circ.id, self.d))
+
+        return self.state.build_circuit(path).addCallback(
+            AppendWaiting(self, deferred)).addErrback(
+            log.err)
+
+class TxtorconImportError(ImportError):
+    """
+    Raised when ooni.lib.txtorcon cannot be imported from. Checks our current
+    working directory and the path given to see if txtorcon has been
+    initialized via /ooni/lib/Makefile.
+    """
+    from os import getcwd, path
+
+    cwd, tx = getcwd(), 'lib/txtorcon/torconfig.py'
+    try:
+        log.msg("Unable to import from ooni.lib.txtorcon")
+        if cwd.endswith('ooni'):
+            check = path.join(cwd, tx)
+        elif cwd.endswith('utils'):
+            check = path.join(cwd, '../'+tx)
+        else:
+            check = path.join(cwd, 'ooni/'+tx)
+        assert path.isfile(check)
+    except:
+        log.msg("Error: Some OONI libraries are missing!")
+        log.msg("Please go to /ooni/lib/ and do \"make all\"")
+
+class PTNoBridgesException(Exception):
+    """Raised when a pluggable transport is specified, but not bridges."""
+    def __init__(self):
+        log.msg("Pluggable transport requires the bridges option")
+        return sys.exit()
+
+class PTNotFoundException(Exception):
+    def __init__(self, transport_type):
+        m  = "Pluggable Transport type %s was unaccounted " % transport_type
+        m += "for, please contact isis(at)torproject(dot)org and it will "
+        m += "get included."
+        log.msg("%s" % m)
+        return sys.exit()
+
+ at defer.inlineCallbacks
+def __start_tor_with_timer__(reactor, config, control_port, tor_binary,
+                             data_dir, bridges=None, relays=None, timeout=None,
+                             retry=None):
+    """
+    A wrapper for :func:`start_tor` which wraps the bootstrapping of a Tor
+    process and its connection to a reactor with a
+    :class:`twisted.internet.defer.Deferred` class decorator utility,
+    :func:`ooni.utils.timer.deferred_timeout`, and a mechanism for resets.
+
+    ## XXX fill me in
+    """
+    raise NotImplementedError
+
+    class RetryException(Exception):
+        pass
+
+    import sys
+    from ooni.utils.timer import deferred_timeout, TimeoutError
+
+    def __make_var__(old, default, _type):
+        if old is not None:
+            assert isinstance(old, _type)
+            new = old
+        else:
+            new = default
+        return new
+
+    reactor = reactor
+    timeout = __make_var__(timeout, 120, int)
+    retry   = __make_var__(retry, 1, int)
+
+    with_timeout = deferred_timeout(timeout)(start_tor)
+
+    @defer.inlineCallbacks
+    def __start_tor__(rc=reactor, cf=config, cp=control_port, tb=tor_binary,
+                      dd=data_dir, br=bridges, rl=relays, cb=setup_done,
+                      eb=setup_fail, af=remove_public_relays, retry=retry):
+        try:
+            setup = yield with_timeout(rc,cf,cp,tb,dd)
+        except TimeoutError:
+            retry -= 1
+            defer.returnValue(retry)
+        else:
+            if setup.callback:
+                setup = yield cb(setup)
+            elif setup.errback:
+                setup = yield eb(setup)
+            else:
+                setup = setup
+
+            if br is not None:
+                state = af(setup,br)
+            else:
+                state = setup
+            defer.returnValue(state)
+
+    @defer.inlineCallbacks
+    def __try_until__(tries):
+        result = yield __start_tor__()
+        try:
+            assert isinstance(result, int)
+        except AssertionError:
+            defer.returnValue(result)
+        else:
+            if result >= 0:
+                tried = yield __try_until__(result)
+                defer.returnValue(tried)
+            else:
+                raise RetryException
+    try:
+        tried = yield __try_until__(retry)
+    except RetryException:
+        log.msg("All retry attempts to bootstrap Tor have timed out.")
+        log.msg("Exiting ...")
+        defer.returnValue(sys.exit())
+    else:
+        defer.returnValue(tried)
diff --git a/ooni/bridget/utils/reports.py b/ooni/bridget/utils/reports.py
new file mode 100644
index 0000000..ae67b13
--- /dev/null
+++ b/ooni/bridget/utils/reports.py
@@ -0,0 +1,144 @@
+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/bridget/utils/tests.py b/ooni/bridget/utils/tests.py
new file mode 100644
index 0000000..ea4be0b
--- /dev/null
+++ b/ooni/bridget/utils/tests.py
@@ -0,0 +1,141 @@
+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/bridget/utils/work.py b/ooni/bridget/utils/work.py
new file mode 100644
index 0000000..c329c20
--- /dev/null
+++ b/ooni/bridget/utils/work.py
@@ -0,0 +1,147 @@
+# -*- 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/plugins/bridget.py b/ooni/plugins/bridget.py
deleted file mode 100644
index 5ff7b3f..0000000
--- a/ooni/plugins/bridget.py
+++ /dev/null
@@ -1,500 +0,0 @@
-#!/usr/bin/env python
-# -*- encoding: utf-8 -*-
-#
-#  +-----------+
-#  |  BRIDGET  |
-#  |        +--------------------------------------------+
-#  +--------| Use a Tor process to test making a Tor     |
-#           | connection to a list of bridges or relays. |
-#           +--------------------------------------------+
-#
-# :authors: Isis Lovecruft, Arturo Filasto
-# :licence: see included LICENSE
-# :version: 0.1.0-alpha
-
-from __future__           import with_statement
-from functools            import partial
-from random               import randint
-
-import os
-import sys
-
-from twisted.python       import usage
-from twisted.plugin       import IPlugin
-from twisted.internet     import defer, error, reactor
-from zope.interface       import implements
-
-from ooni.utils           import log, date
-from ooni.utils.config    import ValueChecker
-
-from ooni.plugoo.tests    import ITest, OONITest
-from ooni.plugoo.assets   import Asset, MissingAssetException
-from ooni.utils.onion     import TxtorconImportError
-from ooni.utils.onion     import PTNoBridgesException, PTNotFoundException
-
-try:
-    from ooni.utils.onion     import parse_data_dir
-except:
-    log.msg("Please go to /ooni/lib and do 'make txtorcon' to run this test!")
-
-class RandomPortException(Exception):
-    """Raised when using a random port conflicts with configured ports."""
-    def __init__(self):
-        log.msg("Unable to use random and specific ports simultaneously")
-        return sys.exit()
-
-class BridgetArgs(usage.Options):
-    """Commandline options."""
-    allowed = "Port to use for Tor's %s, must be between 1024 and 65535."
-    sock_check = ValueChecker(allowed % "SocksPort").port_check
-    ctrl_check = ValueChecker(allowed % "ControlPort").port_check
-
-    optParameters = [
-        ['bridges', 'b', None,
-         'File listing bridge IP:ORPorts to test'],
-        ['relays', 'f', None,
-         'File listing relay IPs to test'],
-        ['socks', 's', 9049, None, sock_check],
-        ['control', 'c', 9052, None, ctrl_check],
-        ['torpath', 'p', None,
-         'Path to the Tor binary to use'],
-        ['datadir', 'd', None,
-         'Tor DataDirectory to use'],
-        ['transport', 't', None,
-         'Tor ClientTransportPlugin'],
-        ['resume', 'r', 0,
-         'Resume at this index']]
-    optFlags = [['random', 'x', 'Use random ControlPort and SocksPort']]
-
-    def postOptions(self):
-        if not self['bridges'] and not self['relays']:
-            raise MissingAssetException(
-                "Bridget can't run without bridges or relays to test!")
-        if self['transport']:
-            ValueChecker.uid_check(
-                "Can't run bridget as root with pluggable transports!")
-            if not self['bridges']:
-                raise PTNoBridgesException
-        if self['socks'] or self['control']:
-            if self['random']:
-                raise RandomPortException
-        if self['datadir']:
-            ValueChecker.dir_check(self['datadir'])
-        if self['torpath']:
-            ValueChecker.file_check(self['torpath'])
-
-class BridgetAsset(Asset):
-    """Class for parsing bridget Assets ignoring commented out lines."""
-    def __init__(self, file=None):
-        self = Asset.__init__(self, file)
-
-    def parse_line(self, line):
-        if line.startswith('#'):
-            return
-        else:
-            return line.replace('\n','')
-
-class BridgetTest(OONITest):
-    """
-    XXX fill me in
-
-    :ivar config:
-        An :class:`ooni.lib.txtorcon.TorConfig` instance.
-    :ivar relays:
-        A list of all provided relays to test.
-    :ivar bridges:
-        A list of all provided bridges to test.
-    :ivar socks_port:
-        Integer for Tor's SocksPort.
-    :ivar control_port:
-        Integer for Tor's ControlPort.
-    :ivar transport:
-        String defining the Tor's ClientTransportPlugin, for testing 
-        a bridge's pluggable transport functionality.
-    :ivar tor_binary:
-        Path to the Tor binary to use, e.g. \'/usr/sbin/tor\'
-    """
-    implements(IPlugin, ITest)
-
-    shortName    = "bridget"
-    description  = "Use a Tor process to test connecting to bridges or relays"
-    requirements = None
-    options      = BridgetArgs
-    blocking     = False
-
-    def initialize(self):
-        """
-        Extra initialization steps. We only want one child Tor process
-        running, so we need to deal with most of the TorConfig() only once,
-        before the experiment runs.
-        """
-        self.socks_port      = 9049
-        self.control_port    = 9052
-        self.circuit_timeout = 90
-        self.tor_binary      = '/usr/sbin/tor'
-        self.data_directory  = None
-
-        def __make_asset_list__(opt, lst):
-            log.msg("Loading information from %s ..." % opt)
-            with open(opt) as opt_file:
-                for line in opt_file.readlines():
-                    if line.startswith('#'):
-                        continue
-                    else:
-                        lst.append(line.replace('\n',''))
-
-        def __count_remaining__(which):
-            total, reach, unreach = map(lambda x: which[x], 
-                                        ['all', 'reachable', 'unreachable'])
-            count = len(total) - reach() - unreach()
-            return count
-
-        ## XXX should we do report['bridges_up'].append(self.bridges['current'])
-        self.bridges = {}
-        self.bridges['all'], self.bridges['up'], self.bridges['down'] = \
-            ([] for i in range(3))
-        self.bridges['reachable']   = lambda: len(self.bridges['up'])
-        self.bridges['unreachable'] = lambda: len(self.bridges['down'])
-        self.bridges['remaining']   = lambda: __count_remaining__(self.bridges)
-        self.bridges['current']     = None
-        self.bridges['pt_type']     = None
-        self.bridges['use_pt']      = False
-
-        self.relays = {} 
-        self.relays['all'], self.relays['up'], self.relays['down'] = \
-            ([] for i in range(3))
-        self.relays['reachable']   = lambda: len(self.relays['up'])
-        self.relays['unreachable'] = lambda: len(self.relays['down'])
-        self.relays['remaining']   = lambda: __count_remaining__(self.relays)
-        self.relays['current']     = None
-
-        if self.local_options:
-            try:
-                from ooni.lib.txtorcon import TorConfig
-            except ImportError:
-                raise TxtorconImportError
-            else:
-                self.config = TorConfig()
-            finally:
-                options = self.local_options
-
-            if options['bridges']:
-                self.config.UseBridges = 1
-                __make_asset_list__(options['bridges'], self.bridges['all'])
-            if options['relays']:
-                ## first hop must be in TorState().guards
-                self.config.EntryNodes = ','.join(relay_list)
-                __make_asset_list__(options['relays'], self.relays['all'])
-            if options['socks']:
-                self.socks_port = options['socks']
-            if options['control']:
-                self.control_port = options['control']
-            if options['random']:
-                log.msg("Using randomized ControlPort and SocksPort ...")
-                self.socks_port   = randint(1024, 2**16)
-                self.control_port = randint(1024, 2**16)
-            if options['torpath']:
-                self.tor_binary = options['torpath']
-            if options['datadir']:
-                self.data_directory = parse_data_dir(options['datadir'])
-            if options['transport']:
-                ## ClientTransportPlugin transport exec pathtobinary [options]
-                ## XXX we need a better way to deal with all PTs
-                log.msg("Using ClientTransportPlugin %s" % options['transport'])
-                self.bridges['use_pt'] = True
-                [self.bridges['pt_type'], pt_exec] = \
-                    options['transport'].split(' ', 1)
-
-                if self.bridges['pt_type'] == "obfs2":
-                    self.config.ClientTransportPlugin = \
-                        self.bridges['pt_type'] + " " + pt_exec
-                else:
-                    raise PTNotFoundException
-
-            self.config.SocksPort            = self.socks_port
-            self.config.ControlPort          = self.control_port
-            self.config.CookieAuthentication = 1
-
-    def __load_assets__(self):
-        """
-        Load bridges and/or relays from files given in user options. Bridges
-        should be given in the form IP:ORport. We don't want to load these as
-        assets, because it's inefficient to start a Tor process for each one.
-
-        We cannot use the Asset model, because that model calls
-        self.experiment() with the current Assets, which would be one relay
-        and one bridge, then it gives the defer.Deferred returned from
-        self.experiment() to self.control(), which means that, for each
-        (bridge, relay) pair, experiment gets called again, which instantiates
-        an additional Tor process that attempts to bind to the same
-        ports. Thus, additionally instantiated Tor processes return with
-        RuntimeErrors, which break the final defer.chainDeferred.callback(),
-        sending it into the errback chain.
-        """
-        assets = {}
-        if self.local_options:
-            if self.local_options['bridges']:
-                assets.update({'bridge': 
-                               BridgetAsset(self.local_options['bridges'])})
-            if self.local_options['relays']:
-                assets.update({'relay': 
-                               BridgetAsset(self.local_options['relays'])})
-        return assets
-
-    def experiment(self, args):
-        """    
-        if bridges:
-            1. configure first bridge line
-            2a. configure data_dir, if it doesn't exist
-            2b. write torrc to a tempfile in data_dir
-            3. start tor                              } if any of these
-            4. remove bridges which are public relays } fail, add current
-            5. SIGHUP for each bridge                 } bridge to unreach-
-                                                      } able bridges.
-        if relays:
-            1a. configure the data_dir, if it doesn't exist
-            1b. write torrc to a tempfile in data_dir
-            2. start tor
-            3. remove any of our relays which are already part of current 
-               circuits
-            4a. attach CustomCircuit() to self.state
-            4b. RELAY_EXTEND for each relay } if this fails, add
-                                            } current relay to list
-                                            } of unreachable relays
-            5. 
-        if bridges and relays:
-            1. configure first bridge line
-            2a. configure data_dir if it doesn't exist
-            2b. write torrc to a tempfile in data_dir
-            3. start tor
-            4. remove bridges which are public relays
-            5. remove any of our relays which are already part of current
-               circuits
-            6a. attach CustomCircuit() to self.state
-            6b. for each bridge, build three circuits, with three
-                relays each
-            6c. RELAY_EXTEND for each relay } if this fails, add
-                                            } current relay to list
-                                            } of unreachable relays
-
-        :param args:
-            The :class:`BridgetAsset` line currently being used. Except that it
-            in Bridget it doesn't, so it should be ignored and avoided.
-        """
-        try:
-            from ooni.utils         import process
-            from ooni.utils.onion   import remove_public_relays, start_tor
-            from ooni.utils.onion   import start_tor_filter_nodes
-            from ooni.utils.onion   import setup_fail, setup_done
-            from ooni.utils.onion   import CustomCircuit
-            from ooni.utils.timer   import deferred_timeout, TimeoutError
-            from ooni.lib.txtorcon  import TorConfig, TorState
-        except ImportError:
-            raise TxtorconImportError
-        except TxtorconImportError, tie:
-            log.err(tie)
-            sys.exit()
-
-        def reconfigure_done(state, bridges):
-            """
-            Append :ivar:`bridges['current']` to the list 
-            :ivar:`bridges['up'].
-            """
-            log.msg("Reconfiguring with 'Bridge %s' successful" 
-                    % bridges['current'])
-            bridges['up'].append(bridges['current'])
-            return state
-
-        def reconfigure_fail(state, bridges):
-            """
-            Append :ivar:`bridges['current']` to the list 
-            :ivar:`bridges['down'].
-            """
-            log.msg("Reconfiguring TorConfig with parameters %s failed"
-                    % state)
-            bridges['down'].append(bridges['current'])
-            return state
-
-        @defer.inlineCallbacks
-        def reconfigure_bridge(state, bridges):
-            """
-            Rewrite the Bridge line in our torrc. If use of pluggable
-            transports was specified, rewrite the line as:
-                Bridge <transport_type> <IP>:<ORPort>
-            Otherwise, rewrite in the standard form:
-                Bridge <IP>:<ORPort>
-
-            :param state:
-                A fully bootstrapped instance of 
-                :class:`ooni.lib.txtorcon.TorState`.
-            :param bridges:
-                A dictionary of bridges containing the following keys:
-
-                bridges['remaining'] :: A function returning and int for the
-                                        number of remaining bridges to test.
-                bridges['current']   :: A string containing the <IP>:<ORPort>
-                                        of the current bridge.
-                bridges['use_pt']    :: A boolean, True if we're testing
-                                        bridges with a pluggable transport;
-                                        False otherwise.
-                bridges['pt_type']   :: If :ivar:`bridges['use_pt'] is True,
-                                        this is a string containing the type
-                                        of pluggable transport to test.
-            :return:
-                :param:`state`
-            """
-            log.msg("Current Bridge: %s" % bridges['current'])
-            log.msg("We now have %d bridges remaining to test..." 
-                    % bridges['remaining']())
-            try:
-                if bridges['use_pt'] is False:
-                    controller_response = yield state.protocol.set_conf(
-                        'Bridge', bridges['current'])
-                elif bridges['use_pt'] and bridges['pt_type'] is not None:
-                    controller_reponse = yield state.protocol.set_conf(
-                        'Bridge', bridges['pt_type'] +' '+ bridges['current'])
-                else:
-                    raise PTNotFoundException
-
-                if controller_response == 'OK':
-                    finish = yield reconfigure_done(state, bridges)
-                else:
-                    log.err("SETCONF for %s responded with error:\n %s"
-                            % (bridges['current'], controller_response))
-                    finish = yield reconfigure_fail(state, bridges)
-
-                defer.returnValue(finish)
-
-            except Exception, e:
-                log.err("Reconfiguring torrc with Bridge line %s failed:\n%s"
-                        % (bridges['current'], e))
-                defer.returnValue(None)
-
-        def attacher_extend_circuit(attacher, deferred, router):
-            ## XXX todo write me
-            ## state.attacher.extend_circuit
-            raise NotImplemented
-            #attacher.extend_circuit
-
-        def state_attach(state, path):
-            log.msg("Setting up custom circuit builder...")
-            attacher = CustomCircuit(state)
-            state.set_attacher(attacher, reactor)
-            state.add_circuit_listener(attacher)
-            return state
-
-            ## OLD
-            #for circ in state.circuits.values():
-            #    for relay in circ.path:
-            #        try:
-            #            relay_list.remove(relay)
-            #        except KeyError:
-            #            continue
-            ## XXX how do we attach to circuits with bridges?
-            d = defer.Deferred()
-            attacher.request_circuit_build(d)
-            return d
-
-        def state_attach_fail(state):
-            log.err("Attaching custom circuit builder failed: %s" % state)
-
-        log.msg("Bridget: initiating test ... ")  ## Start the experiment
-
-        ## if we've at least one bridge, and our config has no 'Bridge' line
-        if self.bridges['remaining']() >= 1 \
-                and not 'Bridge' in self.config.config:
-
-            ## configure our first bridge line
-            self.bridges['current'] = self.bridges['all'][0]
-            self.config.Bridge = self.bridges['current']
-                                                  ## avoid starting several
-            self.config.save()                    ## processes 
-            assert self.config.config.has_key('Bridge'), "No Bridge Line"
-
-            ## start tor and remove bridges which are public relays
-            from ooni.utils.onion import start_tor_filter_nodes
-            state = start_tor_filter_nodes(reactor, self.config, 
-                                           self.control_port, self.tor_binary,
-                                           self.data_directory, self.bridges)
-            #controller = defer.Deferred()
-            #controller.addCallback(singleton_semaphore, tor)
-            #controller.addErrback(setup_fail)
-            #bootstrap = defer.gatherResults([controller, filter_bridges], 
-            #                                consumeErrors=True)
-
-            if state is not None:
-                log.debug("state:\n%s" % state)
-                log.debug("Current callbacks on TorState():\n%s" 
-                          % state.callbacks)
-
-        ## if we've got more bridges
-        if self.bridges['remaining']() >= 2:
-            #all = []
-            for bridge in self.bridges['all'][1:]:
-                self.bridges['current'] = bridge
-                #new = defer.Deferred()
-                #new.addCallback(reconfigure_bridge, state, self.bridges)
-                #all.append(new)
-            #check_remaining = defer.DeferredList(all, consumeErrors=True)
-            #state.chainDeferred(check_remaining)
-                state.addCallback(reconfigure_bridge, self.bridges)
-
-        if self.relays['remaining']() > 0:
-            while self.relays['remaining']() >= 3:
-                #path = list(self.relays.pop() for i in range(3))
-                #log.msg("Trying path %s" % '->'.join(map(lambda node: 
-                #                                         node, path)))
-                self.relays['current'] = self.relays['all'].pop()
-                for circ in state.circuits.values():
-                    for node in circ.path:
-                        if node == self.relays['current']:
-                            self.relays['up'].append(self.relays['current'])
-                    if len(circ.path) < 3:
-                        try:
-                            ext = attacher_extend_circuit(state.attacher, circ,
-                                                          self.relays['current'])
-                            ext.addCallback(attacher_extend_circuit_done, 
-                                            state.attacher, circ, 
-                                            self.relays['current'])
-                        except Exception, e:
-                            log.err("Extend circuit failed: %s" % e)
-                    else:
-                        continue
-
-        #state.callback(all)
-        #self.reactor.run()
-        return state
-
-    def startTest(self, args):
-        """
-        Local override of :meth:`OONITest.startTest` to bypass calling 
-        self.control.
-
-        :param args:
-            The current line of :class:`Asset`, not used but kept for 
-            compatibility reasons.
-        :return:
-            A fired deferred which callbacks :meth:`experiment` and 
-            :meth:`OONITest.finished`. 
-        """
-        self.start_time = date.now()
-        self.d = self.experiment(args)
-        self.d.addErrback(log.err)
-        self.d.addCallbacks(self.finished, log.err)
-        return self.d
-
-## So that getPlugins() can register the Test:
-#bridget = BridgetTest(None, None, None)
-
-
-## ISIS' NOTES
-## -----------
-## TODO:
-##       x  cleanup documentation
-##       x  add DataDirectory option
-##       x  check if bridges are public relays
-##       o  take bridge_desc file as input, also be able to give same
-##          format as output
-##       x  Add asynchronous timeout for deferred, so that we don't wait 
-##       o  Add assychronous timout for deferred, so that we don't wait
-##          forever for bridges that don't work.





More information about the tor-commits mailing list