txtorcon-0.14.2/0000755000175000017500000000000012627745451013367 5ustar mikemike00000000000000txtorcon-0.14.2/scripts/0000755000175000017500000000000012627745451015056 5ustar mikemike00000000000000txtorcon-0.14.2/scripts/asciinema-demo0.py0000755000175000017500000000700712444716153020364 0ustar mikemike00000000000000#!/usr/bin/env python # this is a hack-tacular script to pass to asciinema like: # asciinema -c ./asciinema-demo0.py rec # to script the show. as it were. import os import sys import time import random import colors import subprocess prompt = 'user@machine:~/src/txtorcon$ ' def interkey_interval(): "in milliseconds" # return 0 return (random.lognormvariate(0.0, 0.5) * 30.0) / 1000.0 return (float(random.randrange(10,50)) / 1000.0) def type_it_out(line): for c in line: sys.stdout.write(c) sys.stdout.flush() time.sleep(interkey_interval()) def do_commands(lines): for line in lines: sys.stdout.write(colors.blue(prompt)) type_it_out(line) time.sleep(0.5) print os.system(colors.strip_color(line)) commands = [] commands.append(colors.bold('export TMPDIR=/dev/shm')) commands.append(colors.red('# txtorcon + endpoints demo')) commands.append(colors.red('# we already checked out the code here')) commands.append(colors.red('# first, prepare a virtualenv')) commands.append(colors.bold('virtualenv txtorcon_demo')) commands.append('txtorcon_demo/bin/' + colors.bold(colors.white('pip install -r requirements.txt'))) commands.append(colors.red('# pick up txtorcon from our local Git checkout')) commands.append('export PYTHONPATH=`pwd`') commands.append(colors.red('# prepare example web content')) commands.append('mkdir -p example_website') commands.append('echo "hello, hidden-service world" > example_website/index.html') commands.append('rm -f twistd.log') commands.append('txtorcon_demo/bin/' + colors.bold('twistd web ') + colors.white('--port onion:80') + colors.bold(' --path example_website/')) commands.append(colors.red('# wait until Tor launches etc')) commands.append('tail twistd.log') commands.append(colors.red('# wait for a particular log message to appear')) commands.append('while ! grep "Started hidden service" twistd.log ; do sleep 1; done;') commands.append(colors.red('# save our new hidden service\'s keys')) commands.append('tail twistd.log') commands.append('mkdir hidserv_keys') commands.append(r'cp `grep "Keys are in " twistd.log | cut -d \" -f 2`/* hidserv_keys') commands.append('ls hidserv_keys') commands.append('cat hidserv_keys/hostname') commands.append(colors.red("# now we've got a copy of the private key")) commands.append('tail twistd.log') commands.append(colors.red('# there we go, a new hidden-serivce.')) commands.append(colors.red('# now, what if we kill it and want to re-launch with the same key/hostname?')) commands.append('kill `cat twistd.pid`') commands.append('rm twistd.log') commands.append('txtorcon_demo/bin/' + colors.bold('twistd web ') + colors.white('--port onion:80' + colors.bold(':hiddenServiceDir=hidserv_keys')) + colors.bold(' --path example_website/')) commands.append('# ^^^^^^^^^^^^^^^^') commands.append('sleep 5') commands.append(colors.red('# remember, if anyone gets hold of private_key ' + colors.bold(colors.green(colors.underline('they can BECOME your hidden-service'))))) commands.append(colors.red('# just as if you\'d completely lost control of your DNS entries on "normal" internet')) commands.append('while ! grep "Started hidden service" twistd.log ; do sleep 1; done;') commands.append('tail twistd.log') commands.append(colors.bold('cat hidserv_keys/hostname')) commands.append(colors.green('# thanks for watching!')) commands.append(colors.bold(colors.white('# https://github.com/meejah/txtorcon'))) if __name__ == '__main__': do_commands(commands) txtorcon-0.14.2/twisted/0000755000175000017500000000000012627745451015052 5ustar mikemike00000000000000txtorcon-0.14.2/twisted/plugins/0000755000175000017500000000000012627745451016533 5ustar mikemike00000000000000txtorcon-0.14.2/twisted/plugins/txtorcon_endpoint_parser.py0000644000175000017500000000024112605144171024222 0ustar mikemike00000000000000import txtorcon tcpHiddenServiceEndpointParser = txtorcon.TCPHiddenServiceEndpointParser() tcpTorClientEndpointParser = txtorcon.TorClientEndpointStringParser() txtorcon-0.14.2/dev-requirements.txt0000644000175000017500000000017112627744056017426 0ustar mikemike00000000000000coverage setuptools>=0.8.0 Sphinx repoze.sphinx.autointerface>=0.4 coveralls wheel twine pyflakes pep8 mock ipaddr GeoIP txtorcon-0.14.2/PKG-INFO0000644000175000017500000002424312627745451014471 0ustar mikemike00000000000000Metadata-Version: 1.1 Name: txtorcon Version: 0.14.2 Summary: Twisted-based Tor controller client, with state-tracking and configuration abstractions. Home-page: https://github.com/meejah/txtorcon Author: meejah Author-email: meejah@meejah.ca License: MIT Description: README ====== Documentation at https://txtorcon.readthedocs.org .. image:: https://travis-ci.org/meejah/txtorcon.png?branch=master :target: https://www.travis-ci.org/meejah/txtorcon .. image:: https://coveralls.io/repos/meejah/txtorcon/badge.png :target: https://coveralls.io/r/meejah/txtorcon .. image:: http://api.flattr.com/button/flattr-badge-large.png :target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub quick start ----------- For the impatient, there are two quick ways to install this:: $ pip install txtorcon ... or, if you checked out or downloaded the source:: $ python setup.py install ... or, better yet, use a virtualenv and the dev requirements:: $ virtualenv venv $ ./venv/bin/pip install -e .[dev] For OSX, we can install txtorcon with the help of easy_install:: $ easy_install txtorcon To avoid installing, you can just add the base of the source to your PYTHONPATH:: $ export PYTHONPATH=`pwd`:$PYTHONPATH Then, you will want to explore the examples. Try "python examples/stream\_circuit\_logger.py" for instance. On Debian testing (jessie), or with wheezy-backports (big thanks to Lunar^ for all his packaging work) you can install easily:: $ apt-get install python-txtorcon You may also like `this asciinema demo `_ for an overview. Tor configuration ----------------- You'll want to have the following options on in your ``torrc``:: CookieAuthentication 1 CookieAuthFileGroupReadable 1 If you want to use unix sockets to speak to tor:: ControlSocketsGroupWritable 1 ControlSocket /var/run/tor/control The defaults used by py:meth:`txtorcon.build_local_tor_connection` will find a Tor on ``9051`` or ``/var/run/tor/control`` overview -------- txtorcon is a Twisted-based asynchronous Tor control protocol implementation. Twisted is an event-driven networking engine written in Python and Tor is an onion-routing network designed to improve people's privacy and anonymity on the Internet. The main abstraction of this library is txtorcon.TorControlProtocol which presents an asynchronous API to speak the Tor client protocol in Python. txtorcon also provides abstractions to track and get updates about Tor's state (txtorcon.TorState) and current configuration (including writing it to Tor or disk) in txtorcon.TorConfig, along with helpers to asynchronously launch slave instances of Tor including Twisted endpoint support. txtorcon runs all tests cleanly on: - Debian "squeeze", "wheezy" and "jessie" - OS X 10.4 (naif) - OS X 10.8 (lukas lueg) - OS X 10.9 (kurt neufeld) - Fedora 18 (lukas lueg) - FreeBSD 10 (enrique fynn) (**needed to install "lsof"**) - RHEL6 - Reports from other OSes appreciated. If instead you want a synchronous (threaded) Python controller library, check out Stem at https://stem.torproject.org/ quick implementation overview ----------------------------- txtorcon provides a class to track Tor's current state -- such as details about routers, circuits and streams -- called txtorcon.TorState and an abstraction to the configuration values via txtorcon.TorConfig which provides attribute-style accessors to Tor's state (including making changes). txtorcon.TorState provides txtorcon.Router, txtorcon.Circuit and txtorcon.Stream objects which implement a listener interface so client code may receive updates (in real time) including Tor events. txtorcon uses **trial for unit-tests** and has 100% test-coverage -- which is not to say I've covered all the cases, but nearly all of the code is at least exercised somehow by the unit tests. Tor itself is not required to be running for any of the tests. ohcount claims around 2000 lines of code for the core bit; around 4000 including tests. About 37% comments in the not-test code. There are a few simple integration tests, based on Docker. More are always welcome! dependencies / requirements --------------------------- - `twisted `_: txtorcon should work with any Twisted 11.1.0 or newer. Twisted 15.4.0+ works with Python3, and so does txtorcon (if you find something broken on Py3 please file a bug). - `GeoIP `_: **optional** provides location information for ip addresses; you will want to download GeoLite City from `MaxMind `_ or pay them for more accuracy. Or use tor-geoip, which makes this sort-of optional, in that we'll query Tor for the IP if the GeoIP database doesn't have an answer. It also does ASN lookups if you installed that MaxMind database. - `python-ipaddr `_: **optional**. Google's IP address manipulation code. - development: `Sphinx `_ if you want to build the documentation. In that case you'll also need something called ``python-repoze.sphinx.autointerface`` (at least in Debian) to build the Interface-derived docs properly. - development: `coverage `_ to run the code-coverage metrics, and Tox - optional: GraphViz is used in the tests (and to generate state-machine diagrams, if you like) but those tests are skipped if "dot" isn't in your path .. BEGIN_INSTALL In any case, on a `Debian `_ wheezy, squeeze or Ubuntu system, this should work:: apt-get install -y python-setuptools python-twisted python-ipaddr python-geoip graphviz tor apt-get install -y python-sphinx python-repoze.sphinx.autointerface python-coverage # for development .. END_INSTALL Using pip this would be:: pip install Twisted ipaddr pygeoip pip install GeoIP Sphinx repoze.sphinx.autointerface coverage # for development or:: pip install -r requirements.txt pip install -r dev-requirements.txt or for the bare minimum:: pip install Twisted # will install zope.interface too documentation ------------- It is likely that you will need to read at least some of `control-spec.txt `_ from the torspec git repository so you know what's being abstracted by this library. Run "make doc" to build the Sphinx documentation locally, or rely on ReadTheDocs https://txtorcon.readthedocs.org which builds each tagged release and the latest master. There is also a directory of examples/ scripts, which have inline documentation explaining their use. contact information ------------------- For novelty value, the Web site (with built documentation and so forth) can be viewed via Tor at http://timaq4ygg2iegci7.onion although the code itself is hosted via git:: torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git or:: git clone git://github.com/meejah/txtorcon.git You may contact me via ``meejah at meejah dot ca`` with GPG key `0xC2602803128069A7 `_ or see ``meejah.asc`` in the repository. The fingerprint is ``9D5A 2BD5 688E CB88 9DEB CD3F C260 2803 1280 69A7``. It is often possible to contact me as ``meejah`` in #tor-dev on `OFTC `_ but be patient for replies (I do look at scrollback, so putting "meejah: " in front will alert my client). More conventionally, you may get the code at GitHub and documentation via ReadTheDocs: - https://github.com/meejah/txtorcon - https://txtorcon.readthedocs.org Please do **use the GitHub issue-tracker** to report bugs. Patches, pull-requests, comments and criticisms are all welcomed and appreciated. Keywords: python,twisted,tor,tor controller Platform: UNKNOWN Classifier: Framework :: Twisted Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Internet :: Proxy Servers Classifier: Topic :: Internet Classifier: Topic :: Security txtorcon-0.14.2/setup.cfg0000644000175000017500000000007312627745451015210 0ustar mikemike00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 txtorcon-0.14.2/test/0000755000175000017500000000000012627745451014346 5ustar mikemike00000000000000txtorcon-0.14.2/test/test_circuit.py0000644000175000017500000003567212613266556017435 0ustar mikemike00000000000000import datetime import time from twisted.trial import unittest from twisted.internet import defer from twisted.python.failure import Failure from zope.interface import implements from txtorcon import Circuit from txtorcon import Stream from txtorcon import TorControlProtocol from txtorcon import TorState from txtorcon import Router from txtorcon.interface import IRouterContainer from txtorcon.interface import ICircuitListener from txtorcon.interface import ICircuitContainer from txtorcon.interface import CircuitListenerMixin from txtorcon.interface import ITorControlProtocol class FakeTorController(object): implements(IRouterContainer, ICircuitListener, ICircuitContainer, ITorControlProtocol) post_bootstrap = defer.Deferred() def __init__(self): self.routers = {} self.circuits = {} self.extend = [] self.failed = [] def router_from_id(self, i): return self.routers[i[:41]] def circuit_new(self, circuit): self.circuits[circuit.id] = circuit def circuit_extend(self, circuit, router): self.extend.append((circuit, router)) def circuit_launched(self, circuit): pass def circuit_built(self, circuit): pass def circuit_closed(self, circuit, **kw): if circuit.id in self.circuits: del self.circuits[circuit.id] def circuit_failed(self, circuit, **kw): self.failed.append((circuit, kw)) if circuit.id in self.circuits: del self.circuits[circuit.id] def find_circuit(self, circid): return self.circuits[circid] def close_circuit(self, circid): del self.circuits[circid] return defer.succeed('') class FakeLocation: def __init__(self): self.countrycode = 'NA' class FakeRouter: def __init__(self, hsh, nm): self.name = nm self.hash = hsh self.location = FakeLocation() examples = ['CIRC 365 LAUNCHED PURPOSE=GENERAL', 'CIRC 365 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris PURPOSE=GENERAL', 'CIRC 365 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus PURPOSE=GENERAL', 'CIRC 365 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL', 'CIRC 365 BUILT $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL', 'CIRC 365 CLOSED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL REASON=FINISHED', 'CIRC 365 FAILED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL REASON=TIMEOUT'] class CircuitTests(unittest.TestCase): def test_age(self): """ make sure age does something sensible at least once. """ tor = FakeTorController() circuit = Circuit(tor) now = datetime.datetime.now() update = '1 LAUNCHED PURPOSE=GENERAL TIME_CREATED=%s' % time.strftime('%Y-%m-%dT%H:%M:%S') circuit.update(update.split()) diff = circuit.age(now=now) self.assertEquals(diff, 0) self.assertTrue(circuit.time_created is not None) def test_no_age_yet(self): """ make sure age doesn't explode if there's no TIME_CREATED flag. """ tor = FakeTorController() circuit = Circuit(tor) now = datetime.datetime.now() circuit.update('1 LAUNCHED PURPOSE=GENERAL'.split()) self.assertTrue(circuit.time_created is None) diff = circuit.age(now=now) self.assertEquals(diff, None) def test_listener_mixin(self): listener = CircuitListenerMixin() from zope.interface.verify import verifyObject self.assertTrue(verifyObject(ICircuitListener, listener)) # call all the methods with None for each arg. This is mostly # just to gratuitously increase test coverage, but also # serves to ensure these methods don't just blow up for (methodname, desc) in ICircuitListener.namesAndDescriptions(): method = getattr(listener, methodname) args = [None] * len(desc.positional) method(*args) def test_unlisten(self): tor = FakeTorController() tor.routers['$E11D2B2269CC25E67CA6C9FB5843497539A74FD0'] = FakeRouter( '$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', 'a' ) circuit = Circuit(tor) circuit.listen(tor) circuit.listen(tor) circuit.update('1 LAUNCHED PURPOSE=GENERAL'.split()) circuit.unlisten(tor) circuit.update('1 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris PURPOSE=GENERAL'.split()) self.assertEqual(len(tor.circuits), 1) self.assertTrue(1 in tor.circuits) self.assertEqual(len(tor.extend), 0) self.assertEqual(1, len(circuit.path)) self.assertEqual(0, len(circuit.listeners)) def test_path_update(self): cp = TorControlProtocol() state = TorState(cp, False) circuit = Circuit(state) circuit.update('1 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris PURPOSE=GENERAL'.split()) self.assertEqual(1, len(circuit.path)) self.assertEqual( '$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', circuit.path[0].id_hex ) self.assertEqual('eris', circuit.path[0].name) def test_wrong_update(self): tor = FakeTorController() circuit = Circuit(tor) circuit.listen(tor) circuit.update('1 LAUNCHED PURPOSE=GENERAL'.split()) self.assertRaises( Exception, circuit.update, '2 LAUNCHED PURPOSE=GENERAL'.split() ) def test_closed_remaining_streams(self): tor = FakeTorController() circuit = Circuit(tor) circuit.listen(tor) circuit.update('1 LAUNCHED PURPOSE=GENERAL'.split()) stream = Stream(tor) stream.update("1 NEW 0 94.23.164.42.$43ED8310EB968746970896E8835C2F1991E50B69.exit:9001 SOURCE_ADDR=(Tor_internal):0 PURPOSE=DIR_FETCH".split()) circuit.streams.append(stream) self.assertEqual(len(circuit.streams), 1) circuit.update('1 CLOSED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL REASON=FINISHED'.split()) circuit.update('1 FAILED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL REASON=TIMEOUT'.split()) errs = self.flushLoggedErrors() self.assertEqual(len(errs), 2) def test_updates(self): tor = FakeTorController() circuit = Circuit(tor) circuit.listen(tor) tor.routers['$E11D2B2269CC25E67CA6C9FB5843497539A74FD0'] = FakeRouter( '$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', 'a' ) tor.routers['$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5'] = FakeRouter( '$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5', 'b' ) tor.routers['$253DFF1838A2B7782BE7735F74E50090D46CA1BC'] = FakeRouter( '$253DFF1838A2B7782BE7735F74E50090D46CA1BC', 'c' ) for ex in examples[:-1]: circuit.update(ex.split()[1:]) self.assertEqual(circuit.state, ex.split()[2]) self.assertEqual(circuit.purpose, 'GENERAL') if '$' in ex: self.assertEqual( len(circuit.path), len(ex.split()[3].split(',')) ) for (r, p) in zip(ex.split()[3].split(','), circuit.path): d = r.split('=')[0] self.assertEqual(d, p.hash) def test_extend_messages(self): tor = FakeTorController() a = FakeRouter('$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', 'a') b = FakeRouter('$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5', 'b') c = FakeRouter('$253DFF1838A2B7782BE7735F74E50090D46CA1BC', 'c') tor.routers['$E11D2B2269CC25E67CA6C9FB5843497539A74FD0'] = a tor.routers['$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5'] = b tor.routers['$253DFF1838A2B7782BE7735F74E50090D46CA1BC'] = c circuit = Circuit(tor) circuit.listen(tor) circuit.update('365 LAUNCHED PURPOSE=GENERAL'.split()) self.assertEqual(tor.extend, []) circuit.update('365 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris PURPOSE=GENERAL'.split()) self.assertEqual(len(tor.extend), 1) self.assertEqual(tor.extend[0], (circuit, a)) circuit.update('365 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus PURPOSE=GENERAL'.split()) self.assertEqual(len(tor.extend), 2) self.assertEqual(tor.extend[0], (circuit, a)) self.assertEqual(tor.extend[1], (circuit, b)) circuit.update('365 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL'.split()) self.assertEqual(len(tor.extend), 3) self.assertEqual(tor.extend[0], (circuit, a)) self.assertEqual(tor.extend[1], (circuit, b)) self.assertEqual(tor.extend[2], (circuit, c)) def test_extends_no_path(self): ''' without connectivity, it seems you get EXTENDS messages with no path update. ''' tor = FakeTorController() circuit = Circuit(tor) circuit.listen(tor) circuit.update('753 EXTENDED BUILD_FLAGS=IS_INTERNAL,NEED_CAPACITY,NEED_UPTIME PURPOSE=MEASURE_TIMEOUT TIME_CREATED=2012-07-30T18:23:18.956704'.split()) self.assertEqual(tor.extend, []) self.assertEqual(circuit.path, []) self.assertTrue('IS_INTERNAL' in circuit.build_flags) self.assertTrue('NEED_CAPACITY' in circuit.build_flags) self.assertTrue('NEED_UPTIME' in circuit.build_flags) def test_str(self): tor = FakeTorController() circuit = Circuit(tor) circuit.id = 1 str(circuit) router = Router(tor) circuit.path.append(router) str(circuit) def test_failed_reason(self): tor = FakeTorController() circuit = Circuit(tor) circuit.listen(tor) circuit.update('1 FAILED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris PURPOSE=GENERAL REASON=TIMEOUT'.split()) self.assertEqual(len(tor.failed), 1) circ, kw = tor.failed[0] self.assertEqual(circ, circuit) self.assertTrue('PURPOSE' in kw) self.assertTrue('REASON' in kw) self.assertEqual(kw['PURPOSE'], 'GENERAL') self.assertEqual(kw['REASON'], 'TIMEOUT') def test_close_circuit(self): tor = FakeTorController() a = FakeRouter('$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', 'a') b = FakeRouter('$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5', 'b') c = FakeRouter('$253DFF1838A2B7782BE7735F74E50090D46CA1BC', 'c') tor.routers['$E11D2B2269CC25E67CA6C9FB5843497539A74FD0'] = a tor.routers['$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5'] = b tor.routers['$253DFF1838A2B7782BE7735F74E50090D46CA1BC'] = c circuit = Circuit(tor) circuit.listen(tor) circuit.update('123 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL'.split()) self.assertEqual(3, len(circuit.path)) d = circuit.close() # we already pretended that Tor answered "OK" to the # CLOSECIRCUIT call (see close_circuit() in FakeTorController # above) however the circuit isn't "really" closed yet... self.assertTrue(not d.called) # not unit-test-y? shouldn't probably delve into internals I # suppose... self.assertTrue(circuit._closing_deferred is not None) # simulate that Tor has really closed the circuit for us # this should cause our Deferred to callback circuit.update('123 CLOSED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL REASON=FINISHED'.split()) # confirm that our circuit callback has been triggered already self.assertRaises( defer.AlreadyCalledError, d.callback, "should have been called already" ) return d def test_is_built(self): tor = FakeTorController() a = FakeRouter('$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', 'a') b = FakeRouter('$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5', 'b') c = FakeRouter('$253DFF1838A2B7782BE7735F74E50090D46CA1BC', 'c') tor.routers['$E11D2B2269CC25E67CA6C9FB5843497539A74FD0'] = a tor.routers['$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5'] = b tor.routers['$253DFF1838A2B7782BE7735F74E50090D46CA1BC'] = c circuit = Circuit(tor) circuit.listen(tor) circuit.update('123 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL'.split()) built0 = circuit.is_built built1 = circuit.when_built() self.assertTrue(built0 is not built1) self.assertFalse(built0.called) self.assertFalse(built1.called) circuit.update('123 BUILT $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL'.split()) # create callback when we're alread in BUILT; should be # callback'd already built2 = circuit.when_built() self.assertTrue(built2 is not built1) self.assertTrue(built2 is not built0) self.assertTrue(built0.called) self.assertTrue(built1.called) self.assertTrue(built2.called) self.assertTrue(built0.result == circuit) self.assertTrue(built1.result == circuit) self.assertTrue(built2.result == circuit) def test_is_built_errback(self): tor = FakeTorController() a = FakeRouter('$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', 'a') tor.routers['$E11D2B2269CC25E67CA6C9FB5843497539A74FD0'] = a state = TorState(tor) circuit = Circuit(tor) circuit.listen(tor) circuit.update('123 EXTENDED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris PURPOSE=GENERAL'.split()) state.circuit_new(circuit) d = circuit.when_built() state.circuit_closed(circuit) self.assertTrue(d.called) self.assertTrue(isinstance(d.result, Failure)) txtorcon-0.14.2/test/test_util.py0000644000175000017500000002027112611263616016725 0ustar mikemike00000000000000from mock import patch from twisted.trial import unittest from twisted.internet import defer from twisted.internet.endpoints import TCP4ServerEndpoint from twisted.internet.interfaces import IProtocolFactory from zope.interface import implements from txtorcon.util import process_from_address from txtorcon.util import delete_file_or_tree from txtorcon.util import find_keywords from txtorcon.util import ip_from_int from txtorcon.util import find_tor_binary from txtorcon.util import maybe_ip_addr import os import tempfile class FakeState: tor_pid = 0 class FakeProtocolFactory: implements(IProtocolFactory) def doStart(self): "IProtocolFactory API" def doStop(self): "IProtocolFactory API" def buildProtocol(self, addr): "IProtocolFactory API" return None class TestIPFromInt(unittest.TestCase): def test_cast(self): self.assertEqual(ip_from_int(0x7f000001), '127.0.0.1') class TestGeoIpDatabaseLoading(unittest.TestCase): def test_bad_geoip_path(self): "fail gracefully if a db is missing" from txtorcon import util self.assertRaises(IOError, util.create_geoip, '_missing_path_') def test_missing_geoip_module(self): "return none if geoip module is missing" from txtorcon import util _GeoIP = util.GeoIP util.GeoIP = None (fd, f) = tempfile.mkstemp() ret_val = util.create_geoip(f) delete_file_or_tree(f) util.GeoIP = _GeoIP self.assertEquals(ret_val, None) def test_return_geoip_object(self): from txtorcon import util (fd, f) = tempfile.mkstemp() ret_val = util.create_geoip(f) delete_file_or_tree(f) self.assertEquals(type(ret_val).__name__, 'GeoIP') class TestFindKeywords(unittest.TestCase): def test_filter(self): "make sure we filter out keys that look like router IDs" self.assertEqual( find_keywords("foo=bar $1234567890=routername baz=quux".split()), {'foo': 'bar', 'baz': 'quux'} ) class FakeGeoIP(object): def __init__(self, version=2): self.version = version def record_by_addr(self, ip): r = dict(country_code='XX', latitude=50.0, longitude=0.0, city='City') if self.version == 2: r['region_code'] = 'Region' else: r['region_name'] = 'Region' return r class TestNetLocation(unittest.TestCase): def test_valid_lookup_v2(self): from txtorcon import util orig = util.city try: util.city = FakeGeoIP(version=2) nl = util.NetLocation('127.0.0.1') self.assertTrue(nl.city) self.assertEquals(nl.city[0], 'City') self.assertEquals(nl.city[1], 'Region') finally: util.ity = orig def test_valid_lookup_v3(self): from txtorcon import util orig = util.city try: util.city = FakeGeoIP(version=3) nl = util.NetLocation('127.0.0.1') self.assertTrue(nl.city) self.assertEquals(nl.city[0], 'City') self.assertEquals(nl.city[1], 'Region') finally: util.ity = orig def test_city_fails(self): "make sure we don't fail if the city lookup excepts" from txtorcon import util orig = util.city try: class Thrower(object): def record_by_addr(*args, **kw): raise RuntimeError("testing failure") util.city = Thrower() nl = util.NetLocation('127.0.0.1') self.assertEqual(None, nl.city) finally: util.city = orig def test_no_city_db(self): "ensure we lookup from country if we have no city" from txtorcon import util origcity = util.city origcountry = util.country try: util.city = None obj = object() class CountryCoder(object): def country_code_by_addr(self, ipaddr): return obj util.country = CountryCoder() nl = util.NetLocation('127.0.0.1') self.assertEqual(obj, nl.countrycode) finally: util.city = origcity util.country = origcountry def test_no_city_or_country_db(self): "ensure we lookup from asn if we have no city or country" from txtorcon import util origcity = util.city origcountry = util.country origasn = util.asn try: util.city = None util.country = None class Thrower: def org_by_addr(*args, **kw): raise RuntimeError("testing failure") util.asn = Thrower() nl = util.NetLocation('127.0.0.1') self.assertEqual('', nl.countrycode) finally: util.city = origcity util.country = origcountry util.asn = origasn class TestProcessFromUtil(unittest.TestCase): def setUp(self): self.fakestate = FakeState() def test_none(self): "ensure we do something useful on a None address" self.assertEqual(process_from_address(None, 80, self.fakestate), None) def test_internal(self): "look up the (Tor_internal) PID" pfa = process_from_address('(Tor_internal)', 80, self.fakestate) # depends on whether you have psutil installed or not, and on # whether your system always has a PID 0 process... self.assertEqual(pfa, self.fakestate.tor_pid) def test_internal_no_state(self): "look up the (Tor_internal) PID" pfa = process_from_address('(Tor_internal)', 80) # depends on whether you have psutil installed or not, and on # whether your system always has a PID 0 process... self.assertEqual(pfa, None) @defer.inlineCallbacks def test_real_addr(self): # FIXME should choose a port which definitely isn't used. # it's apparently frowned upon to use the "real" reactor in # tests, but I was using "nc" before, and I think this is # preferable. from twisted.internet import reactor ep = TCP4ServerEndpoint(reactor, 9887) listener = yield ep.listen(FakeProtocolFactory()) try: pid = process_from_address('0.0.0.0', 9887, self.fakestate) finally: listener.stopListening() self.assertEqual(pid, os.getpid()) class TestDelete(unittest.TestCase): def test_delete_file(self): (fd, f) = tempfile.mkstemp() os.write(fd, 'some\ndata\n') os.close(fd) self.assertTrue(os.path.exists(f)) delete_file_or_tree(f) self.assertTrue(not os.path.exists(f)) def test_delete_tree(self): d = tempfile.mkdtemp() f = open(os.path.join(d, 'foo'), 'w') f.write('foo\n') f.close() self.assertTrue(os.path.exists(d)) self.assertTrue(os.path.isdir(d)) self.assertTrue(os.path.exists(os.path.join(d, 'foo'))) delete_file_or_tree(d) self.assertTrue(not os.path.exists(d)) self.assertTrue(not os.path.exists(os.path.join(d, 'foo'))) class TestFindTor(unittest.TestCase): def test_simple_find_tor(self): # just test that this doesn't raise an exception find_tor_binary() def test_find_tor_globs(self): "test searching by globs" find_tor_binary(system_tor=False) def test_find_tor_unfound(self): "test searching by globs" self.assertEqual(None, find_tor_binary(system_tor=False, globs=())) @patch('txtorcon.util.subprocess.Popen') def test_find_ioerror(self, popen): "test searching with which, but it fails" popen.side_effect = OSError self.assertEqual(None, find_tor_binary(system_tor=True, globs=())) class TestIpAddr(unittest.TestCase): @patch('txtorcon.util.ipaddr') def test_create_ipaddr(self, ipaddr): ip = maybe_ip_addr('1.2.3.4') @patch('txtorcon.util.ipaddr') def test_create_ipaddr(self, ipaddr): def foo(blam): raise ValueError('testing') ipaddr.IPAddress.side_effect = foo ip = maybe_ip_addr('1.2.3.4') txtorcon-0.14.2/test/test_torstate.py0000644000175000017500000012406012611263616017616 0ustar mikemike00000000000000from zope.interface import implements from zope.interface.verify import verifyClass from twisted.trial import unittest from twisted.test import proto_helpers from twisted.python.failure import Failure from twisted.internet import task, defer from twisted.internet.interfaces import IStreamClientEndpoint, IReactorCore import os import tempfile from mock import patch from txtorcon import TorControlProtocol from txtorcon import TorProtocolError from txtorcon import TorState from txtorcon import Stream from txtorcon import Circuit from txtorcon import build_tor_connection from txtorcon import build_local_tor_connection from txtorcon.interface import ITorControlProtocol from txtorcon.interface import IStreamAttacher from txtorcon.interface import ICircuitListener from txtorcon.interface import IStreamListener from txtorcon.interface import StreamListenerMixin from txtorcon.interface import CircuitListenerMixin class CircuitListener(object): implements(ICircuitListener) def __init__(self, expected): "expect is a list of tuples: (event, {key:value, key1:value1, ..})" self.expected = expected def checker(self, state, circuit, arg=None): if self.expected[0][0] != state: raise RuntimeError( 'Expected event "%s" not "%s".' % (self.expected[0][0], state) ) for (k, v) in self.expected[0][1].items(): if k == 'arg': if v != arg: raise RuntimeError( 'Expected argument to have value "%s", not "%s"' % (arg, v) ) elif getattr(circuit, k) != v: raise RuntimeError( 'Expected attribute "%s" to have value "%s", not "%s"' % (k, v, getattr(circuit, k)) ) self.expected = self.expected[1:] def circuit_new(self, circuit): self.checker('new', circuit) def circuit_launched(self, circuit): self.checker('launched', circuit) def circuit_extend(self, circuit, router): self.checker('extend', circuit, router) def circuit_built(self, circuit): self.checker('built', circuit) def circuit_closed(self, circuit, **kw): self.checker('closed', circuit, **kw) def circuit_failed(self, circuit, **kw): self.checker('failed', circuit, **kw) class StreamListener(object): implements(IStreamListener) def __init__(self, expected): "expect is a list of tuples: (event, {key:value, key1:value1, ..})" self.expected = expected def checker(self, state, stream, arg=None): if self.expected[0][0] != state: raise RuntimeError( 'Expected event "%s" not "%s".' % (self.expected[0][0], state) ) for (k, v) in self.expected[0][1].items(): if k == 'arg': if v != arg: raise RuntimeError( 'Expected argument to have value "%s", not "%s"' % (arg, v) ) elif getattr(stream, k) != v: raise RuntimeError( 'Expected attribute "%s" to have value "%s", not "%s"' % (k, v, getattr(stream, k)) ) self.expected = self.expected[1:] def stream_new(self, stream): self.checker('new', stream) def stream_succeeded(self, stream): self.checker('succeeded', stream) def stream_attach(self, stream, circuit): self.checker('attach', stream, circuit) def stream_closed(self, stream): self.checker('closed', stream) def stream_failed(self, stream, reason, remote_reason): self.checker('failed', stream, reason) class FakeReactor: implements(IReactorCore) def __init__(self, test): self.test = test def addSystemEventTrigger(self, *args): self.test.assertEqual(args[0], 'before') self.test.assertEqual(args[1], 'shutdown') self.test.assertEqual(args[2], self.test.state.undo_attacher) return 1 def removeSystemEventTrigger(self, id): self.test.assertEqual(id, 1) def connectTCP(self, *args, **kw): """for testing build_tor_connection""" raise RuntimeError('connectTCP: ' + str(args)) def connectUNIX(self, *args, **kw): """for testing build_tor_connection""" raise RuntimeError('connectUNIX: ' + str(args)) class FakeCircuit(Circuit): def __init__(self, id=-999): self.streams = [] self.id = id self.state = 'BOGUS' class FakeEndpoint: implements(IStreamClientEndpoint) def get_info_raw(self, keys): ans = '\r\n'.join(map(lambda k: '%s=' % k, keys.split())) return defer.succeed(ans) def get_info_incremental(self, key, linecb): linecb('%s=' % key) return defer.succeed('') def connect(self, protocol_factory): self.proto = TorControlProtocol() self.proto.transport = proto_helpers.StringTransport() self.proto.get_info_raw = self.get_info_raw self.proto.get_info_incremental = self.get_info_incremental self.proto._set_valid_events( 'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL' ) return defer.succeed(self.proto) class FakeEndpointAnswers: implements(IStreamClientEndpoint) def __init__(self, answers): self.answers = answers # since we use pop() we need these to be "backwards" self.answers.reverse() def get_info_raw(self, keys): ans = '' for k in keys.split(): if len(self.answers) == 0: raise TorProtocolError(551, "ran out of answers") ans += '%s=%s\r\n' % (k, self.answers.pop()) return ans[:-2] # don't want trailing \r\n def get_info_incremental(self, key, linecb): linecb('%s=%s' % (key, self.answers.pop())) return defer.succeed('') def connect(self, protocol_factory): self.proto = TorControlProtocol() self.proto.transport = proto_helpers.StringTransport() self.proto.get_info_raw = self.get_info_raw self.proto.get_info_incremental = self.get_info_incremental self.proto._set_valid_events( 'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL' ) return defer.succeed(self.proto) class BootstrapTests(unittest.TestCase): def confirm_proto(self, x): self.assertTrue(isinstance(x, TorControlProtocol)) self.assertTrue(x.post_bootstrap.called) def confirm_state(self, x): self.assertTrue(isinstance(x, TorState)) self.assertTrue(x.post_bootstrap.called) return x def test_build(self): p = FakeEndpoint() d = build_tor_connection(p, build_state=False) d.addCallback(self.confirm_proto) p.proto.post_bootstrap.callback(p.proto) return d def test_build_tcp(self): d = build_tor_connection((FakeReactor(self), '127.0.0.1', 1234)) d.addCallback(self.fail) d.addErrback(lambda x: None) return d def test_build_unix(self): tf = tempfile.NamedTemporaryFile() d = build_tor_connection((FakeReactor(self), tf.name)) d.addCallback(self.fail) d.addErrback(lambda x: None) return d def test_build_unix_wrong_permissions(self): self.assertRaises( ValueError, build_tor_connection, (FakeReactor(self), 'a non-existant filename') ) def test_build_wrong_size_tuple(self): self.assertRaises(TypeError, build_tor_connection, (1, 2, 3, 4)) def test_build_wrong_args_entirely(self): self.assertRaises( TypeError, build_tor_connection, 'incorrect argument' ) def confirm_pid(self, state): self.assertEqual(state.tor_pid, 1234) def confirm_no_pid(self, state): self.assertEqual(state.tor_pid, 0) def test_build_with_answers(self): p = FakeEndpointAnswers(['', # ns/all '', # circuit-status '', # stream-status '', # address-mappings/all '', # entry-guards '1234' # PID ]) d = build_tor_connection(p, build_state=True) d.addCallback(self.confirm_state).addErrback(self.fail) d.addCallback(self.confirm_pid).addErrback(self.fail) p.proto.post_bootstrap.callback(p.proto) return d def test_build_with_answers_no_pid(self): p = FakeEndpointAnswers(['', # ns/all '', # circuit-status '', # stream-status '', # address-mappings/all '' # entry-guards ]) d = build_tor_connection(p, build_state=True) d.addCallback(self.confirm_state) d.addCallback(self.confirm_no_pid) p.proto.post_bootstrap.callback(p.proto) return d def test_build_with_answers_guards_unfound_entry(self): p = FakeEndpointAnswers(['', # ns/all '', # circuit-status '', # stream-status '', # address-mappings/all '\n\nkerblam up\nOK\n' # entry-guards ]) d = build_tor_connection(p, build_state=True) d.addCallback(self.confirm_state) d.addCallback(self.confirm_no_pid) p.proto.post_bootstrap.callback(p.proto) return d def test_build_local_unix(self): reactor = FakeReactor(self) d = build_local_tor_connection(reactor) d.addErrback(lambda _: None) return d def test_build_local_tcp(self): reactor = FakeReactor(self) d = build_local_tor_connection(reactor, socket=None) d.addErrback(lambda _: None) return d class StateTests(unittest.TestCase): def setUp(self): self.protocol = TorControlProtocol() self.state = TorState(self.protocol) # avoid spew in trial logs; state prints this by default self.state._attacher_error = lambda f: f self.protocol.connectionMade = lambda: None self.transport = proto_helpers.StringTransport() self.protocol.makeConnection(self.transport) def test_close_stream_with_attacher(self): class MyAttacher(object): implements(IStreamAttacher) def __init__(self): self.streams = [] def attach_stream(self, stream, circuits): self.streams.append(stream) return None attacher = MyAttacher() self.state.set_attacher(attacher, FakeReactor(self)) self.state._stream_update("76 CLOSED 0 www.example.com:0 REASON=DONE") def test_attacher_error_handler(self): # make sure error-handling "does something" that isn't blowing up with patch('sys.stdout') as fake_stdout: TorState(self.protocol)._attacher_error(Failure(RuntimeError("quote"))) def test_stream_update(self): # we use a circuit ID of 0 so it doesn't try to look anything # up but it's not really correct to have a SUCCEEDED w/o a # valid circuit, I don't think self.state._stream_update('1610 SUCCEEDED 0 74.125.224.243:80') self.assertTrue(1610 in self.state.streams) def test_single_streams(self): self.state.circuits[496] = FakeCircuit(496) self.state._stream_status( 'stream-status=123 SUCCEEDED 496 www.example.com:6667' ) self.assertEqual(len(self.state.streams), 1) def test_multiple_streams(self): self.state.circuits[496] = FakeCircuit(496) self.state._stream_status( '\r\n'.join([ 'stream-status=', '123 SUCCEEDED 496 www.example.com:6667', '124 SUCCEEDED 496 www.example.com:6667', ]) ) self.assertEqual(len(self.state.streams), 2) def send(self, line): self.protocol.dataReceived(line.strip() + "\r\n") @defer.inlineCallbacks def test_bootstrap_callback(self): ''' FIXME: something is still screwy with this; try throwing an exception from TorState.bootstrap and we'll just hang... ''' from test_torconfig import FakeControlProtocol protocol = FakeControlProtocol( [ "ns/all=", # ns/all "", # circuit-status "", # stream-status "", # address-mappings/all "entry-guards=\r\n$0000000000000000000000000000000000000000=name up\r\n$1111111111111111111111111111111111111111=foo up\r\n$9999999999999999999999999999999999999999=eman unusable 2012-01-01 22:00:00\r\n", # entry-guards "99999", # process/pid "??", # ip-to-country/0.0.0.0 ] ) state = yield TorState.from_protocol(protocol) self.assertEqual(len(state.entry_guards), 2) self.assertTrue('$0000000000000000000000000000000000000000' in state.entry_guards) self.assertTrue('$1111111111111111111111111111111111111111' in state.entry_guards) self.assertEqual(len(state.unusable_entry_guards), 1) self.assertTrue('$9999999999999999999999999999999999999999' in state.unusable_entry_guards[0]) def test_bootstrap_existing_addresses(self): ''' FIXME: something is still screwy with this; try throwing an exception from TorState.bootstrap and we'll just hang... ''' d = self.state.post_bootstrap clock = task.Clock() self.state.addrmap.scheduler = clock self.protocol._set_valid_events(' '.join(self.state.event_map.keys())) self.state._bootstrap() self.send("250+ns/all=") self.send(".") self.send("250 OK") self.send("250+circuit-status=") self.send(".") self.send("250 OK") self.send("250-stream-status=") self.send("250 OK") self.send("250+address-mappings/all=") self.send('www.example.com 127.0.0.1 "2012-01-01 00:00:00"') self.send('subdomain.example.com 10.0.0.0 "2012-01-01 00:01:02"') self.send('.') self.send('250 OK') for ignored in self.state.event_map.items(): self.send("250 OK") self.send("250-entry-guards=") self.send("250 OK") self.send("250 OK") self.assertEqual(len(self.state.addrmap.addr), 2) self.assertTrue('www.example.com' in self.state.addrmap.addr) self.assertTrue('subdomain.example.com' in self.state.addrmap.addr) return d def test_bootstrap_single_existing_circuit(self): ''' test with exactly one circuit. should probably test with 2 as well, since there was a bug with the handling of just one. ''' d = self.state.post_bootstrap clock = task.Clock() self.state.addrmap.scheduler = clock self.protocol._set_valid_events(' '.join(self.state.event_map.keys())) self.state._bootstrap() self.send("250+ns/all=") self.send(".") self.send("250 OK") self.send("250-circuit-status=123 BUILT PURPOSE=GENERAL") self.send("250 OK") self.send("250-stream-status=") self.send("250 OK") self.send("250+address-mappings/all=") self.send('.') self.send('250 OK') for ignored in self.state.event_map.items(): self.send("250 OK") self.send("250-entry-guards=") self.send("250 OK") self.send("250 OK") self.assertTrue(self.state.find_circuit(123)) self.assertEquals(len(self.state.circuits), 1) return d def test_unset_attacher(self): class MyAttacher(object): implements(IStreamAttacher) def attach_stream(self, stream, circuits): return None fr = FakeReactor(self) self.state.set_attacher(MyAttacher(), fr) self.send("250 OK") self.state.set_attacher(None, fr) self.send("250 OK") self.assertEqual( self.transport.value(), 'SETCONF __LeaveStreamsUnattached=1\r\nSETCONF' ' __LeaveStreamsUnattached=0\r\n' ) def test_attacher(self): class MyAttacher(object): implements(IStreamAttacher) def __init__(self): self.streams = [] self.answer = None def attach_stream(self, stream, circuits): self.streams.append(stream) return self.answer attacher = MyAttacher() self.state.set_attacher(attacher, FakeReactor(self)) events = 'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL' self.protocol._set_valid_events(events) self.state._add_events() for ignored in self.state.event_map.items(): self.send("250 OK") self.send("650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER") self.send("650 STREAM 1 REMAP 0 87.248.112.181:80 SOURCE=CACHE") self.assertEqual(len(attacher.streams), 1) self.assertEqual(attacher.streams[0].id, 1) self.assertEqual(len(self.protocol.commands), 1) self.assertEqual(self.protocol.commands[0][1], 'ATTACHSTREAM 1 0') # we should totally ignore .exit URIs attacher.streams = [] self.send("650 STREAM 2 NEW 0 10.0.0.0.$E11D2B2269CC25E67CA6C9FB5843497539A74FD0.exit:80 SOURCE_ADDR=127.0.0.1:12345 PURPOSE=TIME") self.assertEqual(len(attacher.streams), 0) self.assertEqual(len(self.protocol.commands), 1) # we should NOT ignore .onion URIs attacher.streams = [] self.send("650 STREAM 3 NEW 0 xxxxxxxxxxxxxxxx.onion:80 SOURCE_ADDR=127.0.0.1:12345 PURPOSE=TIME") self.assertEqual(len(attacher.streams), 1) self.assertEqual(len(self.protocol.commands), 2) self.assertEqual(self.protocol.commands[1][1], 'ATTACHSTREAM 3 0') # normal attach circ = FakeCircuit(1) circ.state = 'BUILT' self.state.circuits[1] = circ attacher.answer = circ self.send("650 STREAM 4 NEW 0 xxxxxxxxxxxxxxxx.onion:80 SOURCE_ADDR=127.0.0.1:12345 PURPOSE=TIME") self.assertEqual(len(attacher.streams), 2) self.assertEqual(len(self.protocol.commands), 3) self.assertEqual(self.protocol.commands[2][1], 'ATTACHSTREAM 4 1') def test_attacher_defer(self): class MyAttacher(object): implements(IStreamAttacher) def __init__(self, answer): self.streams = [] self.answer = answer def attach_stream(self, stream, circuits): self.streams.append(stream) return defer.succeed(self.answer) self.state.circuits[1] = FakeCircuit(1) self.state.circuits[1].state = 'BUILT' attacher = MyAttacher(self.state.circuits[1]) self.state.set_attacher(attacher, FakeReactor(self)) # boilerplate to finish enough set-up in the protocol so it # works events = 'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL' self.protocol._set_valid_events(events) self.state._add_events() for ignored in self.state.event_map.items(): self.send("250 OK") self.send("650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER") self.send("650 STREAM 1 REMAP 0 87.248.112.181:80 SOURCE=CACHE") self.assertEqual(len(attacher.streams), 1) self.assertEqual(attacher.streams[0].id, 1) self.assertEqual(len(self.protocol.commands), 1) self.assertEqual(self.protocol.commands[0][1], 'ATTACHSTREAM 1 1') @defer.inlineCallbacks def test_attacher_errors(self): class MyAttacher(object): implements(IStreamAttacher) def __init__(self, answer): self.streams = [] self.answer = answer def attach_stream(self, stream, circuits): return self.answer self.state.circuits[1] = FakeCircuit(1) attacher = MyAttacher(FakeCircuit(2)) self.state.set_attacher(attacher, FakeReactor(self)) stream = Stream(self.state) stream.id = 3 msg = '' try: yield self.state._maybe_attach(stream) except Exception, e: msg = str(e) self.assertTrue('circuit unknown' in msg) attacher.answer = self.state.circuits[1] msg = '' try: yield self.state._maybe_attach(stream) except Exception, e: msg = str(e) self.assertTrue('only attach to BUILT' in msg) attacher.answer = 'not a Circuit instance' msg = '' try: yield self.state._maybe_attach(stream) except Exception, e: msg = str(e) self.assertTrue('Circuit instance' in msg) def test_attacher_no_attach(self): class MyAttacher(object): implements(IStreamAttacher) def __init__(self): self.streams = [] def attach_stream(self, stream, circuits): self.streams.append(stream) return TorState.DO_NOT_ATTACH attacher = MyAttacher() self.state.set_attacher(attacher, FakeReactor(self)) events = 'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL' self.protocol._set_valid_events(events) self.state._add_events() for ignored in self.state.event_map.items(): self.send("250 OK") self.transport.clear() self.send("650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER") self.send("650 STREAM 1 REMAP 0 87.248.112.181:80 SOURCE=CACHE") self.assertEqual(len(attacher.streams), 1) self.assertEqual(attacher.streams[0].id, 1) print self.transport.value() self.assertEqual(self.transport.value(), '') def test_close_stream_with_id(self): stream = Stream(self.state) stream.id = 1 self.state.streams[1] = stream self.state.close_stream(stream) self.assertEqual(self.transport.value(), 'CLOSESTREAM 1 1\r\n') def test_close_stream_with_stream(self): stream = Stream(self.state) stream.id = 1 self.state.streams[1] = stream self.state.close_stream(stream.id) self.assertEqual(self.transport.value(), 'CLOSESTREAM 1 1\r\n') def test_close_stream_invalid_reason(self): stream = Stream(self.state) stream.id = 1 self.state.streams[1] = stream self.assertRaises( ValueError, self.state.close_stream, stream, 'FOO_INVALID_REASON' ) def test_close_circuit_with_id(self): circuit = Circuit(self.state) circuit.id = 1 self.state.circuits[1] = circuit self.state.close_circuit(circuit.id) self.assertEqual(self.transport.value(), 'CLOSECIRCUIT 1\r\n') def test_close_circuit_with_circuit(self): circuit = Circuit(self.state) circuit.id = 1 self.state.circuits[1] = circuit self.state.close_circuit(circuit) self.assertEqual(self.transport.value(), 'CLOSECIRCUIT 1\r\n') def test_close_circuit_with_flags(self): circuit = Circuit(self.state) circuit.id = 1 # try: # self.state.close_circuit(circuit.id, IfUnused=True) # self.assertTrue(False) # except KeyError: # pass self.state.circuits[1] = circuit self.state.close_circuit(circuit.id, IfUnused=True) self.assertEqual(self.transport.value(), 'CLOSECIRCUIT 1 IfUnused\r\n') def test_circuit_destroy(self): self.state._circuit_update('365 LAUNCHED PURPOSE=GENERAL') self.assertTrue(365 in self.state.circuits) self.state._circuit_update('365 FAILED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL REASON=TIMEOUT') self.assertTrue(365 not in self.state.circuits) def test_circuit_destroy_already(self): self.state._circuit_update('365 LAUNCHED PURPOSE=GENERAL') self.assertTrue(365 in self.state.circuits) self.state._circuit_update('365 CLOSED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL REASON=TIMEOUT') self.assertTrue(365 not in self.state.circuits) self.state._circuit_update('365 CLOSED $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL REASON=TIMEOUT') self.assertTrue(365 not in self.state.circuits) def test_circuit_listener(self): events = 'CIRC STREAM ORCONN BW DEBUG INFO NOTICE WARN ERR NEWDESC ADDRMAP AUTHDIR_NEWDESCS DESCCHANGED NS STATUS_GENERAL STATUS_CLIENT STATUS_SERVER GUARD STREAM_BW CLIENTS_SEEN NEWCONSENSUS BUILDTIMEOUT_SET' self.protocol._set_valid_events(events) self.state._add_events() for ignored in self.state.event_map.items(): self.send("250 OK") # we use this router later on in an EXTEND self.state._update_network_status("""ns/all= r PPrivCom012 2CGDscCeHXeV/y1xFrq1EGqj5g4 QX7NVLwx7pwCuk6s8sxB4rdaCKI 2011-12-20 08:34:19 84.19.178.6 9001 0 s Fast Guard Running Stable Unnamed Valid w Bandwidth=51500 p reject 1-65535""") expected = [('new', {'id': 456}), ('launched', {}), ('extend', {'id': 123}) ] listen = CircuitListener(expected) # first add a Circuit before we listen self.protocol.dataReceived("650 CIRC 123 LAUNCHED PURPOSE=GENERAL\r\n") self.assertEqual(len(self.state.circuits), 1) # make sure we get added to existing circuits self.state.add_circuit_listener(listen) self.assertTrue(listen in self.state.circuits.values()[0].listeners) # now add a Circuit after we started listening self.protocol.dataReceived("650 CIRC 456 LAUNCHED PURPOSE=GENERAL\r\n") self.assertEqual(len(self.state.circuits), 2) self.assertTrue(listen in self.state.circuits.values()[0].listeners) self.assertTrue(listen in self.state.circuits.values()[1].listeners) # now update the first Circuit to ensure we're really, really # listening self.protocol.dataReceived("650 CIRC 123 EXTENDED $D82183B1C09E1D7795FF2D7116BAB5106AA3E60E~PPrivCom012 PURPOSE=GENERAL\r\n") self.assertEqual(len(listen.expected), 0) def test_router_from_id_invalid_key(self): self.failUnlessRaises(KeyError, self.state.router_from_id, 'somethingcompletelydifferent..thatis42long') def test_router_from_named_router(self): r = self.state.router_from_id('$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=foo') self.assertEqual(r.id_hex, '$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') self.assertEqual(r.unique_name, 'foo') def confirm_router_state(self, x): self.assertTrue('$624926802351575FF7E4E3D60EFA3BFB56E67E8A' in self.state.routers) router = self.state.routers['$624926802351575FF7E4E3D60EFA3BFB56E67E8A'] self.assertTrue('exit' in router.flags) self.assertTrue('fast' in router.flags) self.assertTrue('guard' in router.flags) self.assertTrue('hsdir' in router.flags) self.assertTrue('named' in router.flags) self.assertTrue('running' in router.flags) self.assertTrue('stable' in router.flags) self.assertTrue('v2dir' in router.flags) self.assertTrue('valid' in router.flags) self.assertTrue('futureproof' in router.flags) self.assertEqual(router.bandwidth, 518000) self.assertTrue(router.accepts_port(43)) self.assertTrue(router.accepts_port(53)) self.assertTrue(not router.accepts_port(44)) self.assertTrue(router.accepts_port(989)) self.assertTrue(router.accepts_port(990)) self.assertTrue(not router.accepts_port(991)) self.assertTrue(not router.accepts_port(988)) def test_router_with_ipv6_address(self): self.state._update_network_status("""ns/all= r PPrivCom012 2CGDscCeHXeV/y1xFrq1EGqj5g4 QX7NVLwx7pwCuk6s8sxB4rdaCKI 2011-12-20 08:34:19 84.19.178.6 9001 0 a [2001:0:0:0::0]:4321 s Fast Guard Running Stable Named Valid w Bandwidth=51500 p reject 1-65535""") self.assertEqual(len(self.state.routers_by_name['PPrivCom012'][0].ip_v6), 1) self.assertEqual(self.state.routers_by_name['PPrivCom012'][0].ip_v6[0], '[2001:0:0:0::0]:4321') def test_invalid_routers(self): try: self.state._update_network_status('''ns/all= r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80 r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80 s Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof w Bandwidth=518000 p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888 .''') self.fail() except RuntimeError, e: self.assertTrue('"s "' in str(e)) def test_routers_no_policy(self): """ ensure we can parse a router descriptor which has no p line """ self.state._update_network_status('''ns/all= r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80 s Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof w Bandwidth=518000 r PPrivCom012 2CGDscCeHXeV/y1xFrq1EGqj5g4 QX7NVLwx7pwCuk6s8sxB4rdaCKI 2011-12-20 08:34:19 84.19.178.6 9001 0 s Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof w Bandwidth=518000 p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888 .''') self.assertTrue('fake' in self.state.routers.keys()) self.assertTrue('PPrivCom012' in self.state.routers.keys()) def test_routers_no_bandwidth(self): """ ensure we can parse a router descriptor which has no w line """ self.state._update_network_status('''ns/all= r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80 s Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof r PPrivCom012 2CGDscCeHXeV/y1xFrq1EGqj5g4 QX7NVLwx7pwCuk6s8sxB4rdaCKI 2011-12-20 08:34:19 84.19.178.6 9001 0 s Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof w Bandwidth=518000 p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888 .''') self.assertTrue('fake' in self.state.routers.keys()) self.assertTrue('PPrivCom012' in self.state.routers.keys()) def test_router_factory(self): self.state._update_network_status('''ns/all= r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80 s Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof w Bandwidth=518000 p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888 r fake YxxmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80 s Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof w Bandwidth=543000 p accept 43,53 .''') self.assertTrue('$624926802351575FF7E4E3D60EFA3BFB56E67E8A' in self.state.routers) r = self.state.routers['$624926802351575FF7E4E3D60EFA3BFB56E67E8A'] self.assertEqual(r.controller, self.state.protocol) self.assertEqual(r.bandwidth, 518000) self.assertEqual(len(self.state.routers_by_name['fake']), 2) # now we do an update self.state._update_network_status('''ns/all= r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80 s Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof Authority w Bandwidth=543000 p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888 .''') self.assertEqual(r.bandwidth, 543000) def test_empty_stream_update(self): self.state._stream_update('''stream-status=''') def test_addrmap(self): self.state._addr_map('example.com 127.0.0.1 "2012-01-01 00:00:00" EXPIRES=NEVER') def test_double_newconsensus(self): """ The arrival of a second NEWCONSENSUS event causes parsing errors. """ # bootstrap the TorState so we can send it a "real" 650 # update self.protocol._set_valid_events(' '.join(self.state.event_map.keys())) self.state._bootstrap() self.send("250+ns/all=") self.send(".") self.send("250 OK") self.send("250+circuit-status=") self.send(".") self.send("250 OK") self.send("250-stream-status=") self.send("250 OK") self.send("250-address-mappings/all=") self.send('250 OK') for ignored in self.state.event_map.items(): self.send("250 OK") self.send("250-entry-guards=") self.send("250 OK") self.send("250 OK") # state is now bootstrapped, we can send our NEWCONSENSUS update self.protocol.dataReceived('\r\n'.join('''650+NEWCONSENSUS r Unnamed ABJlguUFz1lvQS0jq8nhTdRiXEk /zIVUg1tKMUeyUBoyimzorbQN9E 2012-05-23 01:10:22 219.94.255.254 9001 0 s Fast Guard Running Stable Valid w Bandwidth=166 p reject 1-65535 . 650 OK '''.split('\n'))) self.protocol.dataReceived('\r\n'.join('''650+NEWCONSENSUS r Unnamed ABJlguUFz1lvQS0jq8nhTdRiXEk /zIVUg1tKMUeyUBoyimzorbQN9E 2012-05-23 01:10:22 219.94.255.254 9001 0 s Fast Guard Running Stable Valid w Bandwidth=166 p reject 1-65535 . 650 OK '''.split('\n'))) self.assertTrue('Unnamed' in self.state.routers) self.assertTrue('$00126582E505CF596F412D23ABC9E14DD4625C49' in self.state.routers) def test_NEWCONSENSUS_ends_with_OK_on_w(self): """ The arrival of a second NEWCONSENSUS event causes parsing errors. """ # bootstrap the TorState so we can send it a "real" 650 # update self.protocol._set_valid_events(' '.join(self.state.event_map.keys())) self.state._bootstrap() self.send("250+ns/all=") self.send(".") self.send("250 OK") self.send("250+circuit-status=") self.send(".") self.send("250 OK") self.send("250-stream-status=") self.send("250 OK") self.send("250-address-mappings/all=") self.send('250 OK') for ignored in self.state.event_map.items(): self.send("250 OK") self.send("250-entry-guards=") self.send("250 OK") self.send("250 OK") # state is now bootstrapped, we can send our NEWCONSENSUS update self.protocol.dataReceived('\r\n'.join('''650+NEWCONSENSUS r Unnamed ABJlguUFz1lvQS0jq8nhTdRiXEk /zIVUg1tKMUeyUBoyimzorbQN9E 2012-05-23 01:10:22 219.94.255.254 9001 0 s Fast Guard Running Stable Valid w Bandwidth=166 . 650 OK '''.split('\n'))) self.assertTrue('Unnamed' in self.state.routers) self.assertTrue('$00126582E505CF596F412D23ABC9E14DD4625C49' in self.state.routers) def test_NEWCONSENSUS_ends_with_OK_on_s(self): """ The arrival of a second NEWCONSENSUS event causes parsing errors. """ # bootstrap the TorState so we can send it a "real" 650 # update self.protocol._set_valid_events(' '.join(self.state.event_map.keys())) self.state._bootstrap() self.send("250+ns/all=") self.send(".") self.send("250 OK") self.send("250+circuit-status=") self.send(".") self.send("250 OK") self.send("250-stream-status=") self.send("250 OK") self.send("250-address-mappings/all=") self.send('250 OK') for ignored in self.state.event_map.items(): self.send("250 OK") self.send("250-entry-guards=") self.send("250 OK") self.send("250 OK") # state is now bootstrapped, we can send our NEWCONSENSUS update self.protocol.dataReceived('\r\n'.join('''650+NEWCONSENSUS r Unnamed ABJlguUFz1lvQS0jq8nhTdRiXEk /zIVUg1tKMUeyUBoyimzorbQN9E 2012-05-23 01:10:22 219.94.255.254 9001 0 s Fast Guard Running Stable Valid . 650 OK '''.split('\n'))) self.assertTrue('Unnamed' in self.state.routers) self.assertTrue('$00126582E505CF596F412D23ABC9E14DD4625C49' in self.state.routers) def test_stream_create(self): self.state._stream_update('1610 NEW 0 1.2.3.4:56') self.assertTrue(1610 in self.state.streams) def test_stream_destroy(self): self.state._stream_update('1610 NEW 0 1.2.3.4:56') self.assertTrue(1610 in self.state.streams) self.state._stream_update("1610 FAILED 0 www.example.com:0 REASON=DONE REMOTE_REASON=FAILED") self.assertTrue(1610 not in self.state.streams) def test_stream_detach(self): circ = FakeCircuit(1) circ.state = 'BUILT' self.state.circuits[1] = circ self.state._stream_update('1610 NEW 0 1.2.3.4:56') self.assertTrue(1610 in self.state.streams) self.state._stream_update("1610 SUCCEEDED 1 4.3.2.1:80") self.assertEqual(self.state.streams[1610].circuit, circ) self.state._stream_update("1610 DETACHED 0 www.example.com:0 REASON=DONE REMOTE_REASON=FAILED") self.assertEqual(self.state.streams[1610].circuit, None) def test_stream_listener(self): self.protocol._set_valid_events('CIRC STREAM ORCONN BW DEBUG INFO NOTICE WARN ERR NEWDESC ADDRMAP AUTHDIR_NEWDESCS DESCCHANGED NS STATUS_GENERAL STATUS_CLIENT STATUS_SERVER GUARD STREAM_BW CLIENTS_SEEN NEWCONSENSUS BUILDTIMEOUT_SET') self.state._add_events() for ignored in self.state.event_map.items(): self.send("250 OK") expected = [('new', {}), ] listen = StreamListener(expected) self.send("650 STREAM 77 NEW 0 www.yahoo.cn:80 SOURCE_ADDR=127.0.0.1:54315 PURPOSE=USER") self.state.add_stream_listener(listen) self.assertTrue(listen in self.state.streams.values()[0].listeners) self.assertEqual(len(self.state.streams), 1) self.assertEqual(len(listen.expected), 1) self.send("650 STREAM 78 NEW 0 www.yahoo.cn:80 SOURCE_ADDR=127.0.0.1:54315 PURPOSE=USER") self.assertEqual(len(self.state.streams), 2) self.assertEqual(len(listen.expected), 0) def test_build_circuit(self): class FakeRouter: def __init__(self, i): self.id_hex = i self.flags = [] path = [] for x in range(3): path.append(FakeRouter("$%040d" % x)) # can't just check flags for guard status, need to know if # it's in the running Tor's notion of Entry Guards path[0].flags = ['guard'] self.state.build_circuit(path, using_guards=True) self.assertEqual(self.transport.value(), 'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002\r\n') # should have gotten a warning about this not being an entry # guard self.assertEqual(len(self.flushWarnings()), 1) def test_build_circuit_no_routers(self): self.state.build_circuit() self.assertEqual(self.transport.value(), 'EXTENDCIRCUIT 0\r\n') def test_build_circuit_unfound_router(self): self.state.build_circuit(routers=['AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'], using_guards=False) self.assertEqual(self.transport.value(), 'EXTENDCIRCUIT 0 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n') def circuit_callback(self, circ): self.assertTrue(isinstance(circ, Circuit)) self.assertEqual(circ.id, 1234) def test_build_circuit_final_callback(self): class FakeRouter: def __init__(self, i): self.id_hex = i self.flags = [] path = [] for x in range(3): path.append(FakeRouter("$%040d" % x)) # can't just check flags for guard status, need to know if # it's in the running Tor's notion of Entry Guards path[0].flags = ['guard'] # FIXME TODO we should verify we get a circuit_new event for # this circuit d = self.state.build_circuit(path, using_guards=True) d.addCallback(self.circuit_callback) self.assertEqual(self.transport.value(), 'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002\r\n') self.send('250 EXTENDED 1234') # should have gotten a warning about this not being an entry # guard self.assertEqual(len(self.flushWarnings()), 1) return d def test_build_circuit_error(self): """ tests that we check the callback properly """ try: self.state._find_circuit_after_extend("FOO 1234") self.assertTrue(False) except RuntimeError, e: self.assertTrue('Expected EXTENDED' in str(e)) def test_listener_mixins(self): self.assertTrue(verifyClass(IStreamListener, StreamListenerMixin)) self.assertTrue(verifyClass(ICircuitListener, CircuitListenerMixin)) txtorcon-0.14.2/test/util.py0000644000175000017500000000066312572755166015705 0ustar mikemike00000000000000 import tempfile import shutil class TempDir(object): ''' This is a simple context manager that handles creating and cleaning up a tempdir. See also: https://gist.github.com/meejah/6430613 ''' def __enter__(self, *args): self.dir_name = tempfile.mkdtemp() return self def __exit__(self, *args): shutil.rmtree(self.dir_name) def __str__(self): return self.dir_name txtorcon-0.14.2/test/test_torinfo.py0000644000175000017500000002243412611263616017433 0ustar mikemike00000000000000from zope.interface import implements from twisted.trial import unittest from twisted.test import proto_helpers from twisted.internet import defer from txtorcon import ITorControlProtocol, TorInfo, TorControlProtocol class FakeControlProtocol: """ """ implements(ITorControlProtocol) def __init__(self, answers): self.answers = answers self.pending = [] self.post_bootstrap = defer.succeed(self) def get_info_raw(self, info): if len(self.answers) == 0: d = defer.Deferred() self.pending.append(d) return d d = defer.succeed(self.answers[0]) self.answers = self.answers[1:] return d get_info = get_info_raw class CheckAnswer: def __init__(self, test, ans): self.answer = ans self.test = test def __call__(self, x): self.test.assertEqual(x, self.answer) class MagicContainerTests(unittest.TestCase): def test_repr(self): from txtorcon.torinfo import MagicContainer m = MagicContainer('foo') self.assertTrue(repr(m) == 'foo') self.assertTrue(str(m) == 'foo') m._setup_complete() self.assertTrue(repr(m) == 'foo') self.assertTrue(str(m) == 'foo') class ProtocolIntegrationTests(unittest.TestCase): """ Tests which use a real TorControlProtocol objects, not a mock. """ def setUp(self): self.protocol = TorControlProtocol(lambda: defer.succeed('foo')) self.transport = proto_helpers.StringTransport() def send(self, line): self.protocol.dataReceived(line.strip() + "\r\n") @defer.inlineCallbacks def test_with_arg(self): info = TorInfo(self.protocol) pb = info.post_bootstrap # now we hook up the protocol like it connected to a real Tor self.protocol.makeConnection(self.transport) # answer all the requests generated by TorControlProtocol # boostrapping etc. self.send('250-AUTH METHODS=PASSWORD') self.send('250 OK') # response to AUTHENTICATE self.send('250 OK') # now we're in _bootstrap() in TorControlProtocol() self.send("250-signal/names=") self.send("250 OK") self.send("250-version=foo") self.send("250 OK") self.send("250-events/names=") self.send("250 OK") self.send("250 OK") # for USEFEATURE # do the TorInfo magic self.send('250-info/names=') self.send('250-multi/path/arg/* a documentation string') self.send('250 OK') # we had to save this up above due to the "interesting" way # TorInfo switches to become a possible-nice magic thingy # that does attribute-access for you. yield pb self.assertTrue(hasattr(info, 'multi')) self.assertTrue(hasattr(getattr(info, 'multi'), 'path')) self.assertTrue( hasattr(getattr(getattr(info, 'multi'), 'path'), 'arg') ) # Finally! The test! We see if we can get this multi-path # value with an argument... # a "real" tor example is "net/listeners/socks" which shows # up in info/names as "net/listeners/*" d = info.multi.path.arg('quux') d.addCallback(CheckAnswer(self, 'foo')) self.send("250-multi/path/arg/quux=foo") self.send("250 OK") yield d class InfoTests(unittest.TestCase): def setUp(self): self.protocol = FakeControlProtocol([]) def test_simple(self): self.protocol.answers.append('''info/names= something a documentation string multi/path a documentation string ''') info = TorInfo(self.protocol) self.assertTrue(hasattr(info, 'something')) self.assertTrue(hasattr(info, 'multi')) self.assertTrue(hasattr(getattr(info, 'multi'), 'path')) self.protocol.answers.append('something=\nfoo') d = info.something() d.addCallback(CheckAnswer(self, 'foo')) return d def test_same_prefix(self): self.protocol.answers.append('''info/names= something/one a documentation string something/two a second documentation string ''') info = TorInfo(self.protocol) self.assertTrue(hasattr(info, 'something')) self.assertTrue(hasattr(info.something, 'one')) self.assertTrue(hasattr(info.something, 'two')) self.protocol.answers.append('something/two=bar') d = info.something.two() d.addCallback(CheckAnswer(self, 'bar')) return d @defer.inlineCallbacks def test_attribute_access(self): ''' test that our post-setup TorInfo pretends to only have attributes that correspond to (valid) GETINFO calls. ''' self.protocol.answers.append('''info/names= something/one a documentation string something/two a second documentation string ''') info = TorInfo(self.protocol) yield self.protocol.post_bootstrap self.assertTrue('something' in dir(info)) self.assertTrue(dir(info.something) == ['one', 'two'] or dir(info.something) == ['two', 'one']) def test_member_access(self): self.protocol.answers.append('info/names blam a thinkg\r\n') info = TorInfo(self.protocol) from txtorcon import torinfo c = torinfo.MagicContainer(None) c._setup = True self.assertEqual([], c.__members__) self.assertEqual(['info'], info.__members__) # make sure __magic__ attr access doesn't throw c.__class__ self.assertRaises(AttributeError, lambda: c.foo_mc_bar_bar) def test_iterator_access(self): ''' confirm we can use the iterator protocol ''' self.protocol.answers.append('''info/names= something/one a documentation string something/two a second documentation string ''') info = TorInfo(self.protocol) self.assertTrue(len(info) == 1) all = [] for x in info: all.append(x) self.assertTrue(len(all) == 1) self.assertTrue(len(info.something) == 2) all = [] for x in info.something: all.append(x) self.assertTrue(len(all) == 2) def test_accessors_not_setup(self): info = TorInfo(self.protocol) self.assertTrue(info.__dict__['_setup'] is False) self.assertRaises(TypeError, len, info) dir(info) try: info[0] self.fail("Should have raised TypeError") except TypeError: pass def handle_error(self, f): if 'Already had something' in f.getErrorMessage(): self.error_happened = True def test_prefix_error(self): self.protocol.answers.append('''info/names= something not allowed I hope something/one a documentation string ''') self.error_happened = False TorInfo(self.protocol, self.handle_error) self.assertTrue(self.error_happened) def test_prefix_error_other_order(self): self.protocol.answers.append('''info/names= other/one a documentation string other not allowed I hope ''') self.error_happened = False TorInfo(self.protocol, self.handle_error) self.assertTrue(self.error_happened) def test_with_arg(self): self.protocol.answers.append('''info/names= multi/path/arg/* a documentation string ''') info = TorInfo(self.protocol) self.assertTrue(hasattr(info, 'multi')) self.assertTrue(hasattr(getattr(info, 'multi'), 'path')) self.assertTrue( hasattr(getattr(getattr(info, 'multi'), 'path'), 'arg') ) # FIXME should have a test that "really" goes out through # TorControlProtocol instance for this stuff... # TorControlProtocol now strips the OK line... self.protocol.answers.append('multi/path/arg/quux=\nbar\nbaz\nquux') try: info.multi.path.arg() self.assertTrue(False) except TypeError: pass d = info.multi.path.arg('quux') d.addCallback(CheckAnswer(self, 'bar\nbaz\nquux')) return d def test_with_arg_error(self): self.protocol.answers.append('''info/names= multi/no-arg docstring ''') info = TorInfo(self.protocol) try: info.multi.no_arg('an argument') self.assertTrue(False) except TypeError: pass def test_dump(self): self.protocol.answers.append('''info/names= multi/path/arg/* a documentation string ''') info = TorInfo(self.protocol) info.dump() def test_config_star_workaround(self): ''' ensure we ignore config/* for now ''' self.protocol.answers.append('''info/names= config/* a documentation string ''') info = TorInfo(self.protocol) self.assertEqual(dir(info), []) def test_other_bootstrap(self): self.protocol.answers.append('''info/names= multi/path/arg/* a documentation string ''') self.protocol.post_bootstrap = None TorInfo(self.protocol) def test_str(self): '''rather silly test to cover string creation''' self.protocol.answers.append('''info/names= version docstring foo/* bar ''') info = TorInfo(self.protocol) # not the end of the world if this fails self.assertTrue(str(info.version) == "version()") self.assertTrue(str(info.foo) == "foo(arg)") txtorcon-0.14.2/test/profile_startup.py0000644000175000017500000000077012523577355020147 0ustar mikemike00000000000000#!/usr/bin/env python from time import time import cProfile import txtorcon proto = txtorcon.TorControlProtocol() state = txtorcon.TorState(proto) data = open('consensus', 'r').read() routers = 4724 # number of routers in above file iters = 100 start = time() if False: cProfile.run('state._update_network_status(data)') else: for x in xrange(iters): state._update_network_status(data) diff = time() - start print "%fs: %f microdescriptors/second" % (diff, (routers * iters) / diff) txtorcon-0.14.2/test/test_log.py0000644000175000017500000000023412515516430016524 0ustar mikemike00000000000000from twisted.trial import unittest from txtorcon import log class LoggingTests(unittest.TestCase): def test_debug(self): log.debug_logging() txtorcon-0.14.2/test/test_endpoints.py0000644000175000017500000005775012627744056020000 0ustar mikemike00000000000000import os import shutil import tempfile from mock import patch from mock import Mock from zope.interface import implements from twisted.trial import unittest from twisted.test import proto_helpers from twisted.internet import defer, error, task, tcp from twisted.internet.endpoints import TCP4ServerEndpoint from twisted.internet.endpoints import serverFromString from twisted.internet.endpoints import clientFromString from twisted.python.failure import Failure from twisted.internet.error import ConnectionRefusedError from twisted.internet.interfaces import IReactorCore from twisted.internet.interfaces import IProtocolFactory from twisted.internet.interfaces import IProtocol from twisted.internet.interfaces import IReactorTCP from twisted.internet.interfaces import IListeningPort from twisted.internet.interfaces import IAddress from txtorcon import TorControlProtocol from txtorcon import ITorControlProtocol from txtorcon import TorConfig from txtorcon import launch_tor from txtorcon import TCPHiddenServiceEndpoint from txtorcon import TorClientEndpoint from txtorcon import TorNotFound from txtorcon import TCPHiddenServiceEndpointParser from txtorcon import IProgressProvider from txtorcon import TorOnionAddress from txtorcon.util import NoOpProtocolFactory from txtorcon.endpoints import get_global_tor # FIXME from txtorcon.endpoints import default_tcp4_endpoint_generator import util connectionRefusedFailure = Failure(ConnectionRefusedError()) class EndpointTests(unittest.TestCase): def setUp(self): from txtorcon import endpoints endpoints._global_tor_config = None del endpoints._global_tor_lock endpoints._global_tor_lock = defer.DeferredLock() self.reactor = FakeReactorTcp(self) self.protocol = FakeControlProtocol([]) self.protocol.event_happened('INFO', 'something craaaaaaazy') self.protocol.event_happened( 'INFO', 'connection_dir_client_reached_eof(): Uploaded rendezvous ' 'descriptor (status 200 ("Service descriptor (v2) stored"))' ) self.config = TorConfig(self.protocol) self.protocol.answers.append( 'config/names=\nHiddenServiceOptions Virtual' ) self.protocol.answers.append('HiddenServiceOptions') self.patcher = patch( 'txtorcon.torconfig.find_tor_binary', return_value='/not/tor' ) self.patcher.start() def tearDown(self): from txtorcon import endpoints endpoints._global_tor_config = None del endpoints._global_tor_lock endpoints._global_tor_lock = defer.DeferredLock() self.patcher.stop() @defer.inlineCallbacks def test_global_tor(self): config = yield get_global_tor( Mock(), _tor_launcher=lambda x, y, z: True ) self.assertEqual(0, config.SOCKSPort) @defer.inlineCallbacks def test_global_tor_error(self): config0 = yield get_global_tor( Mock(), _tor_launcher=lambda x, y, z: True ) # now if we specify a control_port it should be an error since # the above should have launched one. try: config1 = yield get_global_tor(Mock(), control_port=111, _tor_launcher=lambda x, y, z: True) self.fail() except RuntimeError as e: # should be an error pass @defer.inlineCallbacks def test_endpoint_properties(self): ep = yield TCPHiddenServiceEndpoint.private_tor(Mock(), 80) self.assertEqual(None, ep.onion_private_key) self.assertEqual(None, ep.onion_uri) ep.hiddenservice = Mock() ep.hiddenservice.private_key = 'mumble' self.assertEqual('mumble', ep.onion_private_key) @defer.inlineCallbacks def test_private_tor(self): m = Mock() from txtorcon import endpoints endpoints.launch_tor = m ep = yield TCPHiddenServiceEndpoint.private_tor(Mock(), 80, control_port=1234) self.assertTrue(m.called) @defer.inlineCallbacks def test_private_tor_no_control_port(self): m = Mock() from txtorcon import endpoints endpoints.launch_tor = m ep = yield TCPHiddenServiceEndpoint.private_tor(Mock(), 80) self.assertTrue(m.called) @defer.inlineCallbacks def test_system_tor(self): from test_torconfig import FakeControlProtocol def boom(*args): # why does the new_callable thing need a callable that # returns a callable? Feels like I must be doing something # wrong somewhere... def bam(*args, **kw): return self.protocol return bam with patch('txtorcon.endpoints.launch_tor') as launch_mock: with patch('txtorcon.endpoints.build_tor_connection', new_callable=boom) as btc: client = clientFromString( self.reactor, "tcp:host=localhost:port=9050" ) ep = yield TCPHiddenServiceEndpoint.system_tor(self.reactor, client, 80) port = yield ep.listen(NoOpProtocolFactory()) toa = port.getHost() self.assertTrue(hasattr(toa, 'onion_uri')) self.assertTrue(hasattr(toa, 'onion_port')) port.startListening() str(port) port.tor_config # system_tor should be connecting to a running one, # *not* launching a new one. self.assertFalse(launch_mock.called) @defer.inlineCallbacks def test_basic(self): listen = RuntimeError("listen") connect = RuntimeError("connect") reactor = proto_helpers.RaisingMemoryReactor(listen, connect) reactor.addSystemEventTrigger = Mock() ep = TCPHiddenServiceEndpoint(reactor, self.config, 123) self.config.bootstrap() yield self.config.post_bootstrap self.assertTrue(IProgressProvider.providedBy(ep)) try: port = yield ep.listen(NoOpProtocolFactory()) self.fail("Should have been an exception") except RuntimeError as e: # make sure we called listenTCP not connectTCP self.assertEqual(e, listen) repr(self.config.HiddenServices) def test_progress_updates(self): config = TorConfig() ep = TCPHiddenServiceEndpoint(self.reactor, config, 123) self.assertTrue(IProgressProvider.providedBy(ep)) prog = IProgressProvider(ep) ding = Mock() prog.add_progress_listener(ding) args = (50, "blarg", "Doing that thing we talked about.") # kind-of cheating, test-wise? ep._tor_progress_update(*args) self.assertTrue(ding.called_with(*args)) @patch('txtorcon.endpoints.launch_tor') def test_progress_updates_private_tor(self, tor): ep = TCPHiddenServiceEndpoint.private_tor(self.reactor, 1234) tor.call_args[1]['progress_updates'](40, 'FOO', 'foo to the bar') return ep def __test_progress_updates_system_tor(self): ep = TCPHiddenServiceEndpoint.system_tor(self.reactor, 1234) ep._tor_progress_update(40, "FOO", "foo to bar") return ep @patch('txtorcon.endpoints.get_global_tor') def test_progress_updates_global_tor(self, tor): ep = TCPHiddenServiceEndpoint.global_tor(self.reactor, 1234) tor.call_args[1]['progress_updates'](40, 'FOO', 'foo to the bar') return ep def test_hiddenservice_key_unfound(self): ep = TCPHiddenServiceEndpoint.private_tor( self.reactor, 1234, hidden_service_dir='/dev/null' ) # FIXME Mock() should work somehow for this, but I couldn't # make it "go" class Blam(object): @property def private_key(self): raise IOError("blam") ep.hiddenservice = Blam() self.assertEqual(ep.onion_private_key, None) return ep def test_multiple_listen(self): ep = TCPHiddenServiceEndpoint(self.reactor, self.config, 123) d0 = ep.listen(NoOpProtocolFactory()) @defer.inlineCallbacks def more_listen(arg): yield arg.stopListening() d1 = ep.listen(NoOpProtocolFactory()) def foo(arg): return arg d1.addBoth(foo) defer.returnValue(arg) return d0.addBoth(more_listen) self.config.bootstrap() def check(arg): self.assertEqual('127.0.0.1', ep.tcp_endpoint._interface) self.assertEqual(len(self.config.HiddenServices), 1) d0.addCallback(check).addErrback(self.fail) return d0 def test_already_bootstrapped(self): self.config.bootstrap() ep = TCPHiddenServiceEndpoint(self.reactor, self.config, 123) d = ep.listen(NoOpProtocolFactory()) return d @defer.inlineCallbacks def test_explicit_data_dir(self): config = TorConfig(self.protocol) ep = TCPHiddenServiceEndpoint(self.reactor, config, 123, '/dev/null') # make sure listen() correctly configures our hidden-serivce # with the explicit directory we passed in above d = ep.listen(NoOpProtocolFactory()) def foo(fail): print "ERROR", fail d.addErrback(foo) port = yield d self.assertEqual(1, len(config.HiddenServices)) self.assertEqual(config.HiddenServices[0].dir, '/dev/null') def test_failure(self): self.reactor.failures = 1 ep = TCPHiddenServiceEndpoint(self.reactor, self.config, 123) d = ep.listen(NoOpProtocolFactory()) self.config.bootstrap() d.addErrback(self.check_error) return d def check_error(self, failure): self.assertEqual(failure.type, error.CannotListenError) return None def test_parse_via_plugin(self): # make sure we have a valid thing from get_global_tor without # actually launching tor config = TorConfig() config.post_bootstrap = defer.succeed(config) from txtorcon import torconfig torconfig._global_tor_config = None get_global_tor( self.reactor, _tor_launcher=lambda react, config, prog: defer.succeed(config) ) ep = serverFromString( self.reactor, 'onion:88:localPort=1234:hiddenServiceDir=/foo/bar' ) self.assertEqual(ep.public_port, 88) self.assertEqual(ep.local_port, 1234) self.assertEqual(ep.hidden_service_dir, '/foo/bar') def test_parse_user_path(self): # this makes sure we expand users and symlinks in # hiddenServiceDir args. see Issue #77 # make sure we have a valid thing from get_global_tor without # actually launching tor config = TorConfig() config.post_bootstrap = defer.succeed(config) from txtorcon import torconfig torconfig._global_tor_config = None get_global_tor( self.reactor, _tor_launcher=lambda react, config, prog: defer.succeed(config) ) ep = serverFromString( self.reactor, 'onion:88:localPort=1234:hiddenServiceDir=~/blam/blarg' ) # would be nice to have a fixed path here, but then would have # to run as a known user :/ # maybe using the docker stuff to run integration tests better here? self.assertEqual( os.path.expanduser('~/blam/blarg'), ep.hidden_service_dir ) def test_parse_relative_path(self): # this makes sure we convert a relative path to absolute # hiddenServiceDir args. see Issue #77 # make sure we have a valid thing from get_global_tor without # actually launching tor config = TorConfig() config.post_bootstrap = defer.succeed(config) from txtorcon import torconfig torconfig._global_tor_config = None get_global_tor( self.reactor, _tor_launcher=lambda react, config, prog: defer.succeed(config) ) orig = os.path.realpath('.') try: with util.TempDir() as t: t = str(t) os.chdir(t) os.mkdir(os.path.join(t, 'foo')) hsdir = os.path.join(t, 'foo', 'blam') os.mkdir(hsdir) ep = serverFromString( self.reactor, 'onion:88:localPort=1234:hiddenServiceDir=foo/blam' ) self.assertEqual( os.path.realpath(hsdir), ep.hidden_service_dir ) finally: os.chdir(orig) class EndpointLaunchTests(unittest.TestCase): def setUp(self): self.reactor = FakeReactorTcp(self) self.protocol = FakeControlProtocol([]) def test_onion_address(self): addr = TorOnionAddress("foo.onion", 80) # just want to run these and assure they don't throw # exceptions. repr(addr) hash(addr) def test_onion_parse_unix_socket(self): r = Mock() ep = serverFromString(r, "onion:80:controlPort=/tmp/foo") @patch('txtorcon.TCPHiddenServiceEndpoint.system_tor') @patch('txtorcon.TCPHiddenServiceEndpoint.global_tor') @patch('txtorcon.TCPHiddenServiceEndpoint.private_tor') @defer.inlineCallbacks def test_endpoint_launch_tor(self, private_tor, global_tor, system_tor): """ we just want to confirm that calling listen results in the spawning of a Tor process; the parsing/setup from string are checked elsewhere. """ reactor = proto_helpers.MemoryReactor() ep = serverFromString(reactor, 'onion:8888') r = yield ep.listen(NoOpProtocolFactory()) self.assertEqual(global_tor.call_count, 1) self.assertEqual(private_tor.call_count, 0) self.assertEqual(system_tor.call_count, 0) @patch('txtorcon.TCPHiddenServiceEndpoint.system_tor') @patch('txtorcon.TCPHiddenServiceEndpoint.global_tor') @patch('txtorcon.TCPHiddenServiceEndpoint.private_tor') @defer.inlineCallbacks def test_endpoint_connect_tor(self, private_tor, global_tor, system_tor): """ similar to above test, we're confirming that an endpoint-string with 'controlPort=xxxx' in it calls the API that will connect to a running Tor. """ reactor = proto_helpers.MemoryReactor() ep = serverFromString( reactor, 'onion:8888:controlPort=9055:localPort=1234' ) r = yield ep.listen(NoOpProtocolFactory()) self.assertEqual(global_tor.call_count, 0) self.assertEqual(private_tor.call_count, 0) self.assertEqual(system_tor.call_count, 1) # unfortunately, we don't add the hidden-service # configurations until we've connected to the launched Tor # and bootstrapped a TorConfig object -- and that's a ton # of stuff to fake out. Most of that is covered by the # parsing tests (i.e. are we getting the right config # values from a server-endpoint-string) # FIXME should probably go somewhere else, so other tests can easily use these. class FakeProtocol(object): implements(IProtocol) def dataReceived(self, data): print "DATA", data def connectionLost(self, reason): print "LOST", reason def makeConnection(self, transport): print "MAKE", transport transport.protocol = self def connectionMade(self): print "MADE!" class FakeAddress(object): implements(IAddress) compareAttributes = ('type', 'host', 'port') type = 'fakeTCP' def __init__(self, host, port): self.host = host self.port = port def __repr__(self): return '%s(%r, %d)' % ( self.__class__.__name__, self.host, self.port) def __hash__(self): return hash((self.type, self.host, self.port)) class FakeListeningPort(object): implements(IListeningPort) def __init__(self, port): self.port = port def startListening(self): self.factory.doStart() def stopListening(self): self.factory.doStop() def getHost(self): return FakeAddress('host', self.port) def port_generator(): for x in xrange(65535, 0, -1): yield x from test_torconfig import FakeReactor # FIXME put in util or something? from test_torconfig import FakeProcessTransport # FIXME importing from other test sucks from test_torconfig import FakeControlProtocol # FIXME class FakeReactorTcp(FakeReactor): implements(IReactorTCP) failures = 0 _port_generator = port_generator() def __init__(self, test): self.protocol = TorControlProtocol() self.protocol.connectionMade = lambda: None self.transport = proto_helpers.StringTransport() self.transport = FakeProcessTransport() self.transport.protocol = self.protocol def blam(): self.protocol.outReceived("Bootstrap") self.transport.closeStdin = blam self.protocol.makeConnection(self.transport) FakeReactor.__init__(self, test, self.transport, lambda x: None) def listenTCP(self, port, factory, **kwargs): '''returns IListeningPort''' if self.failures > 0: self.failures -= 1 raise error.CannotListenError(None, None, None) if port == 0: port = self._port_generator.next() p = FakeListeningPort(port) p.factory = factory p.startListening() return p def connectTCP(self, host, port, factory, timeout, bindAddress): '''should return IConnector''' r = tcp.Connector( host, port, factory, timeout, bindAddress, reactor=self ) def blam(*args): print "BLAAAAAM", args r.connect = blam return r class FakeTorSocksEndpoint(object): def __init__(self, *args, **kw): self.host = args[1] self.port = args[2] self.transport = None self.failure = kw.get('failure', None) self.acceptPort = kw.get('acceptPort', None) def connect(self, fac): self.factory = fac if self.acceptPort: if self.port != self.acceptPort: return defer.fail(self.failure) else: if self.failure: return defer.fail(self.failure) self.proto = fac.buildProtocol(None) transport = proto_helpers.StringTransport() self.proto.makeConnection(transport) self.transport = transport return defer.succeed(self.proto) class TestTorClientEndpoint(unittest.TestCase): def test_client_connection_failed(self): """ This test is equivalent to txsocksx's TestSOCKS4ClientEndpoint.test_clientConnectionFailed """ def FailTorSocksEndpointGenerator(*args, **kw): kw['failure'] = connectionRefusedFailure return FakeTorSocksEndpoint(*args, **kw) endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=FailTorSocksEndpointGenerator) d = endpoint.connect(None) return self.assertFailure(d, ConnectionRefusedError) def test_client_connection_failed_user_password(self): """ Same as above, but with a username/password. """ def FailTorSocksEndpointGenerator(*args, **kw): kw['failure'] = connectionRefusedFailure return FakeTorSocksEndpoint(*args, **kw) endpoint = TorClientEndpoint( 'invalid host', 0, socks_username='billy', socks_password='s333cure', _proxy_endpoint_generator=FailTorSocksEndpointGenerator) d = endpoint.connect(None) return self.assertFailure(d, ConnectionRefusedError) def test_default_generator(self): # just ensuring the default generator doesn't blow updoesn't blow up default_tcp4_endpoint_generator(None, 'foo.bar', 1234) def test_no_host(self): self.assertRaises( ValueError, TorClientEndpoint, None, None ) def test_parser_basic(self): ep = clientFromString(None, 'tor:host=timaq4ygg2iegci7.onion:port=80:socksPort=9050') self.assertEqual(ep.host, 'timaq4ygg2iegci7.onion') self.assertEqual(ep.port, 80) self.assertEqual(ep.socks_port, 9050) def test_parser_user_password(self): epstring = 'tor:host=torproject.org:port=443' + \ ':socksUsername=foo:socksPassword=bar' ep = clientFromString(None, epstring) self.assertEqual(ep.host, 'torproject.org') self.assertEqual(ep.port, 443) self.assertEqual(ep.socks_username, 'foo') self.assertEqual(ep.socks_password, 'bar') def test_default_factory(self): """ This test is equivalent to txsocksx's TestSOCKS5ClientEndpoint.test_defaultFactory """ def TorSocksEndpointGenerator(*args, **kw): return FakeTorSocksEndpoint(*args, **kw) endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator) endpoint.connect(None) self.assertEqual(endpoint.tor_socks_endpoint.transport.value(), '\x05\x01\x00') def test_good_port_retry(self): """ This tests that our Tor client endpoint retry logic works correctly. We create a proxy endpoint that fires a connectionRefusedFailure unless the connecting port matches. We attempt to connect with the proxy endpoint for each port that the Tor client endpoint will try. """ success_ports = TorClientEndpoint.socks_ports_to_try for port in success_ports: def TorSocksEndpointGenerator(*args, **kw): kw['acceptPort'] = port kw['failure'] = connectionRefusedFailure return FakeTorSocksEndpoint(*args, **kw) endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator) endpoint.connect(None) self.assertEqual(endpoint.tor_socks_endpoint.transport.value(), '\x05\x01\x00') def test_bad_port_retry(self): """ This tests failure to connect to the ports on the "try" list. """ fail_ports = [1984, 666] for port in fail_ports: def TorSocksEndpointGenerator(*args, **kw): kw['acceptPort'] = port kw['failure'] = connectionRefusedFailure return FakeTorSocksEndpoint(*args, **kw) endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator) d = endpoint.connect(None) return self.assertFailure(d, ConnectionRefusedError) def test_good_no_guess_socks_port(self): """ This tests that if a SOCKS port is specified, we *only* attempt to connect to that SOCKS port. """ def TorSocksEndpointGenerator(*args, **kw): kw['acceptPort'] = 6669 kw['failure'] = connectionRefusedFailure return FakeTorSocksEndpoint(*args, **kw) endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator, socks_port=6669) endpoint.connect(None) self.assertEqual(endpoint.tor_socks_endpoint.transport.value(), '\x05\x01\x00') def test_bad_no_guess_socks_port(self): """ This tests that are connection fails if we try to connect to an unavailable specified SOCKS port... even if there is a valid SOCKS port listening on the socks_ports_to_try list. """ def TorSocksEndpointGenerator(*args, **kw): kw['acceptPort'] = 9050 kw['failure'] = connectionRefusedFailure return FakeTorSocksEndpoint(*args, **kw) endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator, socks_port=6669) d = endpoint.connect(None) self.assertFailure(d, ConnectionRefusedError) txtorcon-0.14.2/test/test_torcontrolprotocol.py0000644000175000017500000010251112627744056021746 0ustar mikemike00000000000000from __future__ import with_statement from os.path import exists from twisted.python import log, failure from twisted.trial import unittest from twisted.test import proto_helpers from twisted.internet import defer, error from txtorcon import TorControlProtocol, TorProtocolFactory, TorState from txtorcon import ITorControlProtocol from txtorcon.torcontrolprotocol import parse_keywords, DEFAULT_VALUE from txtorcon.util import hmac_sha256 import types import functools import tempfile import base64 class CallbackChecker: def __init__(self, expected): self.expected_value = expected self.called_back = False def __call__(self, *args, **kwargs): v = args[0] if v != self.expected_value: print "WRONG" raise RuntimeError( 'Expected "%s" but got "%s"' % (self.expected_value, v) ) self.called_back = True return v class InterfaceTests(unittest.TestCase): def test_implements(self): self.assertTrue(ITorControlProtocol.implementedBy(TorControlProtocol)) def test_object_implements(self): self.assertTrue(ITorControlProtocol.providedBy(TorControlProtocol())) class LogicTests(unittest.TestCase): def setUp(self): self.protocol = TorControlProtocol() self.protocol.connectionMade = lambda: None self.transport = proto_helpers.StringTransport() self.protocol.makeConnection(self.transport) def test_set_conf_wrong_args(self): ctl = TorControlProtocol() d = ctl.set_conf('a') self.assertTrue(d.called) self.assertTrue(d.result) self.assertTrue('even number' in d.result.getErrorMessage()) # ignore the error so trial doesn't get unhappy d.addErrback(lambda foo: True) return d class FactoryTests(unittest.TestCase): def test_create(self): TorProtocolFactory().buildProtocol(None) class AuthenticationTests(unittest.TestCase): def setUp(self): self.protocol = TorControlProtocol() self.transport = proto_helpers.StringTransport() def send(self, line): self.protocol.dataReceived(line.strip() + "\r\n") def test_authenticate_cookie(self): self.protocol.makeConnection(self.transport) self.assertEqual(self.transport.value(), 'PROTOCOLINFO 1\r\n') self.transport.clear() cookie_data = 'cookiedata!cookiedata!cookiedata' with open('authcookie', 'w') as f: f.write(cookie_data) self.send('250-PROTOCOLINFO 1') self.send('250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="authcookie"') self.send('250-VERSION Tor="0.2.2.34"') self.send('250 OK') self.assertEqual( self.transport.value(), 'AUTHENTICATE %s\r\n' % cookie_data.encode("hex") ) def test_authenticate_password(self): self.protocol.password_function = lambda: 'foo' self.protocol.makeConnection(self.transport) self.assertEqual(self.transport.value(), 'PROTOCOLINFO 1\r\n') self.transport.clear() self.send('250-PROTOCOLINFO 1') self.send('250-AUTH METHODS=HASHEDPASSWORD') self.send('250-VERSION Tor="0.2.2.34"') self.send('250 OK') self.assertEqual(self.transport.value(), 'AUTHENTICATE %s\r\n' % "foo".encode("hex")) def test_authenticate_password_deferred(self): d = defer.Deferred() self.protocol.password_function = lambda: d self.protocol.makeConnection(self.transport) self.assertEqual(self.transport.value(), 'PROTOCOLINFO 1\r\n') self.transport.clear() self.send('250-PROTOCOLINFO 1') self.send('250-AUTH METHODS=HASHEDPASSWORD') self.send('250-VERSION Tor="0.2.2.34"') self.send('250 OK') # make sure we haven't tried to authenticate before getting # the password callback self.assertEqual(self.transport.value(), '') d.callback('foo') # now make sure we DID try to authenticate self.assertEqual( self.transport.value(), 'AUTHENTICATE %s\r\n' % "foo".encode("hex") ) def test_authenticate_password_deferred_but_no_password(self): d = defer.Deferred() self.protocol.password_function = lambda: d self.protocol.makeConnection(self.transport) self.assertEqual(self.transport.value(), 'PROTOCOLINFO 1\r\n') self.transport.clear() self.send('250-PROTOCOLINFO 1') self.send('250-AUTH METHODS=HASHEDPASSWORD') self.send('250-VERSION Tor="0.2.2.34"') self.send('250 OK') d.callback(None) return self.assertFailure(self.protocol.post_bootstrap, RuntimeError) def confirmAuthFailed(self, *args): self.auth_failed = True def test_authenticate_no_password(self): self.protocol.post_bootstrap.addErrback(self.confirmAuthFailed) self.auth_failed = False self.protocol.makeConnection(self.transport) self.assertEqual(self.transport.value(), 'PROTOCOLINFO 1\r\n') self.send('250-PROTOCOLINFO 1') self.send('250-AUTH METHODS=HASHEDPASSWORD') self.send('250-VERSION Tor="0.2.2.34"') self.send('250 OK') self.assertTrue(self.auth_failed) class DisconnectionTests(unittest.TestCase): def setUp(self): self.protocol = TorControlProtocol() self.protocol.connectionMade = lambda: None self.transport = proto_helpers.StringTransportWithDisconnection() self.protocol.makeConnection(self.transport) # why doesn't makeConnection do this? self.transport.protocol = self.protocol def tearDown(self): self.protocol = None def test_disconnect_callback(self): """ see that we get our callback on_disconnect if the transport goes away """ def it_was_called(*args): it_was_called.yes = True return None it_was_called.yes = False self.protocol.on_disconnect.addCallback(it_was_called) self.protocol.on_disconnect.addErrback(it_was_called) f = failure.Failure(error.ConnectionDone("It's all over")) self.protocol.connectionLost(f) self.assertTrue(it_was_called.yes) def test_disconnect_errback(self): """ see that we get our callback on_disconnect if the transport goes away """ def it_was_called(*args): it_was_called.yes = True return None it_was_called.yes = False self.protocol.on_disconnect.addCallback(it_was_called) self.protocol.on_disconnect.addErrback(it_was_called) f = failure.Failure(RuntimeError("The thing didn't do the stuff.")) self.protocol.connectionLost(f) self.assertTrue(it_was_called.yes) class ProtocolTests(unittest.TestCase): def setUp(self): self.protocol = TorControlProtocol() self.protocol.connectionMade = lambda: None self.transport = proto_helpers.StringTransport() self.protocol.makeConnection(self.transport) def tearDown(self): self.protocol = None def send(self, line): self.protocol.dataReceived(line.strip() + "\r\n") def test_statemachine_broadcast_no_code(self): try: self.protocol._broadcast_response("foo") self.fail() except RuntimeError, e: self.assertTrue('No code set yet' in str(e)) def test_statemachine_broadcast_unknown_code(self): try: self.protocol.code = 999 self.protocol._broadcast_response("foo") self.fail() except RuntimeError, e: self.assertTrue('Unknown code' in str(e)) def test_statemachine_is_finish(self): self.assertTrue(not self.protocol._is_finish_line('')) self.assertTrue(self.protocol._is_finish_line('.')) self.assertTrue(self.protocol._is_finish_line('300 ')) self.assertTrue(not self.protocol._is_finish_line('250-')) def test_statemachine_singleline(self): self.assertTrue(not self.protocol._is_single_line_response('foo')) def test_statemachine_continuation(self): try: self.protocol.code = 250 self.protocol._is_continuation_line("123 ") self.fail() except RuntimeError, e: self.assertTrue('Unexpected code' in str(e)) def test_statemachine_multiline(self): try: self.protocol.code = 250 self.protocol._is_multi_line("123 ") self.fail() except RuntimeError, e: self.assertTrue('Unexpected code' in str(e)) def auth_failed(self, msg): self.assertEqual(str(msg.value), '551 go away') self.got_auth_failed = True def test_authenticate_fail(self): self.got_auth_failed = False self.protocol._auth_failed = self.auth_failed self.protocol.password_function = lambda: 'foo' self.protocol._do_authenticate('''PROTOCOLINFO 1 AUTH METHODS=HASHEDPASSWORD VERSION Tor="0.2.2.35" OK''') self.send('551 go away\r\n') self.assertTrue(self.got_auth_failed) def test_authenticate_no_auth_line(self): try: self.protocol._do_authenticate('''PROTOCOLINFO 1 FOOAUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE="/dev/null" VERSION Tor="0.2.2.35" OK''') self.assertTrue(False) except RuntimeError, e: self.assertTrue('find AUTH line' in str(e)) def test_authenticate_not_enough_cookie_data(self): with tempfile.NamedTemporaryFile() as cookietmp: cookietmp.write('x' * 35) # too much data cookietmp.flush() try: self.protocol._do_authenticate('''PROTOCOLINFO 1 AUTH METHODS=COOKIE COOKIEFILE="%s" VERSION Tor="0.2.2.35" OK''' % cookietmp.name) self.assertTrue(False) except RuntimeError, e: self.assertTrue('cookie to be 32' in str(e)) def test_authenticate_not_enough_safecookie_data(self): with tempfile.NamedTemporaryFile() as cookietmp: cookietmp.write('x' * 35) # too much data cookietmp.flush() try: self.protocol._do_authenticate('''PROTOCOLINFO 1 AUTH METHODS=SAFECOOKIE COOKIEFILE="%s" VERSION Tor="0.2.2.35" OK''' % cookietmp.name) self.assertTrue(False) except RuntimeError, e: self.assertTrue('cookie to be 32' in str(e)) def test_authenticate_safecookie(self): with tempfile.NamedTemporaryFile() as cookietmp: cookiedata = str(bytearray([0] * 32)) cookietmp.write(cookiedata) cookietmp.flush() self.protocol._do_authenticate('''PROTOCOLINFO 1 AUTH METHODS=SAFECOOKIE COOKIEFILE="%s" VERSION Tor="0.2.2.35" OK''' % cookietmp.name) self.assertTrue( 'AUTHCHALLENGE SAFECOOKIE ' in self.transport.value() ) client_nonce = base64.b16decode(self.transport.value().split()[-1]) self.transport.clear() server_nonce = str(bytearray([0] * 32)) server_hash = hmac_sha256( "Tor safe cookie authentication server-to-controller hash", cookiedata + client_nonce + server_nonce ) self.send( '250 AUTHCHALLENGE SERVERHASH=%s SERVERNONCE=%s' % (base64.b16encode(server_hash), base64.b16encode(server_nonce)) ) self.assertTrue('AUTHENTICATE ' in self.transport.value()) def test_authenticate_safecookie_wrong_hash(self): cookiedata = str(bytearray([0] * 32)) server_nonce = str(bytearray([0] * 32)) server_hash = str(bytearray([0] * 32)) # pretend we already did PROTOCOLINFO and read the cookie # file self.protocol.cookie_data = cookiedata self.protocol.client_nonce = server_nonce # all 0's anyway try: self.protocol._safecookie_authchallenge( '250 AUTHCHALLENGE SERVERHASH=%s SERVERNONCE=%s' % (base64.b16encode(server_hash), base64.b16encode(server_nonce)) ) self.assertTrue(False) except RuntimeError, e: self.assertTrue('hash not expected' in str(e)) def confirm_version_events(self, arg): self.assertEqual(self.protocol.version, 'foo') events = 'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL'.split() self.assertEqual(len(self.protocol.valid_events), len(events)) self.assertTrue(all(x in self.protocol.valid_events for x in events)) def test_bootstrap_callback(self): d = self.protocol.post_bootstrap d.addCallback(CallbackChecker(self.protocol)) d.addCallback(self.confirm_version_events) events = 'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL' self.protocol._bootstrap() # answer all the requests generated by boostrapping etc. self.send("250-signal/names=") self.send("250 OK") self.send("250-version=foo") self.send("250 OK") self.send("250-events/names=" + events) self.send("250 OK") self.send("250 OK") # for USEFEATURE return d def test_bootstrap_tor_does_not_support_signal_names(self): self.protocol._bootstrap() self.send('552 Unrecognized key "signal/names"') valid_signals = ["RELOAD", "DUMP", "DEBUG", "NEWNYM", "CLEARDNSCACHE"] self.assertEqual(self.protocol.valid_signals, valid_signals) def test_async(self): """ test the example from control-spec.txt to see that we handle interleaved async notifications properly. """ self.protocol._set_valid_events('CIRC') self.protocol.add_event_listener('CIRC', lambda _: None) self.send("250 OK") d = self.protocol.get_conf("SOCKSPORT ORPORT") self.send("650 CIRC 1000 EXTENDED moria1,moria2") self.send("250-SOCKSPORT=9050") self.send("250 ORPORT=0") return d def test_async_multiline(self): # same as above, but i think the 650's can be multline, # too. Like: # 650-CIRC 1000 EXTENDED moria1,moria2 0xBEEF # 650-EXTRAMAGIC=99 # 650 ANONYMITY=high self.protocol._set_valid_events('CIRC') self.protocol.add_event_listener( 'CIRC', CallbackChecker( "1000 EXTENDED moria1,moria2\nEXTRAMAGIC=99\nANONYMITY=high" ) ) self.send("250 OK") d = self.protocol.get_conf("SOCKSPORT ORPORT") d.addCallback(CallbackChecker({"ORPORT": "0", "SOCKSPORT": "9050"})) self.send("650-CIRC 1000 EXTENDED moria1,moria2") self.send("650-EXTRAMAGIC=99") self.send("650 ANONYMITY=high") self.send("250-SOCKSPORT=9050") self.send("250 ORPORT=0") return d def test_multiline_plus(self): """ """ d = self.protocol.get_info("FOO") d.addCallback(CallbackChecker({"FOO": "\na\nb\nc"})) self.send("250+FOO=") self.send("a") self.send("b") self.send("c") self.send(".") self.send("250 OK") return d def incremental_check(self, expected, actual): if '=' in actual: return self.assertEqual(expected, actual) def test_getinfo_incremental(self): d = self.protocol.get_info_incremental( "FOO", functools.partial(self.incremental_check, "bar") ) self.send("250+FOO=") self.send("bar") self.send("bar") self.send(".") self.send("250 OK") return d def test_getinfo_incremental_continuation(self): d = self.protocol.get_info_incremental( "FOO", functools.partial(self.incremental_check, "bar") ) self.send("250-FOO=") self.send("250-bar") self.send("250-bar") self.send("250 OK") return d def test_getinfo_one_line(self): d = self.protocol.get_info( "foo", functools.partial(self.incremental_check, "bar") ) self.send('250 foo=bar') return d def test_getconf(self): d = self.protocol.get_conf("SOCKSPORT ORPORT") d.addCallback(CallbackChecker({'SocksPort': '9050', 'ORPort': '0'})) self.send("250-SocksPort=9050") self.send("250 ORPort=0") return d def test_getconf_raw(self): d = self.protocol.get_conf_raw("SOCKSPORT ORPORT") d.addCallback(CallbackChecker('SocksPort=9050\nORPort=0')) self.send("250-SocksPort=9050") self.send("250 ORPort=0") return d def response_ok(self, v): self.assertEqual(v, '') def test_setconf(self): d = self.protocol.set_conf("foo", "bar").addCallback( functools.partial(self.response_ok) ) self.send("250 OK") self._wait(d) self.assertEqual(self.transport.value(), "SETCONF foo=bar\r\n") def test_setconf_with_space(self): d = self.protocol.set_conf("foo", "a value with a space") d.addCallback(functools.partial(self.response_ok)) self.send("250 OK") self._wait(d) self.assertEqual( self.transport.value(), 'SETCONF foo="a value with a space"\r\n' ) def test_setconf_multi(self): d = self.protocol.set_conf("foo", "bar", "baz", 1) self.send("250 OK") self._wait(d) self.assertEqual(self.transport.value(), "SETCONF foo=bar baz=1\r\n") def test_quit(self): d = self.protocol.quit() self.send("250 OK") self._wait(d) self.assertEqual(self.transport.value(), "QUIT\r\n") def test_dot(self): # just checking we don't expode self.protocol.graphviz_data() def test_debug(self): self.protocol.start_debug() self.assertTrue(exists('txtorcon-debug.log')) def error(self, failure): print "ERROR", failure self.assertTrue(False) def test_twocommands(self): "Two commands on the wire before first response." d1 = self.protocol.get_conf("FOO") ht = {"a": "one", "b": "two"} d1.addCallback(CallbackChecker(ht)).addErrback(log.err) d2 = self.protocol.get_info_raw("BAR") d2.addCallback(CallbackChecker("bar")).addErrback(log.err) self.send("250-a=one") self.send("250-b=two") self.send("250 OK") self.send("250 bar") return d2 def test_signal_error(self): try: self.protocol.signal('FOO') self.fail() except Exception, e: self.assertTrue('Invalid signal' in str(e)) def test_signal(self): self.protocol.valid_signals = ['NEWNYM'] self.protocol.signal('NEWNYM') self.assertEqual(self.transport.value(), 'SIGNAL NEWNYM\r\n') def test_650_after_authenticate(self): self.protocol._set_valid_events('CONF_CHANGED') self.protocol.add_event_listener( 'CONF_CHANGED', CallbackChecker("Foo=bar") ) self.send("250 OK") self.send("650-CONF_CHANGED") self.send("650-Foo=bar") def test_notify_after_getinfo(self): self.protocol._set_valid_events('CIRC') self.protocol.add_event_listener( 'CIRC', CallbackChecker("1000 EXTENDED moria1,moria2") ) self.send("250 OK") d = self.protocol.get_info("a") d.addCallback(CallbackChecker({'a': 'one'})).addErrback(self.fail) self.send("250-a=one") self.send("250 OK") self.send("650 CIRC 1000 EXTENDED moria1,moria2") return d def test_notify_error(self): self.protocol._set_valid_events('CIRC') self.send("650 CIRC 1000 EXTENDED moria1,moria2") def test_getinfo(self): d = self.protocol.get_info("version") d.addCallback(CallbackChecker({'version': '0.2.2.34'})) d.addErrback(self.fail) self.send("250-version=0.2.2.34") self.send("250 OK") self.assertEqual(self.transport.value(), "GETINFO version\r\n") return d def test_getinfo_for_descriptor(self): descriptor_info = """250+desc/name/moria1= router moria1 128.31.0.34 9101 0 9131 platform Tor 0.2.5.0-alpha-dev on Linux protocols Link 1 2 Circuit 1 published 2013-07-05 23:48:52 fingerprint 9695 DFC3 5FFE B861 329B 9F1A B04C 4639 7020 CE31 uptime 1818933 bandwidth 512000 62914560 1307929 extra-info-digest 17D0142F6EBCDF60160EB1794FA6C9717D581F8C caches-extra-info onion-key -----BEGIN RSA PUBLIC KEY----- MIGJAoGBALzd4bhz1usB7wpoaAvP+BBOnNIk7mByAKV6zvyQ0p1M09oEmxPMc3qD AAm276oJNf0eq6KWC6YprzPWFsXEIdXSqA6RWXCII1JG/jOoy6nt478BkB8TS9I9 1MJW27ppRaqnLiTmBmM+qzrsgJGwf+onAgUKKH2GxlVgahqz8x6xAgMBAAE= -----END RSA PUBLIC KEY----- signing-key -----BEGIN RSA PUBLIC KEY----- MIGJAoGBALtJ9uD7cD7iHjqNA3AgsX9prES5QN+yFQyr2uOkxzhvunnaf6SNhzWW bkfylnMrRm/qCz/czcjZO6N6EKHcXmypehvP566B7gAQ9vDsb+l7VZVWgXvzNc2s tl3P7qpC08rgyJh1GqmtQTCesIDqkEyWxwToympCt09ZQRq+fIttAgMBAAE= -----END RSA PUBLIC KEY----- hidden-service-dir contact 1024D/28988BF5 arma mit edu ntor-onion-key 9ZVjNkf/iLEnD685SpC5kcDytQ7u5ViiI9JOftdbE0k= reject *:* router-signature -----BEGIN SIGNATURE----- Y8Tj2e7mPbFJbguulkPEBVYzyO57p4btpWEXvRMD6vxIh/eyn25pehg5dUVBtZlL iO3EUE0AEYah2W9gdz8t+i3Dtr0zgqLS841GC/TyDKCm+MKmN8d098qnwK0NGF9q 01NZPuSqXM1b6hnl2espFzL7XL8XEGRU+aeg+f/ukw4= -----END SIGNATURE----- . 250 OK""" d = self.protocol.get_info("desc/name/moria1") d.addCallback(CallbackChecker({'desc/name/moria1': '\n' + '\n'.join(descriptor_info.split('\n')[1:-2])})) d.addErrback(self.fail) for line in descriptor_info.split('\n'): self.send(line) return d def test_addevent(self): self.protocol._set_valid_events('FOO BAR') self.protocol.add_event_listener('FOO', lambda _: None) # is it dangerous/ill-advised to depend on internal state of # class under test? d = self.protocol.defer self.send("250 OK") self._wait(d) self.assertEqual( self.transport.value().split('\r\n')[-2], "SETEVENTS FOO" ) self.transport.clear() self.protocol.add_event_listener('BAR', lambda _: None) d = self.protocol.defer self.send("250 OK") self.assertTrue(self.transport.value() == "SETEVENTS FOO BAR\r\n" or self.transport.value() == "SETEVENTS BAR FOO\r\n") self._wait(d) try: self.protocol.add_event_listener( 'SOMETHING_INVALID', lambda _: None ) self.assertTrue(False) except: pass def test_eventlistener(self): self.protocol._set_valid_events('STREAM') class EventListener(object): stream_events = 0 def __call__(self, data): self.stream_events += 1 listener = EventListener() self.protocol.add_event_listener('STREAM', listener) d = self.protocol.defer self.send("250 OK") self._wait(d) self.send("650 STREAM 1234 NEW 4321 1.2.3.4:555 REASON=MISC") self.send("650 STREAM 2345 NEW 4321 2.3.4.5:666 REASON=MISC") self.assertEqual(listener.stream_events, 2) def test_remove_eventlistener(self): self.protocol._set_valid_events('STREAM') class EventListener(object): stream_events = 0 def __call__(self, data): self.stream_events += 1 listener = EventListener() self.protocol.add_event_listener('STREAM', listener) self.assertEqual(self.transport.value(), 'SETEVENTS STREAM\r\n') self.protocol.lineReceived("250 OK") self.transport.clear() self.protocol.remove_event_listener('STREAM', listener) self.assertEqual(self.transport.value(), 'SETEVENTS \r\n') def test_remove_eventlistener_multiple(self): self.protocol._set_valid_events('STREAM') class EventListener(object): stream_events = 0 def __call__(self, data): self.stream_events += 1 listener0 = EventListener() listener1 = EventListener() self.protocol.add_event_listener('STREAM', listener0) self.assertEqual(self.transport.value(), 'SETEVENTS STREAM\r\n') self.protocol.lineReceived("250 OK") self.transport.clear() # add another one, shouldn't issue a tor command self.protocol.add_event_listener('STREAM', listener1) self.assertEqual(self.transport.value(), '') # remove one, should still not issue a tor command self.protocol.remove_event_listener('STREAM', listener0) self.assertEqual(self.transport.value(), '') # remove the other one, NOW should issue a command self.protocol.remove_event_listener('STREAM', listener1) self.assertEqual(self.transport.value(), 'SETEVENTS \r\n') # try removing invalid event try: self.protocol.remove_event_listener('FOO', listener0) self.fail() except Exception, e: self.assertTrue('FOO' in str(e)) def checkContinuation(self, v): self.assertEqual(v, "key=\nvalue0\nvalue1") def test_continuationLine(self): d = self.protocol.get_info_raw("key") d.addCallback(self.checkContinuation) self.send("250+key=") self.send("value0") self.send("value1") self.send(".") self.send("250 OK") return d def test_newdesc(self): """ FIXME: this test is now maybe a little silly, it's just testing multiline GETINFO... (Real test is in TorStateTests.test_newdesc_parse) """ self.protocol.get_info_raw('ns/id/624926802351575FF7E4E3D60EFA3BFB56E67E8A') d = self.protocol.defer d.addCallback(CallbackChecker("""ns/id/624926802351575FF7E4E3D60EFA3BFB56E67E8A= r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80 s Exit Fast Guard HSDir Named Running Stable V2Dir Valid w Bandwidth=518000 p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888""")) self.send("250+ns/id/624926802351575FF7E4E3D60EFA3BFB56E67E8A=") self.send("r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80") self.send("s Exit Fast Guard HSDir Named Running Stable V2Dir Valid") self.send("w Bandwidth=518000") self.send("p accept 43,53,79-81,110,143,194,220,443,953,989-990,993,995,1194,1293,1723,1863,2082-2083,2086-2087,2095-2096,3128,4321,5050,5190,5222-5223,6679,6697,7771,8000,8008,8080-8081,8090,8118,8123,8181,8300,8443,8888") self.send(".") self.send("250 OK") return d def test_plus_line_no_command(self): self.protocol.lineReceived("650+NS\r\n") self.protocol.lineReceived("r Gabor gFpAHsFOHGATy12ZUswRf0ZrqAU GG6GDp40cQfR3ODvkBT0r+Q09kw 2012-05-12 16:54:56 91.219.238.71 443 80\r\n") def test_minus_line_no_command(self): """ haven't seen 600's use - "in the wild" but don't see why it's not possible """ self.protocol._set_valid_events('NS') self.protocol.add_event_listener('NS', lambda _: None) self.protocol.lineReceived("650-NS\r\n") self.protocol.lineReceived("650 OK\r\n") class ParseTests(unittest.TestCase): def setUp(self): self.controller = TorState(TorControlProtocol()) self.controller.connectionMade = lambda _: None def test_keywords(self): x = parse_keywords('events/names=CIRC STREAM ORCONN BW DEBUG INFO NOTICE WARN ERR NEWDESC ADDRMAP AUTHDIR_NEWDESCS DESCCHANGED NS STATUS_GENERAL STATUS_CLIENT STATUS_SERVER GUARD STREAM_BW CLIENTS_SEEN NEWCONSENSUS BUILDTIMEOUT_SET') self.assertTrue('events/names' in x) self.assertEqual(x['events/names'], 'CIRC STREAM ORCONN BW DEBUG INFO NOTICE WARN ERR NEWDESC ADDRMAP AUTHDIR_NEWDESCS DESCCHANGED NS STATUS_GENERAL STATUS_CLIENT STATUS_SERVER GUARD STREAM_BW CLIENTS_SEEN NEWCONSENSUS BUILDTIMEOUT_SET') self.assertEqual(len(x.keys()), 1) def test_keywords_mutli_equals(self): x = parse_keywords('foo=something subvalue="foo"') self.assertEqual(len(x), 1) self.assertTrue('foo' in x) self.assertEqual(x['foo'], 'something subvalue="foo"') def test_default_keywords(self): x = parse_keywords('foo') self.assertEqual(len(x), 1) self.assertTrue('foo' in x) self.assertEqual(x['foo'], DEFAULT_VALUE) def test_multientry_keywords_2(self): x = parse_keywords('foo=bar\nfoo=zarimba') self.assertEqual(len(x), 1) self.assertTrue(isinstance(x['foo'], types.ListType)) self.assertEqual(len(x['foo']), 2) self.assertEqual(x['foo'][0], 'bar') self.assertEqual(x['foo'][1], 'zarimba') def test_multientry_keywords_3(self): x = parse_keywords('foo=bar\nfoo=baz\nfoo=zarimba') self.assertEqual(len(x), 1) self.assertTrue(isinstance(x['foo'], types.ListType)) self.assertEqual(len(x['foo']), 3) self.assertEqual(x['foo'][0], 'bar') self.assertEqual(x['foo'][1], 'baz') self.assertEqual(x['foo'][2], 'zarimba') def test_multientry_keywords_4(self): x = parse_keywords('foo=bar\nfoo=baz\nfoo=zarimba\nfoo=foo') self.assertEqual(len(x), 1) self.assertTrue(isinstance(x['foo'], types.ListType)) self.assertEqual(len(x['foo']), 4) self.assertEqual(x['foo'][0], 'bar') self.assertEqual(x['foo'][1], 'baz') self.assertEqual(x['foo'][2], 'zarimba') self.assertEqual(x['foo'][3], 'foo') def test_multiline_keywords_with_spaces(self): x = parse_keywords('''ns/name/foo= r foo aaaam7E7h1vY5Prk8v9/nSRCydY BBBBOfum4CtAYuOgf/D33Qq5+rk 2013-10-27 06:22:18 1.2.3.4 9001 9030 s Fast Guard HSDir Running Stable V2Dir Valid w Bandwidth=1234 ns/name/bar= r bar aaaaHgNYtTVPw5hHTO28J4je5i8 BBBBBUaJaBFSU/HDrTxnSh+D3+fY 2013-10-27 07:48:56 1.2.4.5 9001 9030 s Exit Fast Guard HSDir Named Running Stable V2Dir Valid w Bandwidth=1234 OK ''') self.assertEqual(2, len(x)) keys = x.keys() keys.sort() self.assertEqual(keys, ['ns/name/bar', 'ns/name/foo']) def test_multiline_keywords(self): x = parse_keywords('''Foo=bar\nBar''') self.assertEqual(x, {'Foo': 'bar\nBar'}) x = parse_keywords('''Foo=bar\nBar''', multiline_values=False) self.assertEqual(x, {'Foo': 'bar', 'Bar': DEFAULT_VALUE}) def test_unquoted_keywords(self): x = parse_keywords('''Tor="0.1.2.3.4-rc44"''') self.assertEqual(x, {'Tor': '0.1.2.3.4-rc44'}) def test_unquoted_keywords_singlequote(self): x = parse_keywords("Tor='0.1.2.3.4-rc44'") self.assertEqual(x, {'Tor': '0.1.2.3.4-rc44'}) def test_unquoted_keywords_empty(self): x = parse_keywords('foo=') self.assertEqual(x, {'foo': ''}) def test_network_status(self): self.controller._update_network_status("""ns/all= r right2privassy3 ADQ6gCT3DiFHKPDFr3rODBUI8HM JehnjB8l4Js47dyjLCEmE8VJqao 2011-12-02 03:36:40 50.63.8.215 9023 0 s Exit Fast Named Running Stable Valid w Bandwidth=53 p accept 80,1194,1220,1293,1500,1533,1677,1723,1863,2082-2083,2086-2087,2095-2096,2102-2104,3128,3389,3690,4321,4643,5050,5190,5222-5223,5228,5900,6660-6669,6679,6697,8000,8008,8074,8080,8087-8088,8443,8888,9418,9999-10000,19294,19638 r Unnamed AHe2V2pmj4Yfn0H9+Np3lci7htU T/g7ZLzG/ooqCn+gdLd9Jjh+AEI 2011-12-02 15:52:09 84.101.216.232 443 9030 s Exit Fast Running V2Dir Valid w Bandwidth=33 p reject 25,119,135-139,445,563,1214,4661-4666,6346-6429,6699,6881-6999""") # the routers list is always keyed with both name and hash self.assertEqual(len(self.controller.routers_by_name), 2) self.assertEqual(len(self.controller.routers_by_hash), 2) self.assertTrue('right2privassy3' in self.controller.routers) self.assertTrue('Unnamed' in self.controller.routers) self.controller.routers.clear() self.controller.routers_by_name.clear() self.controller.routers_by_hash.clear() def test_circuit_status(self): self.controller._update_network_status("""ns/all= r wildnl f+Ty/+B6lgYr0Ntbf67O/L2M8ZI c1iK/kPPXKGZZvwXRWbvL9eCfSc 2011-12-02 19:07:05 209.159.142.164 9001 0 s Exit Fast Named Running Stable Valid w Bandwidth=1900 p reject 25,119,135-139,445,563,1214,4661-4666,6346-6429,6699,6881-6999 r l0l wYXUpLBpzVWfzVSMgGO0dThdd38 KIJC+W1SHeaFOj/BVsEAgxbtQNM 2011-12-02 13:43:39 94.23.168.39 443 80 s Fast Named Running Stable V2Dir Valid w Bandwidth=22800 p reject 1-65535 r Tecumseh /xAD0tFLS50Dkz+O37xGyVLoKlk yJHbad7MFl1VW2/23RxrPKBTOIE 2011-12-02 09:44:10 76.73.48.211 22 9030 s Fast Guard HSDir Named Running Stable V2Dir Valid w Bandwidth=18700 p reject 1-65535""") self.controller._circuit_status("""circuit-status= 4472 BUILT $FF1003D2D14B4B9D03933F8EDFBC46C952E82A59=Tecumseh,$C185D4A4B069CD559FCD548C8063B475385D777F=l0l,$7FE4F2FFE07A96062BD0DB5B7FAECEFCBD8CF192=wildnl PURPOSE=GENERAL""") self.assertEqual(len(self.controller.circuits), 1) self.assertTrue(4472 in self.controller.circuits) self.controller.routers.clear() self.controller.routers_by_name.clear() self.controller.routers_by_hash.clear() self.controller.circuits.clear() txtorcon-0.14.2/test/test_addrmap.py0000644000175000017500000001611712611263616017364 0ustar mikemike00000000000000import datetime from twisted.trial import unittest from twisted.internet import task from twisted.internet.interfaces import IReactorTime from zope.interface import implements from txtorcon.addrmap import AddrMap from txtorcon.interface import IAddrListener class AddrMapTests(unittest.TestCase): implements(IAddrListener) fmt = '%Y-%m-%d %H:%M:%S' def test_parse(self): """ Make sure it's parsing things properly. """ now = datetime.datetime.now() + datetime.timedelta(seconds=10) nowutc = datetime.datetime.utcnow() + datetime.timedelta(seconds=10) # we need to not-barf on extra args as per control-spec.txt line = 'www.example.com 72.30.2.43 "%s" EXPIRES="%s" FOO=bar BAR=baz' % (now.strftime(self.fmt), nowutc.strftime(self.fmt)) am = AddrMap() am.update(line) addr = am.find('www.example.com') self.assertTrue(addr.ip == '72.30.2.43' or addr.ip.exploded == '72.30.2.43') # maybe not the most robust, should convert to # seconds-since-epoch instead? the net result of the parsing # is we've rounded to seconds... self.assertEqual(addr.expires.ctime(), nowutc.ctime()) line = 'www.example.com 72.30.2.43 "%s" "%s"' % (now.strftime(self.fmt), nowutc.strftime(self.fmt)) am.update(line) self.assertEqual(addr.expires.ctime(), nowutc.ctime()) # this will have resulted in an expiry call, which we need to # cancel to keep the reactor clean. for consistency, we use # the IReactorTime interface from AddrMap am.scheduler.getDelayedCalls()[0].cancel() def test_expires(self): """ Test simply expiry case """ clock = task.Clock() am = AddrMap() am.scheduler = IReactorTime(clock) now = datetime.datetime.now() + datetime.timedelta(seconds=10) nowutc = datetime.datetime.utcnow() + datetime.timedelta(seconds=10) line = 'www.example.com 72.30.2.43 "%s" EXPIRES="%s"' % (now.strftime(self.fmt), nowutc.strftime(self.fmt)) am.update(line) self.assertTrue('www.example.com' in am.addr) # advance time past when the expiry should have occurred clock.advance(10) self.assertTrue('www.example.com' not in am.addr) def test_expires_never(self): """ Test a NEVER expires line, as in what we'd get a startup for a configured address-mapping. """ clock = task.Clock() am = AddrMap() am.scheduler = IReactorTime(clock) line = 'www.example.com 72.30.2.43 "NEVER"' am.update(line) self.assertTrue('www.example.com' in am.addr) self.assertEqual(len(clock.getDelayedCalls()), 0) def test_expires_old(self): """ Test something that expires before "now" """ clock = task.Clock() am = AddrMap() am.scheduler = IReactorTime(clock) now = datetime.datetime.now() + datetime.timedelta(seconds=-10) nowutc = datetime.datetime.utcnow() + datetime.timedelta(seconds=-10) line = 'www.example.com 72.30.2.43 "%s" EXPIRES="%s"' % (now.strftime(self.fmt), nowutc.strftime(self.fmt)) am.update(line) self.assertTrue('www.example.com' in am.addr) # arguably we shouldn't even have put this in the map maybe, # but the reactor needs to iterate before our expiry callback # gets called (right away) which is simulated by the # clock.advance call clock.advance(0) self.assertTrue('www.example.com' not in am.addr) def test_expires_with_update(self): """ This test updates the expiry time and checks that we properly delay our expiry callback. """ clock = task.Clock() am = AddrMap() am.scheduler = IReactorTime(clock) # now do an actual update to an existing Addr entry. now = datetime.datetime.now() + datetime.timedelta(seconds=10) nowutc = datetime.datetime.utcnow() + datetime.timedelta(seconds=10) line = 'www.example.com 72.30.2.43 "%s" EXPIRES="%s"' % (now.strftime(self.fmt), nowutc.strftime(self.fmt)) am.update(line) self.assertTrue(am.find('www.example.com')) # the update now = datetime.datetime.now() + datetime.timedelta(seconds=20) nowutc = datetime.datetime.utcnow() + datetime.timedelta(seconds=20) line = 'www.example.com 72.30.2.43 "%s" EXPIRES="%s"' % (now.strftime(self.fmt), nowutc.strftime(self.fmt)) am.update(line) self.assertTrue('www.example.com' in am.addr) # advance time by the old expiry value and we should still # find the entry clock.advance(10) self.assertTrue('www.example.com' in am.addr) # ...but advance past the new expiry (another 10 seconds) and # it should vanish clock.advance(10) self.assertTrue('www.example.com' not in am.addr) def test_8596_cached_1(self): clock = task.Clock() am = AddrMap() am.scheduler = IReactorTime(clock) line = 'example.com 192.0.2.1 NEVER CACHED="YES"' am.update(line) self.assertTrue('example.com' in am.addr) self.assertEqual(len(clock.getDelayedCalls()), 0) def test_8596_cached_2(self): clock = task.Clock() am = AddrMap() am.scheduler = IReactorTime(clock) line = 'example.com 192.0.43.10 "2013-04-03 22:29:11" EXPIRES="2013-04-03 20:29:11" CACHED="NO"' am.update(line) self.assertTrue('example.com' in am.addr) self.assertEqual(len(clock.getDelayedCalls()), 1) def test_8596_cached_3(self): clock = task.Clock() am = AddrMap() am.scheduler = IReactorTime(clock) line = 'example.invalid "2013-04-03 08:28:52" error=yes EXPIRES="2013-04-03 06:28:52" CACHE="NO"' am.update(line) self.assertTrue('example.invalid' not in am.addr) self.assertEqual(len(clock.getDelayedCalls()), 0) def addrmap_expired(self, name): self.expires.append(name) def addrmap_added(self, addr): self.addrmap.append(addr) def test_double_add_listener(self): am = AddrMap() am.add_listener(self) am.add_listener(self) self.assertEqual(1, len(am.listeners)) def test_listeners(self): self.expires = [] self.addrmap = [] clock = task.Clock() am = AddrMap() am.scheduler = IReactorTime(clock) am.add_listener(self) now = datetime.datetime.now() + datetime.timedelta(seconds=10) nowutc = datetime.datetime.utcnow() + datetime.timedelta(seconds=10) line = 'www.example.com 72.30.2.43 "%s" EXPIRES="%s"' % (now.strftime(self.fmt), nowutc.strftime(self.fmt)) am.update(line) # see if our listener got an update a = am.find('www.example.com') self.assertEqual(self.addrmap, [a]) # advance time past when the expiry should have occurred clock.advance(10) # check that our listener got an expires event self.assertEqual(self.expires, ['www.example.com']) txtorcon-0.14.2/test/test_util_imports.py0000644000175000017500000000452312627744056020515 0ustar mikemike00000000000000from twisted.trial import unittest import sys import types import functools from unittest import skipIf def fake_import(orig, name, *args, **kw): if name in ['GeoIP', 'ipaddr']: raise ImportError('testing!') return orig(*((name,) + args), **kw) class TestImports(unittest.TestCase): @skipIf('pypy' in sys.version.lower(), "Doesn't work in PYPY") def test_no_GeoIP(self): """ Make sure we don't explode if there's no GeoIP module """ global __import__ orig = __import__ try: # attempt to ensure we've unimportted txtorcon.util try: del sys.modules['txtorcon.util'] except KeyError: pass import gc gc.collect() # replace global import with our test import, which will # throw on GeoIP import no matter what global __builtins__ __builtins__['__import__'] = functools.partial(fake_import, orig) # now ensure we set up all the databases as "None" when we # import w/o the GeoIP thing available. import txtorcon.util ipa = txtorcon.util.maybe_ip_addr('127.0.0.1') self.assertTrue(isinstance(ipa, types.StringType)) finally: __import__ = orig @skipIf('pypy' in sys.version.lower(), "Doesn't work in PYPY") def test_no_ipaddr(self): """ make sure the code we run if there's no ipaddr installed doesn't do anything horrific """ global __import__ orig = __import__ try: # attempt to ensure we've unimportted txtorcon.util del sys.modules['txtorcon.util'] import gc gc.collect() # replace global import with our test import, which will # throw on GeoIP import no matter what global __builtins__ __builtins__['__import__'] = functools.partial(fake_import, orig) # now ensure we set up all the databases as "None" when we # import w/o the GeoIP thing available. import txtorcon.util self.assertEqual(None, txtorcon.util.city) self.assertEqual(None, txtorcon.util.asn) self.assertEqual(None, txtorcon.util.country) finally: __import__ = orig txtorcon-0.14.2/test/test_stream.py0000644000175000017500000003356112611263616017251 0ustar mikemike00000000000000from txtorcon.util import maybe_ip_addr from twisted.trial import unittest from twisted.internet import defer from zope.interface import implements from txtorcon import Stream from txtorcon import IStreamListener from txtorcon import ICircuitContainer from txtorcon import StreamListenerMixin class FakeCircuit: def __init__(self, id=-999): self.streams = [] self.id = id class Listener(object): implements(IStreamListener) def __init__(self, expected): "expect is a list of tuples: (event, {key:value, key1:value1, ..})" self.expected = expected def checker(self, state, stream, *args, **kw): if self.expected[0][0] != state: raise RuntimeError( 'Expected event "%s" not "%s".' % (self.expected[0][0], state) ) for (k, v) in self.expected[0][1].items(): if k == 'args': if v != args: raise RuntimeError( 'Expected argument to have value "%s", not "%s"' % (v, args) ) elif k == 'kwargs': for (key, value) in v.items(): if key not in kw: print key, value, k, v, kw raise RuntimeError( 'Expected keyword argument for key "%s" but found nothing.' % key ) elif kw[key] != value: raise RuntimeError( 'KW Argument expected "%s" but got "%s"' % (value, kw[key]) ) elif getattr(stream, k) != v: raise RuntimeError( 'Expected attribute "%s" to have value "%s", not "%s"' % (k, v, getattr(stream, k)) ) self.expected = self.expected[1:] def stream_new(self, stream): "a new stream has been created" self.checker('new', stream) def stream_succeeded(self, stream): "stream has succeeded" self.checker('succeeded', stream) def stream_attach(self, stream, circuit): "the stream has been attached to a circuit" self.checker('attach', stream, circuit) def stream_detach(self, stream, **kw): "the stream has been attached to a circuit" self.checker('detach', stream, **kw) def stream_closed(self, stream, **kw): "stream has been closed (won't be in controller's list anymore)" self.checker('closed', stream, **kw) def stream_failed(self, stream, **kw): "stream failed for some reason (won't be in controller's list anymore)" self.checker('failed', stream, **kw) class StreamTests(unittest.TestCase): implements(ICircuitContainer) def find_circuit(self, id): return self.circuits[id] def close_circuit(self, circuit, **kw): raise NotImplementedError() def close_stream(self, stream, **kw): return defer.succeed('') def setUp(self): self.circuits = {} def test_lowercase_flags(self): # testing an internal method, maybe a no-no? stream = Stream(self) kw = dict(FOO='bar', BAR='baz') flags = stream._create_flags(kw) self.assertTrue('FOO' in flags) self.assertTrue('foo' in flags) self.assertTrue(flags['foo'] is flags['FOO']) self.assertTrue('BAR' in flags) self.assertTrue('bar' in flags) self.assertTrue(flags['bar'] is flags['BAR']) def test_listener_mixin(self): listener = StreamListenerMixin() from zope.interface.verify import verifyObject self.assertTrue(verifyObject(IStreamListener, listener)) # call all the methods with None for each arg. This is mostly # just to gratuitously increase test coverage, but also # serves to ensure these methods don't just blow up for (methodname, desc) in IStreamListener.namesAndDescriptions(): method = getattr(listener, methodname) args = [None] * len(desc.positional) method(*args) def test_circuit_already_valid_in_new(self): stream = Stream(self) stream.circuit = FakeCircuit(1) stream.update("1 NEW 0 94.23.164.42.$43ED8310EB968746970896E8835C2F1991E50B69.exit:9001 SOURCE_ADDR=(Tor_internal):0 PURPOSE=DIR_FETCH".split()) errs = self.flushLoggedErrors() self.assertEqual(len(errs), 1) self.assertTrue('Weird' in errs[0].getErrorMessage()) def test_magic_circuit_detach(self): stream = Stream(self) stream.circuit = FakeCircuit(1) stream.circuit.streams = [stream] stream.update("1 SENTCONNECT 0 94.23.164.42.$43ED8310EB968746970896E8835C2F1991E50B69.exit:9001 SOURCE_ADDR=(Tor_internal):0 PURPOSE=DIR_FETCH".split()) self.assertTrue(stream.circuit is None) def test_args_in_ctor(self): stream = Stream(self) stream.update("1 NEW 0 94.23.164.42.$43ED8310EB968746970896E8835C2F1991E50B69.exit:9001 SOURCE_ADDR=(Tor_internal):0 PURPOSE=DIR_FETCH".split()) self.assertEqual(stream.id, 1) self.assertEqual(stream.state, 'NEW') def test_parse_resolve(self): stream = Stream(self) stream.update("1604 NEWRESOLVE 0 www.google.ca:0 PURPOSE=DNS_REQUEST".split()) self.assertEqual(stream.state, 'NEWRESOLVE') def test_listener_new(self): listener = Listener([('new', {'target_port': 9001})]) stream = Stream(self) stream.listen(listener) stream.update("1 NEW 0 94.23.164.42.$43ED8310EB968746970896E8835C2F1991E50B69.exit:9001 SOURCE_ADDR=(Tor_internal):0 PURPOSE=DIR_FETCH".split()) def test_listener_attach(self): self.circuits[186] = FakeCircuit(186) listener = Listener( [ ('new', {'target_host': 'www.yahoo.com', 'target_port': 80}), ('attach', {'target_addr': maybe_ip_addr('1.2.3.4')}) ] ) stream = Stream(self) stream.listen(listener) stream.update("316 NEW 0 www.yahoo.com:80 SOURCE_ADDR=127.0.0.1:55877 PURPOSE=USER".split()) stream.update("316 REMAP 186 1.2.3.4:80 SOURCE=EXIT".split()) self.assertEqual(self.circuits[186].streams[0], stream) def test_listener_attach_no_remap(self): "Attachment is via SENTCONNECT on .onion addresses (for example)" self.circuits[186] = FakeCircuit(186) listener = Listener([('new', {'target_host': 'www.yahoo.com', 'target_port': 80}), ('attach', {})]) stream = Stream(self) stream.listen(listener) stream.update("316 NEW 0 www.yahoo.com:80 SOURCE_ADDR=127.0.0.1:55877 PURPOSE=USER".split()) stream.update("316 SENTCONNECT 186 1.2.3.4:80 SOURCE=EXIT".split()) self.assertEqual(self.circuits[186].streams[0], stream) def test_update_wrong_stream(self): self.circuits[186] = FakeCircuit(186) stream = Stream(self) stream.update("316 NEW 0 www.yahoo.com:80 SOURCE_ADDR=127.0.0.1:55877 PURPOSE=USER".split()) try: stream.update("999 SENTCONNECT 186 1.2.3.4:80 SOURCE=EXIT".split()) self.fail() except Exception, e: self.assertTrue('wrong stream' in str(e)) def test_update_illegal_state(self): self.circuits[186] = FakeCircuit(186) stream = Stream(self) try: stream.update("316 FOO 0 www.yahoo.com:80 SOURCE_ADDR=127.0.0.1:55877 PURPOSE=USER".split()) self.fail() except Exception, e: self.assertTrue('Unknown state' in str(e)) def test_listen_unlisten(self): self.circuits[186] = FakeCircuit(186) listener = Listener([]) stream = Stream(self) stream.listen(listener) stream.listen(listener) self.assertEqual(len(stream.listeners), 1) stream.unlisten(listener) self.assertEqual(len(stream.listeners), 0) def test_stream_changed(self): "Change a stream-id mid-stream." self.circuits[186] = FakeCircuit(186) listener = Listener([('new', {'target_host': 'www.yahoo.com', 'target_port': 80}), ('attach', {}), ('succeeded', {})]) stream = Stream(self) stream.listen(listener) stream.update("316 NEW 0 www.yahoo.com:80 SOURCE_ADDR=127.0.0.1:55877 PURPOSE=USER".split()) stream.update("316 SENTCONNECT 186 1.2.3.4:80 SOURCE=EXIT".split()) self.assertEqual(self.circuits[186].streams[0], stream) # magically change circuit ID without a DETACHED, should fail stream.update("316 SUCCEEDED 999 1.2.3.4:80 SOURCE=EXIT".split()) errs = self.flushLoggedErrors() self.assertEqual(len(errs), 1) # kind of fragile to look at strings, but... self.assertTrue('186 to 999' in str(errs[0])) def test_stream_changed_with_detach(self): "Change a stream-id mid-stream, but with a DETACHED message" self.circuits[123] = FakeCircuit(123) self.circuits[456] = FakeCircuit(456) listener = Listener( [ ('new', {'target_host': 'www.yahoo.com', 'target_port': 80}), ('attach', {}), ('detach', {'kwargs': dict(reason='END', remote_reason='MISC')}), ('attach', {}) ] ) stream = Stream(self) stream.listen(listener) stream.update("999 NEW 0 www.yahoo.com:80 SOURCE_ADDR=127.0.0.1:55877 PURPOSE=USER".split()) stream.update("999 SENTCONNECT 123 1.2.3.4:80".split()) self.assertEqual(len(self.circuits[123].streams), 1) self.assertEqual(self.circuits[123].streams[0], stream) stream.update("999 DETACHED 123 1.2.3.4:80 REASON=END REMOTE_REASON=MISC".split()) self.assertEqual(len(self.circuits[123].streams), 0) stream.update("999 SENTCONNECT 456 1.2.3.4:80 SOURCE=EXIT".split()) self.assertEqual(len(self.circuits[456].streams), 1) self.assertEqual(self.circuits[456].streams[0], stream) def test_listener_close(self): self.circuits[186] = FakeCircuit(186) listener = Listener( [ ('new', {'target_host': 'www.yahoo.com', 'target_port': 80}), ('attach', {'target_addr': maybe_ip_addr('1.2.3.4')}), ('closed', {'kwargs': dict(REASON='END', REMOTE_REASON='DONE')}) ] ) stream = Stream(self) stream.listen(listener) stream.update("316 NEW 0 www.yahoo.com:80 SOURCE_ADDR=127.0.0.1:55877 PURPOSE=USER".split()) stream.update("316 REMAP 186 1.2.3.4:80 SOURCE=EXIT".split()) stream.update("316 CLOSED 186 1.2.3.4:80 REASON=END REMOTE_REASON=DONE".split()) self.assertEqual(len(self.circuits[186].streams), 0) def test_listener_fail(self): listener = Listener( [ ('new', {'target_host': 'www.yahoo.com', 'target_port': 80}), ('attach', {'target_addr': maybe_ip_addr('1.2.3.4')}), ('failed', {'kwargs': dict(REASON='TIMEOUT', REMOTE_REASON='DESTROYED')}) ] ) stream = Stream(self) stream.listen(listener) stream.update("316 NEW 0 www.yahoo.com:80 SOURCE_ADDR=127.0.0.1:55877 PURPOSE=USER".split()) self.circuits[186] = FakeCircuit(186) stream.update("316 REMAP 186 1.2.3.4:80 SOURCE=EXIT".split()) stream.update("316 FAILED 0 1.2.3.4:80 REASON=TIMEOUT REMOTE_REASON=DESTROYED".split()) def test_str(self): stream = Stream(self) stream.update("316 NEW 0 www.yahoo.com:80 SOURCE_ADDR=127.0.0.1:55877 PURPOSE=USER".split()) stream.circuit = FakeCircuit(1) str(stream) def test_ipv6(self): listener = Listener([('new', {'target_host': '::1', 'target_port': 80})]) stream = Stream(self) stream.listen(listener) stream.update("1234 NEW 0 ::1:80 SOURCE_ADDR=127.0.0.1:57349 PURPOSE=USER".split()) def test_ipv6_remap(self): stream = Stream(self) stream.update("1234 REMAP 0 ::1:80 SOURCE_ADDR=127.0.0.1:57349 PURPOSE=USER".split()) self.assertEqual(stream.target_addr, maybe_ip_addr('::1')) def test_ipv6_source(self): listener = Listener( [ ('new', {'source_addr': maybe_ip_addr('::1'), 'source_port': 12345}) ] ) stream = Stream(self) stream.listen(listener) stream.update("1234 NEW 0 127.0.0.1:80 SOURCE_ADDR=::1:12345 PURPOSE=USER".split()) def test_states_and_uris(self): self.circuits[1] = FakeCircuit(1) stream = Stream(self) for address in [ '1.2.3.4:80', '1.2.3.4.315D5684D5343580D409F16119F78D776A58AEFB.exit:80', 'timaq4ygg2iegci7.onion:80']: line = "316 %s 1 %s REASON=FOO" for state in ['NEW', 'SUCCEEDED', 'REMAP', 'SENTCONNECT', 'DETACHED', 'NEWRESOLVE', 'SENTRESOLVE', 'FAILED', 'CLOSED']: stream.update((line % (state, address)).split(' ')) self.assertEqual(stream.state, state) def test_close_stream(self): self.circuits[186] = FakeCircuit(186) stream = Stream(self) stream.update("316 NEW 0 www.yahoo.com:80 SOURCE_ADDR=127.0.0.1:55877 PURPOSE=USER".split()) stream.update("316 REMAP 186 1.2.3.4:80 SOURCE=EXIT".split()) self.assertEqual(len(self.circuits[186].streams), 1) d = stream.close() self.assertTrue(not d.called) self.assertEqual(len(self.circuits[186].streams), 1) stream.update("316 CLOSED 186 1.2.3.4:80 REASON=END REMOTE_REASON=DONE".split()) self.assertTrue(d.called) self.assertEqual(len(self.circuits[186].streams), 0) txtorcon-0.14.2/test/__init__.py0000644000175000017500000000000012515516430016432 0ustar mikemike00000000000000txtorcon-0.14.2/test/test_fsm.py0000644000175000017500000000657012572755166016557 0ustar mikemike00000000000000 import txtorcon.spaghetti from txtorcon.spaghetti import * from twisted.trial import unittest import os import subprocess import tempfile class FsmTests(unittest.TestCase): def match(self, data): if data.split()[0] == '250': return True return False def test_reprs(self): """ not really 'testing' here, going for code-coverage to simply call the __str__ methods to ensure they don't explode """ a = State("A") b = State("B") tran = Transition(b, lambda x: None, lambda x: None) a.add_transition(tran) fsm = FSM([a, b]) str(fsm) str(a) str(tran) tran.start_state = None str(tran) fsm.dotty() def test_no_init(self): fsm = FSM([]) self.assertRaises(Exception, fsm.process, "") def test_no_init_ctor(self): fsm = FSM([]) idle = State("I") str(idle) fsm.add_state(idle) self.assertWarns(RuntimeWarning, "No next state", txtorcon.spaghetti.__file__, fsm.process, "") def test_two_states(self): fsm = FSM([]) idle = State("I") notidle = State("N") fsm.add_state(idle) fsm.add_state(notidle) def test_no_matcher(self): idle = State("I") other = State("O") fsm = FSM([idle, other]) idle.add_transition(Transition(other, None, None)) fsm.process("") def test_bad_transition(self): self.assertRaises(Exception, Transition, None, self.match, None) def test_dotty(self): idle = State("I") fsm = FSM([idle]) self.assertTrue(idle.dotty() in fsm.dotty()) self.assertTrue("digraph" in fsm.dotty()) fname = tempfile.mktemp() + '.dot' try: f = open(fname, 'w') f.write(fsm.dotty()) f.close() try: proc = subprocess.Popen(('dot', fname), stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError: # Graphviz probably not available; skip return else: _, stderr = proc.communicate() retcode = proc.poll() if retcode: self.fail('Calling dot returned %i (%s)' % (retcode, stderr)) finally: os.unlink(fname) def test_handler_state(self): idle = State("I") cmd = State("C") idle.add_transitions([Transition(cmd, self.match, lambda x: idle)]) fsm = FSM([idle, cmd]) self.commands = [] self.assertEqual(fsm.state, idle) fsm.process("250 OK\n") self.assertEqual(fsm.state, idle) def test_simple_machine(self): idle = State("I") cmd = State("C") idle.add_transitions([Transition(cmd, self.match, None)]) fsm = FSM([idle, cmd]) self.commands = [] self.assertEqual(fsm.state, idle) fsm.process("250 OK\n") self.assertEqual(fsm.state, cmd) def doCommand(self, data): print "transition:", data txtorcon-0.14.2/test/test_router.py0000644000175000017500000001405112572755166017303 0ustar mikemike00000000000000from datetime import datetime from twisted.trial import unittest from twisted.internet import defer from txtorcon.router import Router, hexIdFromHash, hashFromHexId class FakeController(object): def get_info_raw(self, i): return defer.succeed('250-ip-to-country/something=XX\r\n250 OK') class UtilityTests(unittest.TestCase): def test_hex_converters(self): self.assertEqual( hexIdFromHash('AHhuQ8zFQJdT8l42Axxc6m6kNwI'), '$00786E43CCC5409753F25E36031C5CEA6EA43702' ) self.assertEqual( hashFromHexId('$00786E43CCC5409753F25E36031C5CEA6EA43702'), 'AHhuQ8zFQJdT8l42Axxc6m6kNwI' ) # should work with or without leading $ self.assertEqual( hexIdFromHash(hashFromHexId('00786E43CCC5409753F25E36031C5CEA6EA43702')), '$00786E43CCC5409753F25E36031C5CEA6EA43702' ) class RouterTests(unittest.TestCase): def test_ctor(self): controller = object() router = Router(controller) router.update("foo", "AHhuQ8zFQJdT8l42Axxc6m6kNwI", "MAANkj30tnFvmoh7FsjVFr+cmcs", "2011-12-16 15:11:34", "77.183.225.114", "24051", "24052") self.assertEqual( router.id_hex, "$00786E43CCC5409753F25E36031C5CEA6EA43702" ) # we assert this twice to cover the cached + uncached cases self.assertTrue(isinstance(router.modified, datetime)) self.assertTrue(isinstance(router.modified, datetime)) self.assertEqual(router.policy, '') def test_unique_name(self): controller = object() router = Router(controller) router.update("foo", "AHhuQ8zFQJdT8l42Axxc6m6kNwI", "MAANkj30tnFvmoh7FsjVFr+cmcs", "2011-12-16 15:11:34", "77.183.225.114", "24051", "24052") self.assertEqual( router.id_hex, "$00786E43CCC5409753F25E36031C5CEA6EA43702" ) self.assertEqual( router.unique_name, "$00786E43CCC5409753F25E36031C5CEA6EA43702" ) router.flags = ['Named'] self.assertEqual(router.unique_name, "foo") def test_flags(self): controller = object() router = Router(controller) router.update("foo", "AHhuQ8zFQJdT8l42Axxc6m6kNwI", "MAANkj30tnFvmoh7FsjVFr+cmcs", "2011-12-16 15:11:34", "77.183.225.114", "24051", "24052") router.flags = "Exit Fast Named Running V2Dir Valid".split() self.assertEqual(router.name_is_unique, True) def test_flags_from_string(self): controller = object() router = Router(controller) router.update("foo", "AHhuQ8zFQJdT8l42Axxc6m6kNwI", "MAANkj30tnFvmoh7FsjVFr+cmcs", "2011-12-16 15:11:34", "77.183.225.114", "24051", "24052") router.flags = "Exit Fast Named Running V2Dir Valid" self.assertEqual(router.name_is_unique, True) def test_policy_accept(self): controller = object() router = Router(controller) router.update("foo", "AHhuQ8zFQJdT8l42Axxc6m6kNwI", "MAANkj30tnFvmoh7FsjVFr+cmcs", "2011-12-16 15:11:34", "77.183.225.114", "24051", "24052") router.policy = "accept 25,128-256".split() self.assertTrue(router.accepts_port(25)) for x in range(128, 256): self.assertTrue(router.accepts_port(x)) self.assertTrue(not router.accepts_port(26)) self.assertEqual(router.policy, 'accept 25,128-256') def test_policy_reject(self): controller = object() router = Router(controller) router.update("foo", "AHhuQ8zFQJdT8l42Axxc6m6kNwI", "MAANkj30tnFvmoh7FsjVFr+cmcs", "2011-12-16 15:11:34", "77.183.225.114", "24051", "24052") router.policy = "reject 500-600,655,7766".split() for x in range(1, 500): self.assertTrue(router.accepts_port(x)) for x in range(500, 601): self.assertTrue(not router.accepts_port(x)) self.assertEqual(router.policy, 'reject 500-600,655,7766') def test_countrycode(self): class CountryCodeController(object): def get_info_raw(self, i): return defer.succeed( '250-ip-to-country/127.1.2.3=ZZ\r\n250 OK' ) controller = CountryCodeController() router = Router(controller) router.update("foo", "AHhuQ8zFQJdT8l42Axxc6m6kNwI", "MAANkj30tnFvmoh7FsjVFr+cmcs", "2011-12-16 15:11:34", "127.1.2.3", "24051", "24052") self.assertEqual(router.location.countrycode, 'ZZ') def test_policy_error(self): router = Router(object()) try: router.policy = 'foo 123' self.fail() except Exception, e: self.assertTrue("Don't understand" in str(e)) def test_policy_not_set_error(self): router = Router(object()) try: router.accepts_port(123) self.fail() except Exception, e: self.assertTrue("policy" in str(e)) def test_repr(self): router = Router(FakeController()) router.update("foo", "AHhuQ8zFQJdT8l42Axxc6m6kNwI", "MAANkj30tnFvmoh7FsjVFr+cmcs", "2011-12-16 15:11:34", "1.2.3.4", "24051", "24052") router.flags = ['Named'] repr(router) def test_repr_no_update(self): router = Router(FakeController()) repr(router) txtorcon-0.14.2/test/test_torconfig.py0000644000175000017500000015523412627744056017763 0ustar mikemike00000000000000import os import shutil import tempfile import functools from getpass import getuser from mock import patch from StringIO import StringIO from mock import Mock, patch from zope.interface import implements from twisted.trial import unittest from twisted.test import proto_helpers from twisted.internet import defer, error, task, tcp from twisted.internet.endpoints import TCP4ServerEndpoint, serverFromString from twisted.python.failure import Failure from twisted.internet.interfaces import IReactorCore from twisted.internet.interfaces import IProtocolFactory from twisted.internet.interfaces import IProtocol from twisted.internet.interfaces import IReactorTCP from twisted.internet.interfaces import IListeningPort from twisted.internet.interfaces import IAddress from txtorcon import TorControlProtocol from txtorcon import ITorControlProtocol from txtorcon import TorConfig from txtorcon import DEFAULT_VALUE from txtorcon import HiddenService from txtorcon import launch_tor from txtorcon import TCPHiddenServiceEndpoint from txtorcon import TorNotFound from txtorcon import TCPHiddenServiceEndpointParser from txtorcon import IProgressProvider from txtorcon import torconfig from txtorcon.torconfig import TorProcessProtocol from txtorcon.util import delete_file_or_tree from txtorcon.torconfig import parse_client_keys class FakeControlProtocol: """ This is a little weird, but in most tests the answer at the top of the list is sent back immediately in an already-called Deferred. However, if the answer list is empty at the time of the call, instead the returned Deferred is added to the pending list and answer_pending() may be called to have the next Deferred fire. (see test_slutty_postbootstrap for an example). It is done this way in case we need to have some other code run between the get_conf (or whatever) and the callback -- if the Deferred is already-fired when get_conf runs, there's a Very Good Chance (always?) that the callback just runs right away. """ implements(ITorControlProtocol) # actually, just get_info_raw def __init__(self, answers): self.answers = answers self.pending = [] self.post_bootstrap = defer.succeed(self) self.on_disconnect = defer.Deferred() self.sets = [] self.events = {} #: event type -> callback self.pending_events = {} #: event type -> list self.is_owned = -1 def event_happened(self, event_type, *args): ''' Use this in your tests to send 650 events when an event-listener is added. XXX Also if we've *already* added one? Do that if there's a use-case for it ''' if event_type in self.pending_events: self.pending_events[event_type].append(args) else: self.pending_events[event_type] = [args] def answer_pending(self, answer): d = self.pending[0] self.pending = self.pending[1:] d.callback(answer) def get_info_raw(self, info): if len(self.answers) == 0: d = defer.Deferred() self.pending.append(d) return d d = defer.succeed(self.answers[0]) self.answers = self.answers[1:] return d @defer.inlineCallbacks def get_info_incremental(self, info, cb): text = yield self.get_info_raw(info) for line in text.split('\r\n'): cb(line) defer.returnValue('') # FIXME uh....what's up at torstate.py:350? def get_conf(self, info): if len(self.answers) == 0: d = defer.Deferred() self.pending.append(d) return d d = defer.succeed(self.answers[0]) self.answers = self.answers[1:] return d get_conf_raw = get_conf # up to test author ensure the answer is a raw string def set_conf(self, *args): for i in range(0, len(args), 2): self.sets.append((args[i], args[i + 1])) return defer.succeed('') def add_event_listener(self, nm, cb): self.events[nm] = cb if nm in self.pending_events: for event in self.pending_events[nm]: cb(*event) def remove_event_listener(self, nm, cb): del self.events[nm] class CheckAnswer: def __init__(self, test, ans): self.answer = ans self.test = test def __call__(self, x): self.test.assertEqual(x, self.answer) class ConfigTests(unittest.TestCase): """ FIXME hmm, this all seems a little convoluted to test errors? Maybe not that bad. """ def setUp(self): self.protocol = FakeControlProtocol([]) def test_boolean_parse_error(self): self.protocol.answers.append('config/names=\nfoo Boolean') self.protocol.answers.append({'foo': 'bar'}) cfg = TorConfig(self.protocol) return self.assertFailure(cfg.post_bootstrap, ValueError) def test_contains(self): cfg = TorConfig() cfg.ControlPort = 4455 self.assertTrue('ControlPort' in cfg) def test_boolean_parser(self): self.protocol.answers.append('config/names=\nfoo Boolean\nbar Boolean') self.protocol.answers.append({'foo': '0'}) self.protocol.answers.append({'bar': '1'}) # FIXME does a Tor controller only ever send "0" and "1" for # true/false? Or do we need to accept others? conf = TorConfig(self.protocol) self.assertTrue(conf.foo is False) self.assertTrue(conf.bar is True) def test_boolean_auto_parser(self): self.protocol.answers.append( 'config/names=\nfoo Boolean+Auto\nbar Boolean+Auto\nbaz Boolean+Auto' ) self.protocol.answers.append({'foo': '0'}) self.protocol.answers.append({'bar': '1'}) self.protocol.answers.append({'baz': 'auto'}) conf = TorConfig(self.protocol) self.assertTrue(conf.foo is 0) self.assertTrue(conf.bar is 1) self.assertTrue(conf.baz is -1) def test_string_parser(self): self.protocol.answers.append('config/names=\nfoo String') self.protocol.answers.append({'foo': 'bar'}) conf = TorConfig(self.protocol) self.assertEqual(conf.foo, 'bar') def test_int_parser(self): self.protocol.answers.append('config/names=\nfoo Integer') self.protocol.answers.append({'foo': '123'}) conf = TorConfig(self.protocol) self.assertEqual(conf.foo, 123) def test_int_parser_error(self): self.protocol.answers.append('config/names=\nfoo Integer') self.protocol.answers.append({'foo': '123foo'}) cfg = TorConfig(self.protocol) self.assertFailure(cfg.post_bootstrap, ValueError) def test_int_parser_error_2(self): self.protocol.answers.append('config/names=\nfoo Integer') self.protocol.answers.append({'foo': '1.23'}) cfg = TorConfig(self.protocol) return self.assertFailure(cfg.post_bootstrap, ValueError) def test_linelist_parser(self): self.protocol.answers.append('config/names=\nfoo LineList') self.protocol.answers.append({'foo': 'bar\nbaz'}) conf = TorConfig(self.protocol) self.assertEqual(conf.foo, ['bar', 'baz']) def test_listlist_parser_with_list(self): self.protocol.answers.append('config/names=\nfoo LineList') self.protocol.answers.append({'foo': [1, 2, 3]}) conf = TorConfig(self.protocol) self.assertEqual(conf.foo, ['1', '2', '3']) def test_float_parser(self): self.protocol.answers.append('config/names=\nfoo Float') self.protocol.answers.append({'foo': '1.23'}) conf = TorConfig(self.protocol) self.assertEqual(conf.foo, 1.23) def test_float_parser_error(self): self.protocol.answers.append('config/names=\nfoo Float') self.protocol.answers.append({'foo': '1.23fff'}) cfg = TorConfig(self.protocol) return self.assertFailure(cfg.post_bootstrap, ValueError) def test_list(self): self.protocol.answers.append('config/names=\nbing CommaList') self.protocol.answers.append({'bing': 'foo,bar,baz'}) conf = TorConfig(self.protocol) self.assertEqual(conf.config['bing'], ['foo', 'bar', 'baz']) # self.assertEqual(conf.bing, ['foo','bar','baz']) def test_single_list(self): self.protocol.answers.append('config/names=\nbing CommaList') self.protocol.answers.append({'bing': 'foo'}) conf = TorConfig(self.protocol) self.assertTrue(conf.post_bootstrap.called) self.assertEqual(conf.config['bing'], ['foo']) def test_multi_list_space(self): self.protocol.answers.append('config/names=\nbing CommaList') self.protocol.answers.append({'bing': 'foo, bar , baz'}) conf = TorConfig(self.protocol) self.assertEqual(conf.bing, ['foo', 'bar', 'baz']) def test_descriptor_access(self): self.protocol.answers.append('config/names=\nbing CommaList') self.protocol.answers.append({'bing': 'foo,bar'}) conf = TorConfig(self.protocol) self.assertEqual(conf.config['bing'], ['foo', 'bar']) self.assertEqual(conf.bing, ['foo', 'bar']) self.protocol.answers.append('250 OK') conf.bing = ['a', 'b'] self.assertEqual(conf.bing, ['foo', 'bar']) d = conf.save() def confirm(conf): self.assertEqual(conf.config['bing'], ['a', 'b']) self.assertEqual(conf.bing, ['a', 'b']) d.addCallbacks(confirm, self.fail) return d def test_unknown_descriptor(self): self.protocol.answers.append('config/names=\nbing CommaList') self.protocol.answers.append({'bing': 'foo'}) conf = TorConfig(self.protocol) try: conf.foo self.assertTrue(False) except KeyError, e: self.assertTrue('foo' in str(e)) def test_invalid_parser(self): self.protocol.answers.append( 'config/names=\nSomethingExciting NonExistantParserType' ) cfg = TorConfig(self.protocol) return self.assertFailure(cfg.post_bootstrap, RuntimeError) def test_iteration(self): conf = TorConfig() conf.SOCKSPort = 9876 conf.save() x = list(conf) self.assertEqual(x, ['SOCKSPort']) conf.save() def test_get_type(self): self.protocol.answers.append( 'config/names=\nSomethingExciting CommaList\nHiddenServices Dependant' ) self.protocol.answers.append({'SomethingExciting': 'a,b'}) conf = TorConfig(self.protocol) from txtorcon.torconfig import CommaList, HiddenService self.assertEqual(conf.get_type('SomethingExciting'), CommaList) self.assertEqual(conf.get_type('HiddenServices'), HiddenService) def test_immediate_hiddenservice_append(self): '''issue #88. we check that a .append(hs) works on a blank TorConfig''' conf = TorConfig() hs = HiddenService(conf, '/dev/null', ['80 127.0.0.1:1234']) conf.HiddenServices.append(hs) self.assertEqual(len(conf.HiddenServices), 1) self.assertEqual(conf.HiddenServices[0], hs) def foo(self, *args): print "FOOO", args def test_slutty_postbootstrap(self): # test that doPostbootstrap still works in "slutty" mode self.protocol.answers.append('config/names=\nORPort Port') # we can't answer right away, or we do all the _do_setup # callbacks before _setup_ is set -- but we need to do an # answer callback after that to trigger this bug conf = TorConfig(self.protocol) self.assertTrue('_setup_' in conf.__dict__) self.protocol.answer_pending({'ORPort': 1}) def test_immediate_bootstrap(self): self.protocol.post_bootstrap = None self.protocol.answers.append('config/names=\nfoo Boolean') self.protocol.answers.append({'foo': '0'}) conf = TorConfig(self.protocol) self.assertTrue('foo' in conf.config) def test_multiple_orports(self): self.protocol.post_bootstrap = None self.protocol.answers.append('config/names=\nOrPort CommaList') self.protocol.answers.append({'OrPort': '1234'}) conf = TorConfig(self.protocol) conf.OrPort = ['1234', '4321'] conf.save() self.assertEqual(self.protocol.sets, [('OrPort', '1234'), ('OrPort', '4321')]) def test_set_multiple(self): self.protocol.answers.append('config/names=\nAwesomeKey String') self.protocol.answers.append({'AwesomeKey': 'foo'}) conf = TorConfig(self.protocol) conf.awesomekey conf.awesomekey = 'baz' self.assertTrue(conf.needs_save()) conf.awesomekey = 'nybble' conf.awesomekey = 'pac man' conf.save() self.assertEqual(len(self.protocol.sets), 1) self.assertEqual(self.protocol.sets[0], ('AwesomeKey', 'pac man')) def test_log_double_save(self): self.protocol.answers.append( 'config/names=\nLog LineList\nFoo String''' ) self.protocol.answers.append( {'Log': 'notice file /var/log/tor/notices.log'} ) self.protocol.answers.append({'Foo': 'foo'}) conf = TorConfig(self.protocol) conf.log.append('info file /tmp/foo.log') conf.foo = 'bar' self.assertTrue(conf.needs_save()) conf.save() conf.save() # just for the code coverage... self.assertTrue(not conf.needs_save()) self.protocol.sets = [] conf.save() self.assertEqual(self.protocol.sets, []) def test_set_save_modify(self): self.protocol.answers.append('config/names=\nLog LineList') self.protocol.answers.append( {'Log': 'notice file /var/log/tor/notices.log'} ) conf = TorConfig(self.protocol) conf.log = [] self.assertTrue(conf.needs_save()) conf.save() conf.log.append('notice file /tmp/foo.log') self.assertTrue(conf.needs_save()) def test_proper_sets(self): self.protocol.answers.append('config/names=\nLog LineList') self.protocol.answers.append({'Log': 'foo'}) conf = TorConfig(self.protocol) conf.log.append('bar') conf.save() self.assertEqual(len(self.protocol.sets), 2) self.assertEqual(self.protocol.sets[0], ('Log', 'foo')) self.assertEqual(self.protocol.sets[1], ('Log', 'bar')) @defer.inlineCallbacks def test_attach_protocol(self): self.protocol.answers.append('config/names=\nLog LineList') self.protocol.answers.append({'Log': 'foo'}) conf = TorConfig() d = conf.attach_protocol(self.protocol) yield d conf.log.append('bar') yield conf.save() self.assertEqual(len(self.protocol.sets), 2) self.assertEqual(self.protocol.sets[0], ('Log', 'foo')) self.assertEqual(self.protocol.sets[1], ('Log', 'bar')) def test_attach_protocol_but_already_have_one(self): conf = TorConfig(self.protocol) self.assertRaises(RuntimeError, conf.attach_protocol, self.protocol) def test_no_confchanged_event(self): conf = TorConfig(self.protocol) self.protocol.add_event_listener = Mock(side_effect=RuntimeError) d = defer.Deferred() self.protocol.get_info_raw = Mock(return_value=d) conf.bootstrap() # this should log a message, do we really care what? def test_attribute_access(self): conf = TorConfig(self.protocol) self.assertNotIn('_slutty_', conf.__dict__) self.assertNotIn('foo', conf) class LogTests(unittest.TestCase): def setUp(self): self.protocol = FakeControlProtocol([]) self.protocol.answers.append('config/names=\nLog LineList''') self.protocol.answers.append( {'Log': 'notice file /var/log/tor/notices.log'} ) def test_log_set(self): conf = TorConfig(self.protocol) conf.log.append('info file /tmp/foo.log') self.assertTrue(conf.needs_save()) conf.save() self.assertEqual( self.protocol.sets[0], ('Log', 'notice file /var/log/tor/notices.log') ) self.assertEqual( self.protocol.sets[1], ('Log', 'info file /tmp/foo.log') ) def test_log_set_capital(self): conf = TorConfig(self.protocol) conf.Log.append('info file /tmp/foo.log') self.assertTrue(conf.needs_save()) conf.save() self.assertEqual( self.protocol.sets[0], ('Log', 'notice file /var/log/tor/notices.log') ) self.assertEqual( self.protocol.sets[1], ('Log', 'info file /tmp/foo.log') ) def test_log_set_index(self): conf = TorConfig(self.protocol) conf.log[0] = 'info file /tmp/foo.log' self.assertTrue(conf.needs_save()) conf.save() self.assertEqual( self.protocol.sets[0], ('Log', 'info file /tmp/foo.log') ) def test_log_set_slice(self): conf = TorConfig(self.protocol) conf.log[0:1] = ['info file /tmp/foo.log'] self.assertTrue(conf.needs_save()) conf.save() self.assertEqual(1, len(self.protocol.sets)) self.assertEqual( self.protocol.sets[0], ('Log', 'info file /tmp/foo.log') ) def test_log_set_pop(self): conf = TorConfig(self.protocol) self.assertEqual(len(conf.log), 1) conf.log.pop() self.assertTrue(conf.needs_save()) conf.save() self.assertEqual(len(conf.log), 0) self.assertEqual(len(self.protocol.sets), 0) def test_log_set_extend(self): conf = TorConfig(self.protocol) self.assertEqual(len(conf.log), 1) conf.log.extend(['info file /tmp/foo']) self.assertTrue(conf.needs_save()) conf.save() self.assertEqual(len(conf.log), 2) self.assertEqual(len(self.protocol.sets), 2) self.assertEqual( self.protocol.sets[0], ('Log', 'notice file /var/log/tor/notices.log') ) self.assertEqual( self.protocol.sets[1], ('Log', 'info file /tmp/foo') ) def test_log_set_insert(self): conf = TorConfig(self.protocol) self.assertEqual(len(conf.log), 1) conf.log.insert(0, 'info file /tmp/foo') self.assertTrue(conf.needs_save()) conf.save() self.assertEqual(len(conf.log), 2) self.assertEqual(len(self.protocol.sets), 2) self.assertEqual( self.protocol.sets[1], ('Log', 'notice file /var/log/tor/notices.log') ) self.assertEqual( self.protocol.sets[0], ('Log', 'info file /tmp/foo') ) def test_log_set_remove(self): conf = TorConfig(self.protocol) self.assertEqual(len(conf.log), 1) conf.log.remove('notice file /var/log/tor/notices.log') self.assertTrue(conf.needs_save()) conf.save() self.assertEqual(len(conf.log), 0) self.assertEqual(len(self.protocol.sets), 0) def test_log_set_multiple(self): conf = TorConfig(self.protocol) self.assertEqual(len(conf.log), 1) conf.log[0] = 'foo' self.assertTrue(conf.needs_save()) conf.log[0] = 'heavy' conf.log[0] = 'round' conf.save() self.assertEqual(len(self.protocol.sets), 1) self.assertEqual(self.protocol.sets[0], ('Log', 'round')) def test_set_wrong_object(self): conf = TorConfig(self.protocol) self.assertTrue(conf.post_bootstrap.called) try: conf.log = ('this', 'is', 'a', 'tuple') self.fail() except ValueError, e: self.assertTrue('Not valid' in str(e)) class EventTests(unittest.TestCase): def test_conf_changed(self): control = FakeControlProtocol([]) config = TorConfig(control) self.assertTrue('CONF_CHANGED' in control.events) control.events['CONF_CHANGED']('Foo=bar\nBar') self.assertEqual(len(config.config), 2) self.assertEqual(config.Foo, 'bar') self.assertEqual(config.Bar, DEFAULT_VALUE) class CreateTorrcTests(unittest.TestCase): def test_create_torrc(self): config = TorConfig() config.SocksPort = 1234 config.hiddenservices = [ HiddenService(config, '/some/dir', '80 127.0.0.1:1234', 'auth', 2, True) ] config.Log = ['80 127.0.0.1:80', '90 127.0.0.1:90'] config.save() torrc = config.create_torrc() lines = torrc.split('\n') lines.sort() torrc = '\n'.join(lines).strip() self.assertEqual(torrc, '''HiddenServiceAuthorizeClient auth HiddenServiceDir /some/dir HiddenServicePort 80 127.0.0.1:1234 HiddenServiceVersion 2 Log 80 127.0.0.1:80 Log 90 127.0.0.1:90 SocksPort 1234''') class HiddenServiceTests(unittest.TestCase): def setUp(self): self.protocol = FakeControlProtocol([]) self.protocol.answers.append('''config/names= HiddenServiceOptions Virtual HiddenServiceVersion Dependant HiddenServiceDirGroupReadable Dependant HiddenServiceAuthorizeClient Dependant''') @defer.inlineCallbacks def test_options_hidden(self): self.protocol.answers.append( 'HiddenServiceDir=/fake/path\nHiddenServicePort=80 ' '127.0.0.1:1234\nHiddenServiceDirGroupReadable=1\n' ) conf = TorConfig(self.protocol) yield conf.post_bootstrap self.assertTrue(conf.post_bootstrap.called) self.assertTrue('HiddenServiceOptions' not in conf.config) self.assertTrue('HiddenServices' in conf.config) self.assertEqual(len(conf.HiddenServices), 1) self.assertTrue(not conf.needs_save()) conf.hiddenservices.append( HiddenService(conf, '/some/dir', '80 127.0.0.1:2345', 'auth', 2, True) ) conf.hiddenservices[0].ports.append('443 127.0.0.1:443') self.assertTrue(conf.needs_save()) conf.save() self.assertEqual(len(self.protocol.sets), 9) self.assertEqual(self.protocol.sets[0], ('HiddenServiceDir', '/fake/path')) self.assertEqual(self.protocol.sets[1], ('HiddenServiceDirGroupReadable', '1')) self.assertEqual(self.protocol.sets[2], ('HiddenServicePort', '80 127.0.0.1:1234')) self.assertEqual(self.protocol.sets[3], ('HiddenServicePort', '443 127.0.0.1:443')) self.assertEqual(self.protocol.sets[4], ('HiddenServiceDir', '/some/dir')) self.assertEqual(self.protocol.sets[5], ('HiddenServiceDirGroupReadable', '1')) self.assertEqual(self.protocol.sets[6], ('HiddenServicePort', '80 127.0.0.1:2345')) self.assertEqual(self.protocol.sets[7], ('HiddenServiceVersion', '2')) self.assertEqual(self.protocol.sets[8], ('HiddenServiceAuthorizeClient', 'auth')) def test_save_no_protocol(self): conf = TorConfig() conf.HiddenServices = [HiddenService(conf, '/fake/path', ['80 127.0.0.1:1234'])] conf.save() def test_two_hidden_services_before_save(self): conf = TorConfig() conf.HiddenServices = [HiddenService(conf, '/fake/path', ['80 127.0.0.1:1234'])] conf.HiddenServices.append(HiddenService(conf, '/fake/path/two', ['1234 127.0.0.1:1234'])) conf.save() self.assertEqual(2, len(conf.HiddenServices)) def test_onion_keys(self): # FIXME test without crapping on filesystem self.protocol.answers.append('HiddenServiceDir=/fake/path\n') d = tempfile.mkdtemp() try: with open(os.path.join(d, 'hostname'), 'w') as f: f.write('public') with open(os.path.join(d, 'private_key'), 'w') as f: f.write('private') with open(os.path.join(d, 'client_keys'), 'w') as f: f.write('client-name hungry\ndescriptor-cookie omnomnom\n') conf = TorConfig(self.protocol) hs = HiddenService(conf, d, []) self.assertEqual(hs.hostname, 'public') self.assertEqual(hs.private_key, 'private') self.assertEqual(len(hs.client_keys), 1) self.assertEqual(hs.client_keys[0].name, 'hungry') self.assertEqual(hs.client_keys[0].cookie, 'omnomnom') self.assertEqual(hs.client_keys[0].key, None) finally: shutil.rmtree(d, ignore_errors=True) def test_modify_hidden_service(self): self.protocol.answers.append('HiddenServiceDir=/fake/path\nHiddenServicePort=80 127.0.0.1:1234\n') conf = TorConfig(self.protocol) conf.hiddenservices[0].version = 3 self.assertTrue(conf.needs_save()) def test_add_hidden_service_to_empty_config(self): conf = TorConfig() h = HiddenService(conf, '/fake/path', ['80 127.0.0.1:1234'], '', 3) conf.HiddenServices.append(h) self.assertEqual(len(conf.hiddenservices), 1) self.assertEqual(h, conf.hiddenservices[0]) self.assertTrue(conf.needs_save()) def test_multiple_append(self): conf = TorConfig() h0 = HiddenService(conf, '/fake/path', ['80 127.0.0.1:1234'], '', 3) h1 = HiddenService(conf, '/fake/path', ['90 127.0.0.1:4321'], '', 3) h2 = HiddenService(conf, '/fake/path', ['90 127.0.0.1:5432'], '', 3, True) conf.hiddenservices = [h0] conf.hiddenservices.append(h1) conf.hiddenservices.append(h2) self.assertEqual(len(conf.hiddenservices), 3) self.assertEqual(h0, conf.hiddenservices[0]) self.assertEqual(h1, conf.hiddenservices[1]) self.assertEqual(h2, conf.hiddenservices[2]) self.assertTrue(conf.needs_save()) def test_multiple_startup_services(self): conf = TorConfig(FakeControlProtocol(['config/names='])) conf._setup_hidden_services('''HiddenServiceDir=/fake/path HiddenServicePort=80 127.0.0.1:1234 HiddenServiceVersion=2 HiddenServiceAuthorizeClient=basic HiddenServiceDir=/some/other/fake/path HiddenServicePort=80 127.0.0.1:1234 HiddenServicePort=90 127.0.0.1:2345''') self.assertEqual(len(conf.hiddenservices), 2) self.assertEqual(conf.hiddenservices[0].dir, '/fake/path') self.assertEqual(conf.hiddenservices[0].version, 2) self.assertEqual(len(conf.hiddenservices[0].authorize_client), 1) self.assertEqual(conf.hiddenservices[0].authorize_client[0], 'basic') self.assertEqual(len(conf.hiddenservices[0].ports), 1) self.assertEqual(conf.hiddenservices[0].ports[0], '80 127.0.0.1:1234') self.assertEqual(conf.hiddenservices[1].dir, '/some/other/fake/path') self.assertEqual(len(conf.hiddenservices[1].ports), 2) self.assertEqual(conf.hiddenservices[1].ports[0], '80 127.0.0.1:1234') self.assertEqual(conf.hiddenservices[1].ports[1], '90 127.0.0.1:2345') def test_hidden_service_parse_error(self): conf = TorConfig(FakeControlProtocol(['config/names='])) try: conf._setup_hidden_services('''FakeHiddenServiceKey=foo''') self.fail() except RuntimeError, e: self.assertTrue('parse' in str(e)) def test_hidden_service_directory_absolute_path(self): conf = TorConfig(FakeControlProtocol(['config/names='])) conf._setup_hidden_services('HiddenServiceDir=/fake/path/../path') self.assertEqual(len(self.flushWarnings()), 1) def test_hidden_service_same_directory(self): conf = TorConfig(FakeControlProtocol(['config/names='])) servicelines = '''HiddenServiceDir=/fake/path HiddenServiceDir=/fake/path''' self.assertRaises(RuntimeError, conf._setup_hidden_services, servicelines) conf = TorConfig() conf.HiddenServices = [HiddenService(conf, '/fake/path', ['80 127.0.0.1:1234'])] conf.HiddenServices.append(HiddenService(conf, '/fake/path', ['80 127.0.0.1:1234'])) self.assertTrue(conf.needs_save()) self.assertRaises(RuntimeError, conf.save) conf = TorConfig() conf.HiddenServices = [HiddenService(conf, '/fake/path', ['80 127.0.0.1:1234'])] conf.HiddenServices.append(HiddenService(conf, '/fake/path/two', ['80 127.0.0.1:1234'])) self.assertTrue(conf.needs_save()) conf.save() conf.hiddenservices[1].dir = '/fake/path' self.assertTrue(conf.needs_save()) self.assertRaises(RuntimeError, conf.save) def test_multiple_modify_hidden_service(self): self.protocol.answers.append('HiddenServiceDir=/fake/path\nHiddenServicePort=80 127.0.0.1:1234\n') conf = TorConfig(self.protocol) self.assertTrue(self.protocol.post_bootstrap.called) self.assertTrue(conf.post_bootstrap is None or conf.post_bootstrap.called) self.assertEqual(len(conf.hiddenservices), 1) self.assertTrue(conf.hiddenservices[0].conf) conf.hiddenservices[0].version = 3 self.assertTrue(conf.needs_save()) conf.hiddenservices[0].version = 4 conf.hiddenservices[0].version = 5 self.assertEqual(conf.hiddenservices[0].version, 5) conf.save() self.assertEqual(len(self.protocol.sets), 3) self.assertEqual(self.protocol.sets[0], ('HiddenServiceDir', '/fake/path')) self.assertEqual(self.protocol.sets[1], ('HiddenServicePort', '80 127.0.0.1:1234')) self.assertEqual(self.protocol.sets[2], ('HiddenServiceVersion', '5')) def test_set_save_modify(self): self.protocol.answers.append('') conf = TorConfig(self.protocol) conf.hiddenservices = [HiddenService(conf, '/fake/path', ['80 127.0.0.1:1234'], '', 3)] self.assertTrue(conf.needs_save()) conf.save() self.assertEqual(len(conf.hiddenservices), 1) self.assertEqual(conf.hiddenservices[0].dir, '/fake/path') self.assertEqual(conf.hiddenservices[0].version, 3) self.assertEqual(0, len(conf.hiddenservices[0].authorize_client)) conf.hiddenservices[0].ports = ['123 127.0.0.1:4321'] conf.save() self.assertTrue(not conf.needs_save()) conf.hiddenservices[0].ports.append('90 127.0.0.1:2345') self.assertTrue(conf.needs_save()) class FakeReactor(task.Clock): implements(IReactorCore) def __init__(self, test, trans, on_protocol): super(FakeReactor, self).__init__() self.test = test self.transport = trans self.on_protocol = on_protocol def spawnProcess(self, processprotocol, bin, args, env, path, uid=None, gid=None, usePTY=None, childFDs=None): self.protocol = processprotocol self.protocol.makeConnection(self.transport) self.transport.process_protocol = processprotocol self.on_protocol(self.protocol) return self.transport def addSystemEventTrigger(self, *args): self.test.assertEqual(args[0], 'before') self.test.assertEqual(args[1], 'shutdown') # we know this is just for the temporary file cleanup, so we # nuke it right away to avoid polluting /tmp by calling the # callback now. args[2]() def removeSystemEventTrigger(self, id): pass class FakeProcessTransport(proto_helpers.StringTransportWithDisconnection): pid = -1 def signalProcess(self, signame): self.process_protocol.processEnded( Failure(error.ProcessTerminated(signal=signame)) ) def closeStdin(self): self.protocol.dataReceived('250 OK\r\n') self.protocol.dataReceived('250 OK\r\n') self.protocol.dataReceived('250 OK\r\n') self.protocol.dataReceived( '650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=90 ' 'TAG=circuit_create SUMMARY="Establishing a Tor circuit"\r\n' ) self.protocol.dataReceived( '650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=100 ' 'TAG=done SUMMARY="Done"\r\n' ) class FakeProcessTransportNeverBootstraps(FakeProcessTransport): pid = -1 def closeStdin(self): self.protocol.dataReceived('250 OK\r\n') self.protocol.dataReceived('250 OK\r\n') self.protocol.dataReceived('250 OK\r\n') self.protocol.dataReceived( '650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=90 TAG=circuit_create ' 'SUMMARY="Establishing a Tor circuit"\r\n') class FakeProcessTransportNoProtocol(FakeProcessTransport): def closeStdin(self): pass class LaunchTorTests(unittest.TestCase): def setUp(self): self.protocol = TorControlProtocol() self.protocol.connectionMade = lambda: None self.transport = proto_helpers.StringTransport() self.protocol.makeConnection(self.transport) self.clock = task.Clock() def setup_complete_with_timer(self, proto): proto._check_timeout.stop() proto.checkTimeout() def setup_complete_no_errors(self, proto, config, stdout, stderr): self.assertEqual("Bootstrapped 100%\n", stdout.getvalue()) self.assertEqual("", stderr.getvalue()) todel = proto.to_delete self.assertTrue(len(todel) > 0) # ...because we know it's a TorProcessProtocol :/ proto.cleanup() self.assertEqual(len(proto.to_delete), 0) for f in todel: self.assertTrue(not os.path.exists(f)) self.assertEqual(proto._timeout_delayed_call, None) # make sure we set up the config to track the created tor # protocol connection self.assertEquals(config.protocol, proto.tor_protocol) def setup_complete_fails(self, proto, stdout, stderr): self.assertEqual("Bootstrapped 90%\n", stdout.getvalue()) self.assertEqual("", stderr.getvalue()) todel = proto.to_delete self.assertTrue(len(todel) > 0) # the "12" is just arbitrary, we check it later in the error-message proto.processEnded( Failure(error.ProcessTerminated(12, None, 'statusFIXME')) ) self.assertEqual(1, len(self.flushLoggedErrors(RuntimeError))) self.assertEqual(len(proto.to_delete), 0) for f in todel: self.assertTrue(not os.path.exists(f)) return None @patch('txtorcon.torconfig.os.geteuid') def test_basic_launch(self, geteuid): # pretend we're root to exercise the "maybe chown data dir" codepath geteuid.return_value = 0 config = TorConfig() config.ORPort = 1234 config.SOCKSPort = 9999 config.User = getuser() def connector(proto, trans): proto._set_valid_events('STATUS_CLIENT') proto.makeConnection(trans) proto.post_bootstrap.callback(proto) return proto.post_bootstrap class OnProgress: def __init__(self, test, expected): self.test = test self.expected = expected def __call__(self, percent, tag, summary): self.test.assertEqual( self.expected[0], (percent, tag, summary) ) self.expected = self.expected[1:] self.test.assertTrue('"' not in summary) self.test.assertTrue(percent >= 0 and percent <= 100) def on_protocol(proto): proto.outReceived('Bootstrapped 100%\n') proto.progress = OnProgress( self, [ (90, 'circuit_create', 'Establishing a Tor circuit'), (100, 'done', 'Done'), ] ) trans = FakeProcessTransport() trans.protocol = self.protocol fakeout = StringIO() fakeerr = StringIO() creator = functools.partial(connector, self.protocol, self.transport) d = launch_tor( config, FakeReactor(self, trans, on_protocol), connection_creator=creator, tor_binary='/bin/echo', stdout=fakeout, stderr=fakeerr ) d.addCallback(self.setup_complete_no_errors, config, fakeout, fakeerr) return d def check_setup_failure(self, fail): self.assertTrue("with error-code 12" in fail.getErrorMessage()) # cancel the errback chain, we wanted this return None def test_launch_tor_fails(self): config = TorConfig() config.OrPort = 1234 config.SocksPort = 9999 def connector(proto, trans): proto._set_valid_events('STATUS_CLIENT') proto.makeConnection(trans) proto.post_bootstrap.callback(proto) return proto.post_bootstrap def on_protocol(proto): proto.outReceived('Bootstrapped 90%\n') trans = FakeProcessTransport() trans.protocol = self.protocol fakeout = StringIO() fakeerr = StringIO() creator = functools.partial(connector, self.protocol, self.transport) d = launch_tor( config, FakeReactor(self, trans, on_protocol), connection_creator=creator, tor_binary='/bin/echo', stdout=fakeout, stderr=fakeerr ) d.addCallback(self.setup_complete_fails, fakeout, fakeerr) self.flushLoggedErrors(RuntimeError) return d def test_launch_with_timeout_no_ireactortime(self): config = TorConfig() return self.assertRaises( RuntimeError, launch_tor, config, None, timeout=5, tor_binary='/bin/echo' ) @patch('txtorcon.torconfig.sys') @patch('txtorcon.torconfig.pwd') @patch('txtorcon.torconfig.os.geteuid') @patch('txtorcon.torconfig.os.chown') def test_launch_root_changes_tmp_ownership(self, chown, euid, _pwd, _sys): _pwd.return_value = 1000 _sys.platform = 'linux2' euid.return_value = 0 config = TorConfig() config.User = 'chuffington' d = launch_tor(config, Mock(), tor_binary='/bin/echo') self.assertEqual(1, chown.call_count) @defer.inlineCallbacks def test_launch_timeout_exception(self): self.protocol = FakeControlProtocol([]) self.protocol.answers.append('''config/names= DataDirectory String ControlPort Port''') self.protocol.answers.append({'DataDirectory': 'foo'}) self.protocol.answers.append({'ControlPort': 0}) config = TorConfig(self.protocol) yield config.post_bootstrap config.DataDirectory = '/dev/null' trans = Mock() d = launch_tor( config, FakeReactor(self, trans, Mock()), tor_binary='/bin/echo' ) tpp = yield d tpp.transport = trans trans.signalProcess = Mock(side_effect=error.ProcessExitedAlready) trans.loseConnection = Mock() tpp.timeout_expired() self.assertTrue(tpp.transport.loseConnection.called) @defer.inlineCallbacks def test_launch_timeout_process_exits(self): # cover the "one more edge case" where we get a processEnded() # but we've already "done" a timeout. self.protocol = FakeControlProtocol([]) self.protocol.answers.append('''config/names= DataDirectory String ControlPort Port''') self.protocol.answers.append({'DataDirectory': 'foo'}) self.protocol.answers.append({'ControlPort': 0}) config = TorConfig(self.protocol) yield config.post_bootstrap config.DataDirectory = '/dev/null' trans = Mock() d = launch_tor( config, FakeReactor(self, trans, Mock()), tor_binary='/bin/echo' ) tpp = yield d tpp.timeout_expired() tpp.transport = trans trans.signalProcess = Mock() trans.loseConnection = Mock() status = Mock() status.value.exitCode = None self.assertTrue(tpp._did_timeout) tpp.processEnded(status) errs = self.flushLoggedErrors(RuntimeError) self.assertEqual(1, len(errs)) def test_launch_wrong_stdout(self): config = TorConfig() try: launch_tor(config, None, stdout=object(), tor_binary='/bin/echo') self.fail("Should have thrown an error") except RuntimeError: pass def test_launch_with_timeout(self): config = TorConfig() config.OrPort = 1234 config.SocksPort = 9999 timeout = 5 def connector(proto, trans): proto._set_valid_events('STATUS_CLIENT') proto.makeConnection(trans) proto.post_bootstrap.callback(proto) return proto.post_bootstrap class OnProgress: def __init__(self, test, expected): self.test = test self.expected = expected def __call__(self, percent, tag, summary): self.test.assertEqual( self.expected[0], (percent, tag, summary) ) self.expected = self.expected[1:] self.test.assertTrue('"' not in summary) self.test.assertTrue(percent >= 0 and percent <= 100) def on_protocol(proto): proto.outReceived('Bootstrapped 100%\n') trans = FakeProcessTransportNeverBootstraps() trans.protocol = self.protocol creator = functools.partial(connector, self.protocol, self.transport) react = FakeReactor(self, trans, on_protocol) d = launch_tor(config, react, connection_creator=creator, timeout=timeout, tor_binary='/bin/echo') # FakeReactor is a task.Clock subclass and +1 just to be sure react.advance(timeout + 1) self.assertTrue(d.called) self.assertTrue( d.result.getErrorMessage().strip().endswith('Tor was killed (TERM).') ) self.flushLoggedErrors(RuntimeError) return self.assertFailure(d, RuntimeError) def test_launch_with_timeout_that_doesnt_expire(self): config = TorConfig() config.OrPort = 1234 config.SocksPort = 9999 timeout = 5 def connector(proto, trans): proto._set_valid_events('STATUS_CLIENT') proto.makeConnection(trans) proto.post_bootstrap.callback(proto) return proto.post_bootstrap class OnProgress: def __init__(self, test, expected): self.test = test self.expected = expected def __call__(self, percent, tag, summary): self.test.assertEqual( self.expected[0], (percent, tag, summary) ) self.expected = self.expected[1:] self.test.assertTrue('"' not in summary) self.test.assertTrue(percent >= 0 and percent <= 100) def on_protocol(proto): proto.outReceived('Bootstrapped 100%\n') trans = FakeProcessTransport() trans.protocol = self.protocol creator = functools.partial(connector, self.protocol, self.transport) react = FakeReactor(self, trans, on_protocol) d = launch_tor(config, react, connection_creator=creator, timeout=timeout, tor_binary='/bin/echo') # FakeReactor is a task.Clock subclass and +1 just to be sure react.advance(timeout + 1) self.assertTrue(d.called) self.assertTrue(d.result.tor_protocol == self.protocol) def setup_fails_stderr(self, fail, stdout, stderr): self.assertEqual('', stdout.getvalue()) self.assertEqual('Something went horribly wrong!\n', stderr.getvalue()) self.assertTrue( 'Something went horribly wrong!' in fail.getErrorMessage() ) # cancel the errback chain, we wanted this return None def test_tor_produces_stderr_output(self): config = TorConfig() config.OrPort = 1234 config.SocksPort = 9999 def connector(proto, trans): proto._set_valid_events('STATUS_CLIENT') proto.makeConnection(trans) proto.post_bootstrap.callback(proto) return proto.post_bootstrap def on_protocol(proto): proto.errReceived('Something went horribly wrong!\n') trans = FakeProcessTransport() trans.protocol = self.protocol fakeout = StringIO() fakeerr = StringIO() creator = functools.partial(connector, self.protocol, self.transport) d = launch_tor(config, FakeReactor(self, trans, on_protocol), connection_creator=creator, tor_binary='/bin/echo', stdout=fakeout, stderr=fakeerr) d.addCallback(self.fail) # should't get callback d.addErrback(self.setup_fails_stderr, fakeout, fakeerr) self.assertFalse(self.protocol.on_disconnect) return d def test_tor_connection_fails(self): """ We fail to connect once, and then successfully connect -- testing whether we're retrying properly on each Bootstrapped line from stdout. """ config = TorConfig() config.OrPort = 1234 config.SocksPort = 9999 class Connector: count = 0 def __call__(self, proto, trans): self.count += 1 if self.count < 2: return defer.fail( error.CannotListenError(None, None, None) ) proto._set_valid_events('STATUS_CLIENT') proto.makeConnection(trans) proto.post_bootstrap.callback(proto) return proto.post_bootstrap def on_protocol(proto): proto.outReceived('Bootstrapped 90%\n') trans = FakeProcessTransport() trans.protocol = self.protocol creator = functools.partial(Connector(), self.protocol, self.transport) d = launch_tor( config, FakeReactor(self, trans, on_protocol), connection_creator=creator, tor_binary='/bin/echo' ) d.addCallback(self.setup_complete_fails) return self.assertFailure(d, Exception) def test_tor_connection_user_data_dir(self): """ Test that we don't delete a user-supplied data directory. """ config = TorConfig() config.OrPort = 1234 class Connector: def __call__(self, proto, trans): proto._set_valid_events('STATUS_CLIENT') proto.makeConnection(trans) proto.post_bootstrap.callback(proto) return proto.post_bootstrap def on_protocol(proto): proto.outReceived('Bootstrapped 90%\n') my_dir = tempfile.mkdtemp(prefix='tortmp') config.DataDirectory = my_dir trans = FakeProcessTransport() trans.protocol = self.protocol creator = functools.partial(Connector(), self.protocol, self.transport) d = launch_tor( config, FakeReactor(self, trans, on_protocol), connection_creator=creator, tor_binary='/bin/echo' ) def still_have_data_dir(proto, tester): proto.cleanup() # FIXME? not really unit-testy as this is sort of internal function tester.assertTrue(os.path.exists(my_dir)) delete_file_or_tree(my_dir) d.addCallback(still_have_data_dir, self) d.addErrback(self.fail) return d def test_tor_connection_user_control_port(self): """ Confirm we use a user-supplied control-port properly """ config = TorConfig() config.OrPort = 1234 config.ControlPort = 4321 class Connector: def __call__(self, proto, trans): proto._set_valid_events('STATUS_CLIENT') proto.makeConnection(trans) proto.post_bootstrap.callback(proto) return proto.post_bootstrap def on_protocol(proto): proto.outReceived('Bootstrapped 90%\n') proto.outReceived('Bootstrapped 100%\n') trans = FakeProcessTransport() trans.protocol = self.protocol creator = functools.partial(Connector(), self.protocol, self.transport) d = launch_tor( config, FakeReactor(self, trans, on_protocol), connection_creator=creator, tor_binary='/bin/echo' ) def check_control_port(proto, tester): # we just want to ensure launch_tor() didn't mess with # the controlport we set tester.assertEquals(config.ControlPort, 4321) d.addCallback(check_control_port, self) d.addErrback(self.fail) return d def test_tor_connection_default_control_port(self): """ Confirm a default control-port is set if not user-supplied. """ config = TorConfig() class Connector: def __call__(self, proto, trans): proto._set_valid_events('STATUS_CLIENT') proto.makeConnection(trans) proto.post_bootstrap.callback(proto) return proto.post_bootstrap def on_protocol(proto): proto.outReceived('Bootstrapped 90%\n') proto.outReceived('Bootstrapped 100%\n') trans = FakeProcessTransport() trans.protocol = self.protocol creator = functools.partial(Connector(), self.protocol, self.transport) d = launch_tor( config, FakeReactor(self, trans, on_protocol), connection_creator=creator, tor_binary='/bin/echo' ) def check_control_port(proto, tester): # ensure ControlPort was set to a default value tester.assertEquals(config.ControlPort, 9052) d.addCallback(check_control_port, self) d.addErrback(self.fail) return d def test_progress_updates(self): self.got_progress = False def confirm_progress(p, t, s): self.assertEqual(p, 10) self.assertEqual(t, 'tag') self.assertEqual(s, 'summary') self.got_progress = True process = TorProcessProtocol(None, confirm_progress) process.progress(10, 'tag', 'summary') self.assertTrue(self.got_progress) def test_status_updates(self): process = TorProcessProtocol(None) process.status_client("NOTICE CONSENSUS_ARRIVED") def test_tor_launch_success_then_shutdown(self): """ There was an error where we double-callbacked a deferred, i.e. success and then shutdown. This repeats it. """ process = TorProcessProtocol(None) process.status_client( 'STATUS_CLIENT BOOTSTRAP PROGRESS=100 TAG=foo SUMMARY=cabbage' ) self.assertEqual(None, process.connected_cb) class Value(object): exitCode = 123 class Status(object): value = Value() process.processEnded(Status()) self.assertEquals(len(self.flushLoggedErrors(RuntimeError)), 1) def test_launch_tor_no_control_port(self): ''' See Issue #80. This allows you to launch tor with a TorConfig with ControlPort=0 in case you don't want a control connection at all. In this case you get back a TorProcessProtocol and you own both pieces. (i.e. you have to kill it yourself). ''' config = TorConfig() config.ControlPort = 0 trans = FakeProcessTransportNoProtocol() trans.protocol = self.protocol def creator(*args, **kw): print "Bad: connection creator called" self.fail() def on_protocol(proto): self.process_proto = proto pp = launch_tor(config, FakeReactor(self, trans, on_protocol), connection_creator=creator, tor_binary='/bin/echo') self.assertTrue(pp.called) self.assertEqual(pp.result, self.process_proto) return pp class IteratorTests(unittest.TestCase): def test_iterate_torconfig(self): cfg = TorConfig() cfg.FooBar = 'quux' cfg.save() cfg.Quux = 'blimblam' keys = sorted([k for k in cfg]) self.assertEqual(['FooBar', 'Quux'], keys) class ErrorTests(unittest.TestCase): @patch('txtorcon.torconfig.find_tor_binary') def test_no_tor_binary(self, ftb): """FIXME: do I really need all this crap in here?""" self.transport = proto_helpers.StringTransport() config = TorConfig() d = None class Connector: def __call__(self, proto, trans): proto._set_valid_events('STATUS_CLIENT') proto.makeConnection(trans) proto.post_bootstrap.callback(proto) return proto.post_bootstrap self.protocol = FakeControlProtocol([]) torconfig.find_tor_binary = lambda: None trans = FakeProcessTransport() trans.protocol = self.protocol creator = functools.partial(Connector(), self.protocol, self.transport) try: d = launch_tor( config, FakeReactor(self, trans, lambda x: None), connection_creator=creator ) self.fail() except TorNotFound: pass # success! return d # the RSA keys have been shortened below for readability keydata = '''client-name bar descriptor-cookie O4rQyZ+IJr2PNHUdeXi0nA== client-key -----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQC1R/bPGTWnpGJpNCfT1KIfFq1QEGHz4enKSEKUDkz1CSEPOMGS bV37dfqTuI4klsFvdUsR3NpYXLin9xRWvw1viKwAN0y8cv5totl4qMxO5i+zcfVh bJiNvVv2EjfEyQaZfAy2PUfp/tAPYZMsyfps2DptWyNR -----END RSA PRIVATE KEY----- client-name foo descriptor-cookie btlj4+RsWEkxigmlszInhQ== client-key -----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDdLdHU1fbABtFutOFtpdWQdv/9qG1OAc0r1TfaBtkPSNcLezcx SThalIEnRFfejy0suOHmsqspruvn0FEflIEQvFWeXAPvXg== -----END RSA PRIVATE KEY----- client-name quux descriptor-cookie asdlkjasdlfkjalsdkfffj== ''' class HiddenServiceAuthTests(unittest.TestCase): def test_parse_client_keys(self): data = StringIO(keydata) clients = list(parse_client_keys(data)) self.assertEqual(3, len(clients)) self.assertEqual('bar', clients[0].name) self.assertEqual('O4rQyZ+IJr2PNHUdeXi0nA', clients[0].cookie) self.assertEqual('MIICXQIBAAKBgQC1R/bPGTWnpGJpNCfT1KIfFq1QEGHz4enKSEKUDkz1CSEPOMGSbV37dfqTuI4klsFvdUsR3NpYXLin9xRWvw1viKwAN0y8cv5totl4qMxO5i+zcfVhbJiNvVv2EjfEyQaZfAy2PUfp/tAPYZMsyfps2DptWyNR', clients[0].key) self.assertEqual('foo', clients[1].name) self.assertEqual('btlj4+RsWEkxigmlszInhQ', clients[1].cookie) self.assertEqual(clients[1].key, 'MIICXgIBAAKBgQDdLdHU1fbABtFutOFtpdWQdv/9qG1OAc0r1TfaBtkPSNcLezcxSThalIEnRFfejy0suOHmsqspruvn0FEflIEQvFWeXAPvXg==') self.assertEqual('quux', clients[2].name) self.assertEqual('asdlkjasdlfkjalsdkfffj', clients[2].cookie) self.assertEqual(None, clients[2].key) def test_parse_error(self): data = StringIO('client-name foo\nclient-name xxx\n') self.assertRaises( RuntimeError, parse_client_keys, data ) txtorcon-0.14.2/requirements.txt0000644000175000017500000000030512611263616016640 0ustar mikemike00000000000000## see also dev-requirements.txt to build ## hmm, travis-ci doesn't like this since we need a GeoIP-dev package ##GeoIP>=1.2.9 Twisted>=11.1.0 ipaddr>=2.1.10 zope.interface>=3.6.1 txsocksx>=1.13.0 txtorcon-0.14.2/Makefile0000644000175000017500000000702312627745163015031 0ustar mikemike00000000000000.PHONY: test html counts coverage sdist clean install doc integration default: test VERSION = 0.14.2 test: trial --reporter=text test tox: tox -i http://localhost:3141/root/pypi # see also http://docs.docker.io/en/latest/use/baseimages/ dockerbase-wheezy: @echo 'Building a minimal "wheezy" system.' @echo "This may take a while...and will consume about 240MB when done." debootstrap wheezy dockerbase-wheezy dockerbase-wheezy-image: dockerbase-wheezy @echo 'Importing dockerbase-wheezy into docker' tar -C dockerbase-wheezy -c . | docker import - dockerbase-wheezy docker run dockerbase-wheezy cat /etc/issue txtorcon-tester: Dockerfile dockerbase-wheezy-image @echo "Creating a Docker.io container" docker build -rm -q -t txtorcon-tester ./ integration: ## txtorcon-tester python integration/run.py install: sudo apt-get install python-setuptools python-twisted python-ipaddr python-geoip python-psutil graphviz python setup.py install doc: docs/*.rst cd docs && make html -cp dist/txtorcon-${VERSION}.tar.gz docs/_build/html coverage: coverage run --source=txtorcon `which trial` test coverage report --show-missing htmlcoverage: coverage run --source=txtorcon `which trial` test coverage -a -d annotated_coverage coverage report --show-missing coverage html # creates htmlcov/ sensible-browser htmlcov/index.html # dang, this is a little annoying. maybe add a shell-script which # looks for "coverage" or "python-coverage"?? coverage-debian: python-coverage run --source=txtorcon `which trial` test python-coverage -a -d annotated_coverage python-coverage report pep8: txtorcon/*.py test/*.py examples/*.py pep8 --ignore=E501 $^ pep8count: pep8 --ignore=E501,E265 $^ | wc -l pyflakes: pyflakes txtorcon/ examples/ test/ pyflakescount: pyflakes txtorcon/ examples/ | wc -l clean: -rm -rf _trial_temp -rm -rf build -rm -rf dist -rm -rf html -rm MANIFEST -rm `find . -name \*.py[co]` -cd docs && make clean -rm -rf dockerbase-wheezy -docker rmi txtorcon-tester -docker rmi dockerbase-wheezy counts: ohcount -s txtorcon/*.py test-release: dist ./test-release.sh $(shell pwd) ${VERSION} dist: dist/txtorcon-${VERSION}-py2-none-any.whl dist/txtorcon-${VERSION}.tar.gz dist-sigs: dist/txtorcon-${VERSION}-py2-none-any.whl.asc dist/txtorcon-${VERSION}.tar.gz.asc sdist: setup.py python setup.py sdist dist/txtorcon-${VERSION}-py2-none-any.whl: python setup.py bdist_wheel dist/txtorcon-${VERSION}-py2-none-any.whl.asc: dist/txtorcon-${VERSION}-py2-none-any.whl gpg --verify dist/txtorcon-${VERSION}-py2-none-any.whl.asc || gpg --no-version --detach-sign --armor --local-user meejah@meejah.ca dist/txtorcon-${VERSION}-py2-none-any.whl dist/txtorcon-${VERSION}.tar.gz: sdist dist/txtorcon-${VERSION}.tar.gz.asc: dist/txtorcon-${VERSION}.tar.gz gpg --verify dist/txtorcon-${VERSION}.tar.gz.asc || gpg --no-version --detach-sign --armor --local-user meejah@meejah.ca dist/txtorcon-${VERSION}.tar.gz release: twine upload -r pypi -c "txtorcon v${VERSION} tarball" dist/txtorcon-${VERSION}.tar.gz dist/txtorcon-${VERSION}.tar.gz.asc twine upload -r pypi -c "txtorcon v${VERSION} wheel" dist/txtorcon-${VERSION}-py2-none-any.whl dist/txtorcon-${VERSION}-py2-none-any.whl.asc venv: virtualenv --never-download --extra-search-dir=/usr/lib/python2.7/dist-packages/ venv @echo "created venv" @echo "see INSTALL for more information; to use:" @echo ". ./venv/bin/activate" @echo "pip install -r requirements.txt" @echo "pip install -r dev-requirements.txt" @echo "python examples/monitor.py" html: docs/README.rst cd docs && make html txtorcon-0.14.2/LICENSE0000644000175000017500000000204012515516430014355 0ustar mikemike00000000000000Copyright (c) 2012, 2013 meejah Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. txtorcon-0.14.2/meejah.asc0000644000175000017500000000323012312757205015275 0ustar mikemike00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Version: SKS 1.1.0 mQENBE86z3cBCADpSDuSTovhjXCzg/D4uw3ve9BIB+klOXxAXtpSwtMdfuTQrJ2aM5QkhkKK uWpmTravNM6Bg7U0qwjvjbrYKSfarDiRvCD8x7rfSnqn9EEOwtcQpVSmPqUaAF32FohHGyK1 +M3ka8TdpLwqBr2v02usWWt8IKMSiMy0d3VO6Mj2HS/9ppuYMDpthD5lttToE2gksmCA4TOL G63IfXx2C/NuVrQo+vI4FGH/UI0R+zN8ibVq+j6gj9j9awSeUEiv2nQmUBZWSFncu/FXOFxD FLXdTDbFveYYEAQTYvDNp6k8sW6YmOKRCckhIV2NCDOUHgEiKuERAd0wuna9f3ylL8F1ABEB AAG0GW1lZWphaCA8bWVlamFoQG1lZWphaC5jYT6JATgEEwECACIFAk86z3cCGwMGCwkIBwMC BhUIAgkKCwQWAgMBAh4BAheAAAoJEMJgKAMSgGmneVwIAIM6/UQGGDCwtdnCVB3YmrtHxpsC DmgRNenB95e9GNNONr0FvwgfHz2vVD3JczYy2cnFxHASBoMcreCNHqkC5sg4XTtqwLju3HaA 4bagR0e/CvyBgREar2m00uCNGcmY9vWyZOBBYXnV4aIf1sC4XQEuehjg/pbmaBdYqjVy8hUx qW6hZ36In2UcGFdWS3aT6QmAXhoxM5Yt955X2ZT5EPV6yRqjdENyhOsOtPro6fEWFGYFr4ev 3oBBEZZQFJWtJfKBPWZK/xLka4fd5IppctmymXq7/BRcTcpQaKhSHlOLjjQA+fzmjizXK9hL KQ3bWml5zSlMxBvggrQYOR9yxHa5AQ0ETzrPdwEIAMqGB9+VB1yOK6xATYRd2crvKA2EASo2 OMELsWgXnhWuZpsQj/w3IIDiK6n3M+z1AvxfU6YXZJta7obD3KZQrqAetZBHBoBT7Qgo7i5M 4/v5AVXGMlwgirlwPZ0N8KcFZn0QbyELF0acNYZ8xlUt1EQEHIKKxHThtGy8t7oJS7CMb83v NPo/E9NZ7AP1odQUD3+riADA7bPGGUksGRzKODRaOta93IhwEDNgcTRBU73/0RKS3mQTfY8f DXDUfNLtEtK8C1PtQqGcuC8zm0Kt7YK1+TPtSSbbseh36GmRTWkJ7/GYYoI4ZHjSAeJcsdkx OPs1dzEQkc+/q07WhimcRXkAEQEAAYkBHwQYAQIACQUCTzrPdwIbDAAKCRDCYCgDEoBpp+hc CACRh57atr3wUS7og3tL6NEsaa210CRUOGbU5vj1T6RJesmizG28JtfIY8oGpACAkQexOQIx BiIg9xP8tSiaJWlhrt1VVVk23O6FjBkLYraTl3h/yU+/hNFwn1zKrpRyXIiTfnO0PNe5jLeE aOuKy1E14fL8xN9c8dpJB1KxC95S1Ol+SDTEpfmY4NaRZdR8PViKxc3rJKE5sRBR6R320oEO o8DzQrkjnBRHXI6YgzvIQpLpJaMPk3826ImcCfLksID+RE73sMBEExaslGQGzUKTZEXEyk5/ 7ZAnWYZwB0CUU7QoU9NXBAtBb105fkqk+9k6p7ymANmPqwLFgwjzjArf =+73u -----END PGP PUBLIC KEY BLOCK----- txtorcon-0.14.2/TODO0000644000175000017500000001262412312757205014053 0ustar mikemike00000000000000 . strip OKs off the end of responses in TorControlProtocol -- should simplify the rest, and testing. See FIXME in at least torinfo.py:89 or so . look at get_info versus get_info_raw for entry_guards: parse keywords needs to be more smarterer . looks like ~41 hours to do full scan of 850321 combinations, at 3.5 seconds per combo and 20 outstanding requests (i.e. 20 in parallel at 3.5 seconds each). . should handle the case when routers are added/delete in exit-scanning thing. Maybe/probably add an IRouterListener that has callbacks for new and removed routers? . need test for authentication (and other) bootstrap errors -- does the Deferred from build_tor_connection get the errbacks properly? . If I want to depend on sphinx-contrib's programoutput then I can add this to README to get live results from the tests/coverage things: .. command-output:: make test :ellipsis: 0,-5 :shell: :nostderr: .. command-output:: make coverage :ellipsis: 0,-5 :shell: :nostderr: This also needs a couple changes to doc, in Makefile: test: cd .. && make test coverage: cd .. && make coverage and to conf.py, adding to extensions: 'sphinxcontrib.programoutput' . if we're going with "attribute-style access for everything" then also basically everything should be audited for this: . TorControlProtocol needs some things marked read-only? . TorState.set_attacher . put the nginx config for the hidden service somewhere in git; need to remember to change the redirect for "-latest" if releasing a new version... . Looking briefly at "chutney" (tor test-network setup thing) it seems to me an improvement on the templates would be: use txtor.TorConfig in a mode that allows one to set objects by keys (including hidden services, lists-of-strings, etc) and somewhere else is code which can start Tor based on a TorConfig -- *after* it connects, it does validation on the TorConfig by going through all the now-valid Parser objects asking them to validate the options. Then, instead of templates which "inherit" from each other and have an environment to set up, you have Python types (following Builder pattern) which represent the Tors you want to set up so you have a Relay class that has a subclass Authority whith the bonus it can override anything in Relay. They'd all implement a method in something like ITorConfigBuilder that asks it to return a config give a Node object (for example, FIXME: look up Builder again). For example: class ITorConfigBuilder(Interface): def build_config_for(self, node): """return a TorConfig object for the given Node instance""" class Relay: implements(ITorConfigBuilder) def build_config_for(self, node): config = txtor.TorConfig() config.SocksPort = 0 config.OrPort = node.or_port config.Address = node.ip config.DirPort = node.dir_port return config class Authority(Relay): implements(ITorConfigBuilder) def build_config_for(self, node): config = super(self, Relay).build_config_for(node) config.AuthoritativeDirectory 1 config.V3AuthoritativeDirectory 1 config.ContactInfo = '%d@test.test' % node.number config.ExitPolicy = 'reject *:*' return config . double-double check that i have a utest covering the case of multi-line 650 in the midst of a multi-line response OR re-verify that this can never happen -- the FSM is right now accumulating in only one buffer I believe (after simplifying in commit a62dfe0a1511eae717788732de54269920206015) . should support CIRC_MINOR event, too (in TorState) . $B56F17701DC248F8C42150942BDF7A7FAD6C6FC6~Thaolia is in one of my circuits, but not showing up in ns/all (nor via ns/id/XX). talked about it in #tor a little, but no conclusion. also tried starting up a separate Tor and that one also failed to find the key. (And possibly triggered my main Tor failing to COOKIE authenticate -- probably had the cookie file overwritten?) - it seems that streams aren't getting set up right if there is exactly one right now in tor? via telnet (url changed): getinfo stream-status 250-stream-status=123 SUCCEEDED 496 www.example.com:6667 250 OK fixed, but is this the same for circuits? Probably but hard to have precisely one circuit (still, should utest + fix) . ICircuitListener and IStreamListener are pretty complicated interfaces; might be better to make a simpler interface that is more like "pull" Observer pattern with "stateChanged(newstate, **kwargs)" or something and an interface on TorController to listen for newly created streams and circuits. Could still provide the complicated-interface via a multiplex that implemented IStreamListener and fanned out to the complicated states. This would ease live for clients merely wanting to know, e.g., when there are new circuits (or streams). (Instead, or as a stopgap, I've provided StreamListenerMixin and CircuitListenerMixin with empty default methods). . need to interrogate Tor for its bootstrap state when connection, as per control-spec.txt (e.g. post_boostrap callback shouldn't be issued until both the TorController are up and running AND Tor is fully bootstrapped, if we connected while it was still starting up). What to do if Tor starts bootstrapping (again) while we're running? txtorcon-0.14.2/txtorcon.egg-info/0000755000175000017500000000000012627745451016741 5ustar mikemike00000000000000txtorcon-0.14.2/txtorcon.egg-info/SOURCES.txt0000644000175000017500000000525612627745451020635 0ustar mikemike00000000000000INSTALL LICENSE MANIFEST.in Makefile README.rst TODO dev-requirements.txt meejah.asc requirements.txt setup.py docs/Makefile docs/README.rst docs/apilinks_sphinxext.py docs/conf.py docs/examples.rst docs/howtos.rst docs/index.rst docs/introduction.rst docs/release-checklist.rst docs/releases.rst docs/txtorcon-config.rst docs/txtorcon-endpoints.rst docs/txtorcon-interface.rst docs/txtorcon-launching.rst docs/txtorcon-protocol.rst docs/txtorcon-state.rst docs/txtorcon-util.rst docs/txtorcon.rst docs/walkthrough.rst docs/_static/avatar.png docs/_static/haiku.css docs/_static/logo.png docs/_static/logo.svg docs/_themes/README docs/_themes/alabaster/__init__.py docs/_themes/alabaster/_version.py docs/_themes/alabaster/about.html docs/_themes/alabaster/donate.html docs/_themes/alabaster/layout.html docs/_themes/alabaster/navigation.html docs/_themes/alabaster/support.py docs/_themes/alabaster/theme.conf docs/_themes/alabaster/static/alabaster.css_t docs/_themes/alabaster/static/pygments.css examples/attach_streams_by_country.py examples/circuit_failure_rates.py examples/circuit_for_next_stream.py examples/disallow_streams_by_port.py examples/dump_config.py examples/ephemeral_endpoint.py examples/hello_darkweb.py examples/hidden-service-systemd.service examples/hidden_echo.py examples/launch_tor.py examples/launch_tor2web.py examples/launch_tor_endpoint.py examples/launch_tor_endpoint2.py examples/launch_tor_with_hiddenservice.py examples/launch_tor_with_simplehttpd.py examples/list_circuits.py examples/minimal_endpoint.py examples/monitor.py examples/multiple-socks-ports.py examples/redirect_streams.py examples/schedule_bandwidth.py examples/stem_relay_descriptor.py examples/stream_circuit_logger.py examples/systemd.service examples/tor_info.py examples/torflow_path_selection.py examples/txtorcon.tac examples/webui_server.py scripts/asciinema-demo0.py test/__init__.py test/profile_startup.py test/test_addrmap.py test/test_circuit.py test/test_endpoints.py test/test_fsm.py test/test_log.py test/test_router.py test/test_stream.py test/test_torconfig.py test/test_torcontrolprotocol.py test/test_torinfo.py test/test_torstate.py test/test_util.py test/test_util_imports.py test/util.py twisted/plugins/txtorcon_endpoint_parser.py txtorcon/__init__.py txtorcon/addrmap.py txtorcon/circuit.py txtorcon/endpoints.py txtorcon/interface.py txtorcon/log.py txtorcon/router.py txtorcon/spaghetti.py txtorcon/stream.py txtorcon/torconfig.py txtorcon/torcontrolprotocol.py txtorcon/torinfo.py txtorcon/torstate.py txtorcon/util.py txtorcon.egg-info/PKG-INFO txtorcon.egg-info/SOURCES.txt txtorcon.egg-info/dependency_links.txt txtorcon.egg-info/pbr.json txtorcon.egg-info/requires.txt txtorcon.egg-info/top_level.txttxtorcon-0.14.2/txtorcon.egg-info/top_level.txt0000644000175000017500000000002112627745450021463 0ustar mikemike00000000000000txtorcon twisted txtorcon-0.14.2/txtorcon.egg-info/PKG-INFO0000644000175000017500000002424312627745450020042 0ustar mikemike00000000000000Metadata-Version: 1.1 Name: txtorcon Version: 0.14.2 Summary: Twisted-based Tor controller client, with state-tracking and configuration abstractions. Home-page: https://github.com/meejah/txtorcon Author: meejah Author-email: meejah@meejah.ca License: MIT Description: README ====== Documentation at https://txtorcon.readthedocs.org .. image:: https://travis-ci.org/meejah/txtorcon.png?branch=master :target: https://www.travis-ci.org/meejah/txtorcon .. image:: https://coveralls.io/repos/meejah/txtorcon/badge.png :target: https://coveralls.io/r/meejah/txtorcon .. image:: http://api.flattr.com/button/flattr-badge-large.png :target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub quick start ----------- For the impatient, there are two quick ways to install this:: $ pip install txtorcon ... or, if you checked out or downloaded the source:: $ python setup.py install ... or, better yet, use a virtualenv and the dev requirements:: $ virtualenv venv $ ./venv/bin/pip install -e .[dev] For OSX, we can install txtorcon with the help of easy_install:: $ easy_install txtorcon To avoid installing, you can just add the base of the source to your PYTHONPATH:: $ export PYTHONPATH=`pwd`:$PYTHONPATH Then, you will want to explore the examples. Try "python examples/stream\_circuit\_logger.py" for instance. On Debian testing (jessie), or with wheezy-backports (big thanks to Lunar^ for all his packaging work) you can install easily:: $ apt-get install python-txtorcon You may also like `this asciinema demo `_ for an overview. Tor configuration ----------------- You'll want to have the following options on in your ``torrc``:: CookieAuthentication 1 CookieAuthFileGroupReadable 1 If you want to use unix sockets to speak to tor:: ControlSocketsGroupWritable 1 ControlSocket /var/run/tor/control The defaults used by py:meth:`txtorcon.build_local_tor_connection` will find a Tor on ``9051`` or ``/var/run/tor/control`` overview -------- txtorcon is a Twisted-based asynchronous Tor control protocol implementation. Twisted is an event-driven networking engine written in Python and Tor is an onion-routing network designed to improve people's privacy and anonymity on the Internet. The main abstraction of this library is txtorcon.TorControlProtocol which presents an asynchronous API to speak the Tor client protocol in Python. txtorcon also provides abstractions to track and get updates about Tor's state (txtorcon.TorState) and current configuration (including writing it to Tor or disk) in txtorcon.TorConfig, along with helpers to asynchronously launch slave instances of Tor including Twisted endpoint support. txtorcon runs all tests cleanly on: - Debian "squeeze", "wheezy" and "jessie" - OS X 10.4 (naif) - OS X 10.8 (lukas lueg) - OS X 10.9 (kurt neufeld) - Fedora 18 (lukas lueg) - FreeBSD 10 (enrique fynn) (**needed to install "lsof"**) - RHEL6 - Reports from other OSes appreciated. If instead you want a synchronous (threaded) Python controller library, check out Stem at https://stem.torproject.org/ quick implementation overview ----------------------------- txtorcon provides a class to track Tor's current state -- such as details about routers, circuits and streams -- called txtorcon.TorState and an abstraction to the configuration values via txtorcon.TorConfig which provides attribute-style accessors to Tor's state (including making changes). txtorcon.TorState provides txtorcon.Router, txtorcon.Circuit and txtorcon.Stream objects which implement a listener interface so client code may receive updates (in real time) including Tor events. txtorcon uses **trial for unit-tests** and has 100% test-coverage -- which is not to say I've covered all the cases, but nearly all of the code is at least exercised somehow by the unit tests. Tor itself is not required to be running for any of the tests. ohcount claims around 2000 lines of code for the core bit; around 4000 including tests. About 37% comments in the not-test code. There are a few simple integration tests, based on Docker. More are always welcome! dependencies / requirements --------------------------- - `twisted `_: txtorcon should work with any Twisted 11.1.0 or newer. Twisted 15.4.0+ works with Python3, and so does txtorcon (if you find something broken on Py3 please file a bug). - `GeoIP `_: **optional** provides location information for ip addresses; you will want to download GeoLite City from `MaxMind `_ or pay them for more accuracy. Or use tor-geoip, which makes this sort-of optional, in that we'll query Tor for the IP if the GeoIP database doesn't have an answer. It also does ASN lookups if you installed that MaxMind database. - `python-ipaddr `_: **optional**. Google's IP address manipulation code. - development: `Sphinx `_ if you want to build the documentation. In that case you'll also need something called ``python-repoze.sphinx.autointerface`` (at least in Debian) to build the Interface-derived docs properly. - development: `coverage `_ to run the code-coverage metrics, and Tox - optional: GraphViz is used in the tests (and to generate state-machine diagrams, if you like) but those tests are skipped if "dot" isn't in your path .. BEGIN_INSTALL In any case, on a `Debian `_ wheezy, squeeze or Ubuntu system, this should work:: apt-get install -y python-setuptools python-twisted python-ipaddr python-geoip graphviz tor apt-get install -y python-sphinx python-repoze.sphinx.autointerface python-coverage # for development .. END_INSTALL Using pip this would be:: pip install Twisted ipaddr pygeoip pip install GeoIP Sphinx repoze.sphinx.autointerface coverage # for development or:: pip install -r requirements.txt pip install -r dev-requirements.txt or for the bare minimum:: pip install Twisted # will install zope.interface too documentation ------------- It is likely that you will need to read at least some of `control-spec.txt `_ from the torspec git repository so you know what's being abstracted by this library. Run "make doc" to build the Sphinx documentation locally, or rely on ReadTheDocs https://txtorcon.readthedocs.org which builds each tagged release and the latest master. There is also a directory of examples/ scripts, which have inline documentation explaining their use. contact information ------------------- For novelty value, the Web site (with built documentation and so forth) can be viewed via Tor at http://timaq4ygg2iegci7.onion although the code itself is hosted via git:: torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git or:: git clone git://github.com/meejah/txtorcon.git You may contact me via ``meejah at meejah dot ca`` with GPG key `0xC2602803128069A7 `_ or see ``meejah.asc`` in the repository. The fingerprint is ``9D5A 2BD5 688E CB88 9DEB CD3F C260 2803 1280 69A7``. It is often possible to contact me as ``meejah`` in #tor-dev on `OFTC `_ but be patient for replies (I do look at scrollback, so putting "meejah: " in front will alert my client). More conventionally, you may get the code at GitHub and documentation via ReadTheDocs: - https://github.com/meejah/txtorcon - https://txtorcon.readthedocs.org Please do **use the GitHub issue-tracker** to report bugs. Patches, pull-requests, comments and criticisms are all welcomed and appreciated. Keywords: python,twisted,tor,tor controller Platform: UNKNOWN Classifier: Framework :: Twisted Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Internet :: Proxy Servers Classifier: Topic :: Internet Classifier: Topic :: Security txtorcon-0.14.2/txtorcon.egg-info/requires.txt0000644000175000017500000000030612627745450021337 0ustar mikemike00000000000000Twisted>=11.1.0 ipaddr>=2.1.10 zope.interface>=3.6.1 txsocksx>=1.13.0 [dev] coverage setuptools>=0.8.0 Sphinx repoze.sphinx.autointerface>=0.4 coveralls wheel twine pyflakes pep8 mock ipaddr GeoIP txtorcon-0.14.2/txtorcon.egg-info/dependency_links.txt0000644000175000017500000000000112627745450023006 0ustar mikemike00000000000000 txtorcon-0.14.2/txtorcon.egg-info/pbr.json0000644000175000017500000000005712627745450020420 0ustar mikemike00000000000000{"is_release": false, "git_version": "1ae3aad"}txtorcon-0.14.2/MANIFEST.in0000644000175000017500000000114012515516430015106 0ustar mikemike00000000000000include Makefile include README.rst include INSTALL include TODO include LICENSE include meejah.asc include scripts/*.py include docs/Makefile include docs/apilinks_sphinxext.py include docs/conf.py include docs/*.rst include docs/_static/* exclude docs/_static/*~ include docs/_themes/* exclude docs/_themes/*~ include docs/_themes/alabaster/* exclude docs/_themes/alabaster/*~ exclude docs/_themes/alabaster/*.pyc include docs/_themes/alabaster/static/* exclude docs/_themes/alabaster/static/*~ include examples/* exclude examples/*~ include requirements.txt include dev-requirements.txt include test/*.py txtorcon-0.14.2/txtorcon/0000755000175000017500000000000012627745451015247 5ustar mikemike00000000000000txtorcon-0.14.2/txtorcon/spaghetti.py0000644000175000017500000000746612611263616017615 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import with_statement import warnings class FSM(object): """ Override Matcher and Handler and pass instances to add_handler to create transitions between states. If a transition handles something, it returns the next state. If you want something to track global state, but it in your data instance passed to process so that transitions, states can access it. """ states = [] state = None def __init__(self, states): """first state is the initial state""" if len(states) > 0: self.state = states[0] self.states = states def process(self, data): if self.state is None: raise RuntimeError("There is no initial state.") next_state = self.state.process(data) if next_state: self.state = next_state else: warnings.warn("No next state", RuntimeWarning) def add_state(self, state): # first added state is initial state if len(self.states) == 0: self.state = state self.states.append(state) def dotty(self): r = 'digraph fsm {\n\n' for s in self.states: r = r + s.dotty() r = r + '\n}\n' return r class State(object): def __init__(self, name): self.name = name self.transitions = [] def process(self, data): for t in self.transitions: r = t.process(data) if r is not None: return r return None def add_transition(self, t): self.transitions.append(t) t.start_state = self def add_transitions(self, transitions): for t in transitions: self.add_transition(t) def __str__(self): r = '%s ' % t.next_state.name) r = r + ']>' return r def dotty(self): r = '%s;\n' % self.name r = r + 'edge [fontsize=8]\n' r = r + 'rankdir=TB;\nnodesep=2;\n' for t in self.transitions: r = r + '%s -> %s [label="%s\\n%s"]\n' % (self.name, t.next_state.name, t.matcher.__name__, t.handler.__name__) return r class Transition(object): def __init__(self, next_state, matcher, handler): self.matcher = matcher self.handler = handler self.start_state = None self.next_state = next_state if self.next_state is None: raise RuntimeError("next_state must be valid") def match(self, data): """ used by process; calls handler if matcher returns true for data by default. may override instead of providing a matcher methdo to ctor. """ if self.matcher is not None: return self.matcher(data) return True def handle(self, data): """ return next state. May override in a subclass to change behavior or pass a handler method to ctor """ if self.handler: state = self.handler(data) if state is None: return self.next_state return state return self.next_state def process(self, data): """return next state, or None if not handled.""" if self.match(data): return self.handle(data) return None def __str__(self): if self.start_state: return "%s>" % (self.start_state.name, self.next_state.name) return "%s>" % (self.next_state.name,) txtorcon-0.14.2/txtorcon/endpoints.py0000644000175000017500000006750312627744100017625 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import with_statement import os import shutil import weakref import tempfile import functools from txtorcon.util import available_tcp_port # backwards-compatibility dance: we "should" be using the # ...WithReactor class, but in Twisted prior to 14, there is no such # class (and the parse() doesn't provide a 'reactor' argument). try: from twisted.internet.interfaces import IStreamClientEndpointStringParserWithReactor _HAVE_TX_14 = True except ImportError: from twisted.internet.interfaces import IStreamClientEndpointStringParser as IStreamClientEndpointStringParserWithReactor _HAVE_TX_14 = False from twisted.internet import defer, reactor from twisted.python import log from twisted.internet.interfaces import IStreamServerEndpointStringParser from twisted.internet.interfaces import IStreamServerEndpoint from twisted.internet.interfaces import IStreamClientEndpoint from twisted.internet.interfaces import IListeningPort from twisted.internet.interfaces import IAddress from twisted.internet.endpoints import serverFromString from twisted.internet.endpoints import clientFromString from twisted.internet.endpoints import TCP4ClientEndpoint from twisted.internet import error from twisted.plugin import IPlugin from twisted.python.util import FancyEqMixin from zope.interface import implementer from zope.interface import Interface, Attribute from txsocksx.client import SOCKS5ClientEndpoint from .torconfig import TorConfig, launch_tor, HiddenService from .torstate import build_tor_connection _global_tor_config = None _global_tor_lock = defer.DeferredLock() # we need the lock because we (potentially) yield several times while # "creating" the TorConfig instance @defer.inlineCallbacks def get_global_tor(reactor, control_port=None, progress_updates=None, _tor_launcher=lambda r, c, p: launch_tor( c, r, progress_updates=p)): """ See description of :class:`txtorcon.TCPHiddenServiceEndpoint`'s class-method ``global_tor`` :param control_port: a TCP port upon which to run the launched Tor's control-protocol (selected by the OS by default). :param progress_updates: A callable that takes 3 args: ``percent, tag, message`` which is called when Tor announcing some progress setting itself up. :returns: a ``Deferred`` that fires a :class:`txtorcon.TorConfig` which is bootstrapped. The _tor_launcher keyword arg is internal-only. """ global _global_tor_config global _global_tor_lock yield _global_tor_lock.acquire() try: if _global_tor_config is None: _global_tor_config = config = yield _create_default_config(reactor) # start Tor launching yield _tor_launcher(reactor, config, progress_updates) yield config.post_bootstrap else: cp = _global_tor_config.ControlPort if control_port is not None and control_port != cp: raise RuntimeError( "ControlPort is %s, you wanted %s" % (cp, control_port)) defer.returnValue(_global_tor_config) finally: _global_tor_lock.release() @defer.inlineCallbacks def _create_default_config(reactor, control_port=None): """ Internal method to create a new TorConfig instance with defaults. """ config = TorConfig() if control_port is None: control_port = yield available_tcp_port(reactor) config.ControlPort = control_port config.SOCKSPort = 0 defer.returnValue(config) class IProgressProvider(Interface): """FIXME move elsewhere? think harder?""" def add_progress_listener(listener): """ Adds a progress listener. The listener is a callable that gets called with 3 arguments corresponding to Tor's updates: (percent, tag, message). percent is an integer from 0 to 100, tag and message are both strings. (message is the human-readable one) """ @implementer(IStreamServerEndpoint, IProgressProvider) class TCPHiddenServiceEndpoint(object): """This represents something listening on an arbitrary local port that has a Tor configured with a Hidden Service pointing at it. :api:`twisted.internet.endpoints.TCP4ServerEndpoint ` is used under the hood to do the local listening. There are three main ways to use this class, and you are encouraged to use the @classmethod ways of creating instances: `system_tor <#txtorcon.TCPHiddenServiceEndpoint.system_tor>`_, `global_tor <#txtorcon.TCPHiddenServiceEndpoint.global_tor>`_, and `private_tor <#txtorcon.TCPHiddenServiceEndpoint.private_tor>`_ 1. system_tor(...) connects to an already-started tor on the endpoint you specify; stricly speaking not a "system" tor since you could have spawned it some other way. See `Tor bug 11291 `_ however. 2. global_tor(...) refers to a single possible Tor instance per python process. So the first call to this launches a new Tor, and subsequent calls re-use the existing Tor (that is, add more hidden services to it). 3. private_tor(...) launches a new Tor instance no matter what, so it will have just the one hidden serivce on it. If you need to set configuration options that are not reflected in any of the method signatures above, you'll have to construct an instance of this class yourself (i.e. with a TorConfig instance you've created). No matter how you came by your instance, calling `listen()` on it causes Tor to be launched or connected-to, your hidden service to be added, checks that the descriptor is uploaded and you get a ``Deferred`` with an ``IListeningPort`` whose ``getHost()`` will return a :class:`txtorcon.TorOnionAddress`. The port object will also implement :class:`txtorcon.IHiddenService` so you can get the locally-listening address and hidden serivce directory:: endpoint = ... port = yield endpoint.listen(...) uri = port.getHost().onion_uri port = port.getHost().onion_port addr = IHiddenService(port).local_address hsdir = IHiddenService(port).hidden_service_dir returns (via Deferred) an object that implements :api:`twisted.internet.interfaces.IStreamServerEndpoint` :ivar onion_uri: the public key, like ``timaq4ygg2iegci7.onion`` which came from the hidden_service_dir's ``hostname`` file :ivar onion_private_key: the contents of ``hidden_service_dir/private_key`` :ivar hiddenServiceDir: the data directory, either passed in or created with ``tempfile.mkstemp`` """ @classmethod def system_tor(cls, reactor, control_endpoint, public_port, hidden_service_dir=None, local_port=None): """ This returns a TCPHiddenServiceEndpoint connected to the endpoint you specify in `control_endpoint`. After connecting, a single hidden service is added. The endpoint can be a Unix socket if Tor's `ControlSocket` option was used (instead of `ControlPort`). .. note:: If Tor bug #11291 is not yet fixed, this won't work if you only have Group access. XXX FIXME re-test """ @defer.inlineCallbacks def _connect(): tor_protocol = yield build_tor_connection(control_endpoint, build_state=False) config = TorConfig(tor_protocol) yield config.post_bootstrap defer.returnValue(config) return TCPHiddenServiceEndpoint(reactor, _connect(), public_port, hidden_service_dir=hidden_service_dir, local_port=local_port) @classmethod def global_tor(cls, reactor, public_port, hidden_service_dir=None, local_port=None, control_port=None): """ This returns a TCPHiddenServiceEndpoint connected to a txtorcon global Tor instance. The first time you call this, a new Tor will be launched. Subsequent calls will re-use the same connection (in fact, the very same TorControlProtocol and TorConfig instances). If the options you pass are incompatible with an already-launched Tor, RuntimeError will be thrown. It's probably best to not specify any option besides `public_port`, `hidden_service_dir`, and maybe `local_port` unless you have a specific need to. You can also access this global txtorcon instance via :method:`txtorcon.get_global_tor` (which is precisely what this method uses to get it). All keyword options have defaults (e.g. random ports, or tempdirs). """ def progress(*args): progress.target(*args) config = get_global_tor(reactor, control_port=control_port, progress_updates=progress) # config is a Deferred here, but endpoint resolves in listen() r = TCPHiddenServiceEndpoint(reactor, config, public_port, hidden_service_dir=hidden_service_dir, local_port=local_port) progress.target = r._tor_progress_update return r @classmethod def private_tor(cls, reactor, public_port, hidden_service_dir=None, local_port=None, control_port=None): """ This returns a TCPHiddenServiceEndpoint that's always connected to its own freshly-launched Tor instance. All keyword options have defaults (e.g. random ports, or tempdirs). """ def progress(*args): progress.target(*args) @defer.inlineCallbacks def _launch(control_port): config = yield _create_default_config(reactor, control_port) yield launch_tor(config, reactor, progress_updates=progress) yield config.post_bootstrap defer.returnValue(config) r = TCPHiddenServiceEndpoint(reactor, _launch(control_port), public_port, hidden_service_dir=hidden_service_dir, local_port=local_port) progress.target = r._tor_progress_update return r def __init__(self, reactor, config, public_port, hidden_service_dir=None, local_port=None): """ :param reactor: :api:`twisted.internet.interfaces.IReactorTCP` provider :param config: :class:`txtorcon.TorConfig` instance (doesn't need to be bootstrapped) or a Deferred. Note that ``save()`` will be called on this at least once. FIXME should I just accept a TorControlProtocol instance instead, and create my own TorConfig? :param public_port: The port number we will advertise in the hidden serivces directory. :param local_port: The port number we will perform our local tcp listen on and receive incoming connections from the tor process. :param hidden_service_dir: If not None, point to a HiddenServiceDir directory (i.e. with "hostname" and "private_key" files in it). If not provided, one is created with temp.mkstemp() AND DELETED when the reactor shuts down. :param endpoint_generator: A callable that generates a new instance of something that implements IServerEndpoint (by default TCP4ServerEndpoint) """ self.reactor = reactor self.config = defer.maybeDeferred(lambda: config) self.public_port = public_port self.local_port = local_port self.hidden_service_dir = hidden_service_dir self.tcp_listening_port = None self.hiddenservice = None self.retries = 0 '''for IProgressProvider to add_progress_listener''' self.progress_listeners = [] if self.hidden_service_dir is None: self.hidden_service_dir = tempfile.mkdtemp(prefix='tortmp') log.msg('Will delete "%s" at shutdown.' % self.hidden_service_dir) delete = functools.partial(shutil.rmtree, self.hidden_service_dir) reactor.addSystemEventTrigger('before', 'shutdown', delete) @property def onion_uri(self): if self.hiddenservice is None: return None try: return self.hiddenservice.hostname except IOError: return None @property def onion_private_key(self): if self.hiddenservice is None: return None try: return self.hiddenservice.private_key except IOError: return None def add_progress_listener(self, listener): """IProgressProvider API""" self.progress_listeners.append(listener) def _tor_progress_update(self, prog, tag, summary): log.msg('%d%% %s' % (prog, summary)) for p in self.progress_listeners: p(prog, tag, summary) @defer.inlineCallbacks def listen(self, protocolfactory): """Implement :api:`twisted.internet.interfaces.IStreamServerEndpoint `. Returns a Deferred that delivers an :api:`twisted.internet.interfaces.IListeningPort` implementation. This port can also be adapted to two other interfaces: :class:`txtorcon.IHiddenService` so you can get the `onion_uri` and `onion_private_key` members (these correspond to "hostname" and "private_key" from the HiddenServiceDir Tor is using). :class:`txtorcon.IProgressProvider` can provide you progress updates while Tor is launched. Note that Tor is not always launched when calling this listen() method. At this point, Tor will have fully started up and successfully accepted the hidden service's config. FIXME TODO: also listen for an INFO-level Tor message (does exist, #tor-dev says) that indicates the hidden service's descriptor is published. It is "connection_dir_client_reached_eof(): Uploaded rendezvous descriptor (status 200 ("Service descriptor (v2) stored"))" at INFO level. """ self.protocolfactory = protocolfactory # self.config is always a Deferred; see __init__ self.config = yield self.config # just to be sure: yield self.config.post_bootstrap # XXX - perhaps allow the user to pass in an endpoint # descriptor and make this one the default? Then would # probably want to check for "is a local interface or not" and # at *least* warn if it's not local... self.tcp_endpoint = serverFromString(self.reactor, 'tcp:0:interface=127.0.0.1') d = self.tcp_endpoint.listen(self.protocolfactory) self.tcp_listening_port = yield d self.local_port = self.tcp_listening_port.getHost().port # NOTE at some point, we can support unix sockets here # once Tor does. See bug #XXX # specifically NOT creating the hidden-service dir; letting # Tor do it will more-likely result in a usable situation... if not os.path.exists(self.hidden_service_dir): log.msg( 'Noting that "%s" does not exist; letting Tor create it.' % self.hidden_service_dir ) # listen for the descriptor upload event info_callback = defer.Deferred() def info_event(msg): # XXX giant hack here; Right Thing would be to implement a # "real" event in Tor and listen for that. if 'Service descriptor (v2) stored' in msg: info_callback.callback(None) self.config.protocol.add_event_listener('INFO', info_event) if self.hidden_service_dir not in [hs.dir for hs in self.config.HiddenServices]: self.hiddenservice = HiddenService( self.config, self.hidden_service_dir, ['%d 127.0.0.1:%d' % (self.public_port, self.local_port)], group_readable=1) self.config.HiddenServices.append(self.hiddenservice) yield self.config.save() self._tor_progress_update(100.0, 'wait_descriptor', 'Waiting for descriptor upload...') yield info_callback # awaits an INFO log-line from Tor .. sketchy yield self.config.protocol.remove_event_listener('INFO', info_event) self._tor_progress_update(100.0, 'wait_descriptor', 'At least one descriptor uploaded.') log.msg( 'Started hidden service "%s" on port %d' % (self.onion_uri, self.public_port)) log.msg('Keys are in "%s".' % (self.hidden_service_dir,)) defer.returnValue(TorOnionListeningPort(self.tcp_listening_port, self.hidden_service_dir, self.onion_uri, self.public_port, self.config)) @implementer(IAddress) class TorOnionAddress(FancyEqMixin, object): """ A ``TorOnionAddress`` represents the public address of a Tor hidden service. :ivar type: A string describing the type of transport, 'onion'. :ivar onion_uri: The public-key onion address (e.g. timaq4ygg2iegci7.onion) :ivar onion_port: The port we're advertising inside the Tor network. In otherwords, we should be reachable at (onion_uri, onion_port) via Tor. """ compareAttributes = ('type', 'onion_uri', 'onion_port') type = 'onion' def __init__(self, uri, port): self.onion_uri = uri self.onion_port = port def __repr__(self): return '%s(%r, %d)' % ( self.__class__.__name__, self.onion_uri, self.onion_port) def __hash__(self): return hash((self.type, self.onion_uri, self.onion_port)) class IHiddenService(Interface): local_address = Attribute( 'The actual machine address we are listening on.') hidden_service_dir = Attribute( 'The hidden service directory, where "hostname" and "private_key" ' 'files live.') tor_config = Attribute( 'The TorConfig object attached to the Tor hosting this hidden service ' '(in turn has .protocol for TorControlProtocol).') @implementer(IListeningPort, IHiddenService) class TorOnionListeningPort(object): """ Our TCPHiddenServiceEndpoint's `listen` method will return a deferred which fires an instance of this object. The `getHost` method will return a TorOnionAddress instance... which can be used to determine the onion address of a newly created Tor Hidden Service. `startListening` and `stopListening` methods proxy to the "TCP ListeningPort" object... which implements IListeningPort interface but has many more responsibilities we needn't worry about here. """ def __init__(self, listening_port, hs_dir, uri, port, tor_config): self.local_address = listening_port self.hidden_service_dir = hs_dir self._config_ref = weakref.ref(tor_config) self.address = TorOnionAddress(uri, port) def startListening(self): """IListeningPort API""" self.local_address.startListening() def stopListening(self): """IListeningPort API""" self.local_address.stopListening() def getHost(self): """IListeningPort API""" return self.address def __str__(self): return '' % (self.address.onion_uri, self.address.onion_port) # local_address IHiddenService API fulfilled in ctor # hidden_service_dir IHiddenService API fulfilled in ctor @property def tor_config(self): return self._config_ref() # None if ref dead @implementer(IStreamServerEndpointStringParser, IPlugin) class TCPHiddenServiceEndpointParser(object): """ This provides a twisted IPlugin and IStreamServerEndpointsStringParser so you can call :api:`twisted.internet.endpoints.serverFromString ` with a string argument like: ``onion:80:localPort=9876:controlPort=9052:hiddenServiceDir=/dev/shm/foo`` ...or simply: ``onion:80`` If ``controlPort`` is specified, it means connect to an already-running Tor on that port and add a hidden-serivce to it. ``localPort`` is optional and if not specified, a port is selected by the OS. If ``hiddenServiceDir`` is not specified, one is created with ``tempfile.mkstemp()``. The IStreamServerEndpoint returned will be an instance of :class:`txtorcon.TCPHiddenServiceEndpoint` """ prefix = "onion" # note that these are all camelCase because Twisted uses them to # do magic parsing stuff, and to conform to Twisted's conventions # we should use camelCase in the endpoint definitions... def parseStreamServer(self, reactor, public_port, localPort=None, controlPort=None, hiddenServiceDir=None): ''' :api:`twisted.internet.interfaces.IStreamServerEndpointStringParser` ''' public_port = int(public_port) if localPort is not None: localPort = int(localPort) hsd = hiddenServiceDir if hsd: orig = hsd hsd = os.path.expanduser(hsd) hsd = os.path.realpath(hsd) if orig != hsd: log.msg('Using "%s" for hsd' % hsd) if controlPort: try: ep = clientFromString( reactor, "tcp:host=127.0.0.1:port=%d" % int(controlPort)) except ValueError: ep = clientFromString(reactor, "unix:path=%s" % controlPort) return TCPHiddenServiceEndpoint.system_tor(reactor, ep, public_port, hidden_service_dir=hsd, local_port=localPort) return TCPHiddenServiceEndpoint.global_tor(reactor, public_port, hidden_service_dir=hsd, local_port=localPort, control_port=controlPort) def default_tcp4_endpoint_generator(*args, **kw): """ Default generator used to create client-side TCP4ClientEndpoint instances. We do this to make the unit tests work... """ return TCP4ClientEndpoint(*args, **kw) @implementer(IStreamClientEndpoint) class TorClientEndpoint(object): """ I am an endpoint class who attempts to establish a SOCKS5 connection with the system tor process. Either the user must pass a SOCKS port into my constructor OR I will attempt to guess the Tor SOCKS port by iterating over a list of ports that tor is likely to be listening on. :param host: The hostname to connect to. This of course can be a Tor Hidden Service onion address. :param port: The tcp port or Tor Hidden Service port. :param _proxy_endpoint_generator: This is used for unit tests. :param socks_port: This optional argument lets the user specify which Tor SOCKS port should be used. """ # XXX should get these via the control connection, i.e. ask Tor # via GETINFO net/listeners/socks or whatever socks_ports_to_try = [9050, 9150] def __init__(self, host, port, socks_hostname=None, socks_port=None, socks_username=None, socks_password=None, _proxy_endpoint_generator=default_tcp4_endpoint_generator): if host is None or port is None: raise ValueError('host and port must be specified') self.host = host self.port = port self._proxy_endpoint_generator = _proxy_endpoint_generator self.socks_hostname = socks_hostname self.socks_port = socks_port self.socks_username = socks_username self.socks_password = socks_password if self.socks_port is None: self._socks_port_iter = iter(self.socks_ports_to_try) self._socks_guessing_enabled = True else: self._socks_guessing_enabled = False def connect(self, protocolfactory): self.protocolfactory = protocolfactory if self._socks_guessing_enabled: self.socks_port = self._socks_port_iter.next() d = self._try_connect() return d def _try_connect(self): self.tor_socks_endpoint = self._proxy_endpoint_generator( reactor, self.socks_hostname, self.socks_port ) if self.socks_username is None or self.socks_password is None: ep = SOCKS5ClientEndpoint( self.host, self.port, self.tor_socks_endpoint ) else: ep = SOCKS5ClientEndpoint( self.host, self.port, self.tor_socks_endpoint, methods=dict(login=(self.socks_username, self.socks_password)) ) d = ep.connect(self.protocolfactory) if self._socks_guessing_enabled: d.addErrback(self._retry_socks_port) return d def _retry_socks_port(self, failure): failure.trap(error.ConnectError) try: self.socks_port = self._socks_port_iter.next() except StopIteration: return failure d = self._try_connect() d.addErrback(self._retry_socks_port) return d @implementer(IPlugin, IStreamClientEndpointStringParserWithReactor) class TorClientEndpointStringParser(object): """ This provides a twisted IPlugin and IStreamClientEndpointsStringParser so you can call :api:`twisted.internet.endpoints.clientFromString ` with a string argument like: ``tor:host=timaq4ygg2iegci7.onion:port=80:socksPort=9050`` ...or simply: ``tor:host=timaq4ygg2iegci7.onion:port=80`` You may also include a username + password. By default, Tor will not put two streams that provided different authentication on the same circuit. ``tor:host=torproject.org:port=443:socksUsername=foo:socksPassword=bar`` If ``socksPort`` is specified, it means only use that port to attempt to proxy through Tor. If unspecified then try some likely socksPorts such as [9050, 9150]. NOTE that I'm using camelCase variable names in the endpoint string to be consistent with the rest of Twisted's naming (and their endpoint parsers). XXX FIXME if there is no Tor instance found at socksPort, we should launch one. Perhaps a separate option? (Should be on by default, though, I think). """ prefix = "tor" def _parseClient(self, host=None, port=None, socksHostname=None, socksPort=None, socksUsername=None, socksPassword=None): if port is not None: port = int(port) if socksHostname is None: socksHostname = '127.0.0.1' if socksPort is not None: socksPort = int(socksPort) return TorClientEndpoint( host, port, socks_hostname=socksHostname, socks_port=socksPort, socks_username=socksUsername, socks_password=socksPassword ) def parseStreamClient(self, *args, **kwargs): # for Twisted 14 and 15 (and more) the first argument is # 'reactor', for older Twisteds it's not if _HAVE_TX_14: return self._parseClient(*args[1:], **kwargs) return self._parseClient(*args, **kwargs) txtorcon-0.14.2/txtorcon/torcontrolprotocol.py0000644000175000017500000007271712627744056021626 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import with_statement from twisted.python import log from twisted.internet import defer from twisted.internet.interfaces import IProtocolFactory from twisted.internet.error import ConnectionDone from twisted.protocols.basic import LineOnlyReceiver from zope.interface import implementer from txtorcon.util import hmac_sha256, compare_via_hash from txtorcon.log import txtorlog from txtorcon.interface import ITorControlProtocol from .spaghetti import FSM, State, Transition import os import re import base64 DEFAULT_VALUE = 'DEFAULT' class TorProtocolError(RuntimeError): """ Happens on 500-level responses in the protocol, almost certainly in an errback chain. :ivar code: the actual error code :ivar text: other text from the protocol """ def __init__(self, code, text): self.code = code self.text = text super(TorProtocolError, self).__init__(text) def __str__(self): return str(self.code) + ' ' + self.text @implementer(IProtocolFactory) class TorProtocolFactory(object): """ Builds TorControlProtocol objects. Implements IProtocolFactory for Twisted interaction. If your running Tor doesn't support COOKIE authentication, then you should supply a password callback. """ def __init__(self, password_function=lambda: None): """ Builds protocols to talk to a Tor client on the specified address. For example:: TCP4ClientEndpoint(reactor, "localhost", 9051).connect(TorProtocolFactory()) reactor.run() By default, COOKIE authentication is used if available. :param password_function: If supplied, this is a zero-argument method that returns a password (or a Deferred). By default, it returns None. This is only queried if the Tor we connect to doesn't support (or hasn't enabled) COOKIE authentication. """ self.password_function = password_function def doStart(self): ":api:`twisted.internet.interfaces.IProtocolFactory` API" def doStop(self): ":api:`twisted.internet.interfaces.IProtocolFactory` API" def buildProtocol(self, addr): ":api:`twisted.internet.interfaces.IProtocolFactory` API" proto = TorControlProtocol(self.password_function) proto.factory = self return proto class Event(object): """ A class representing one of the valid EVENTs that Tor supports. This allows you to listen for such an event; see TorController.add_event The callbacks will be called every time the event in question is received. """ def __init__(self, name): self.name = name self.callbacks = [] def listen(self, cb): self.callbacks.append(cb) def unlisten(self, cb): self.callbacks.remove(cb) def got_update(self, data): # print self.name,"got_update:",data for cb in self.callbacks: cb(data) def unquote(word): if len(word) == 0: return word if word[0] == '"' and word[-1] == '"': return word[1:-1] elif word[0] == "'" and word[-1] == "'": return word[1:-1] return word def parse_keywords(lines, multiline_values=True): """ Utility method to parse name=value pairs (GETINFO etc). Takes a string with newline-separated lines and expects at most one = sign per line. Accumulates multi-line values. :param multiline_values: The default is True which allows for multi-line values until a line with the next = sign on it. So: '''Foo=bar\nBar''' produces one key, 'Foo', with value 'bar\nBar' -- set to False, there would be two keys: 'Foo' with value 'bar' and 'Bar' with value DEFAULT_VALUE. """ rtn = {} key = None value = '' # FIXME could use some refactoring to reduce code duplication! for line in lines.split('\n'): if line.strip() == 'OK': continue if '=' in line and ' ' not in line.split('=', 1)[0]: if key: if key in rtn: if isinstance(rtn[key], list): rtn[key].append(unquote(value)) else: rtn[key] = [rtn[key], unquote(value)] else: rtn[key] = unquote(value) (key, value) = line.split('=', 1) else: if key is None: rtn[line.strip()] = DEFAULT_VALUE elif multiline_values is False: rtn[key] = value rtn[line.strip()] = DEFAULT_VALUE key = None value = '' else: value = value + '\n' + line if key: if key in rtn: if isinstance(rtn[key], list): rtn[key].append(unquote(value)) else: rtn[key] = [rtn[key], unquote(value)] else: rtn[key] = unquote(value) return rtn @implementer(ITorControlProtocol) class TorControlProtocol(LineOnlyReceiver): """ This is the main class that talks to a Tor and implements the "raw" procotol. This instance does not track state; see :class:`txtorcon.TorState` for the current state of all Circuits, Streams and Routers. :meth:`txtorcon.TorState.build_circuit` allows you to build custom circuits. :meth:`txtorcon.TorControlProtocol.add_event_listener` can be used to listen for specific events. To see how circuit and stream listeners are used, see :class:`txtorcon.TorState`, which is also the place to go if you wish to add your own stream or circuit listeners. """ def __init__(self, password_function=None): """ :param password_function: A zero-argument callable which returns a password (or Deferred). It is only called if the Tor doesn't have COOKIE authentication turned on. Tor's default is COOKIE. """ self.password_function = password_function """If set, a callable to query for a password to use for authentication to Tor (default is to use COOKIE, however). May return Deferred.""" self.version = None """Version of Tor we've connected to.""" self.is_owned = None """If not None, this is the PID of the Tor process we own (TAKEOWNERSHIP, etc).""" self.events = {} """events we've subscribed to (keyed by name like "GUARD", "STREAM")""" self.valid_events = {} """all valid events (name -> Event instance)""" self.valid_signals = [] """A list of all valid signals we accept from Tor""" self.on_disconnect = defer.Deferred() """ This Deferred is triggered when the connection is closed. If there was an error, the errback is called instead. """ self.post_bootstrap = defer.Deferred() """ This Deferred is triggered when we're done setting up (authentication, getting information from Tor). You will want to use this to do things with the :class:`TorControlProtocol` class when it's set up, like:: def setup_complete(proto): print "Setup complete, attached to Tor version",proto.version def setup(proto): proto.post_bootstrap.addCallback(setup_complete) TCP4ClientEndpoint(reactor, "localhost", 9051).connect(TorProtocolFactory()) d.addCallback(setup) See the helper method :func:`txtorcon.build_tor_connection`. """ # variables related to the state machine self.defer = None # Deferred we returned for the current command self.response = '' self.code = None self.command = None # currently processing this command self.commands = [] # queued commands # Here we build up the state machine. Mostly it's pretty # simply, confounded by the fact that 600's (notify) can come # at any time AND can be multi-line itself. Luckily, these # can't be nested, nor can the responses be interleaved. idle = State("IDLE") recv = State("RECV") recvmulti = State("RECV_PLUS") recvnotify = State("NOTIFY_MULTILINE") idle.add_transition(Transition(idle, self._is_single_line_response, self._broadcast_response)) idle.add_transition(Transition(recvmulti, self._is_multi_line, self._start_command)) idle.add_transition(Transition(recv, self._is_continuation_line, self._start_command)) recv.add_transition(Transition(recvmulti, self._is_multi_line, self._accumulate_response)) recv.add_transition(Transition(recv, self._is_continuation_line, self._accumulate_response)) recv.add_transition(Transition(idle, self._is_finish_line, self._broadcast_response)) recvmulti.add_transition(Transition(recv, self._is_end_line, lambda x: None)) recvmulti.add_transition(Transition(recvmulti, self._is_not_end_line, self._accumulate_multi_response)) self.fsm = FSM([recvnotify, idle, recvmulti, recv]) self.state_idle = idle # hand-set initial state default start state is first in the # list; the above looks nice in dotty though self.fsm.state = idle self.stop_debug() def start_debug(self): self.debuglog = open('txtorcon-debug.log', 'w') def stop_debug(self): def noop(*args, **kw): pass class NullLog(object): write = noop flush = noop self.debuglog = NullLog() def graphviz_data(self): return self.fsm.dotty() # see end of file for all the state machine matcher and # transition methods. def get_info_raw(self, *args): """ Mostly for internal use; gives you the raw string back from the GETINFO command. See :meth:`getinfo ` """ info = ' '.join([str(x) for x in list(args)]) return self.queue_command('GETINFO %s' % info) def get_info_incremental(self, key, line_cb): """ Mostly for internal use; calls GETINFO for a single key and calls line_cb with each line received, as it is received. See :meth:`getinfo ` """ def strip_ok_and_call(line): if line.strip() != 'OK': line_cb(line) return self.queue_command('GETINFO %s' % key, strip_ok_and_call) # The following methods are the main TorController API and # probably the most interesting for users. @defer.inlineCallbacks def get_info(self, *args): """ Uses GETINFO to obtain informatoin from Tor. :param args: should be a list or tuple of strings which are valid information keys. For valid keys, see control-spec.txt from torspec. .. todo:: make some way to automagically obtain valid keys, either from running Tor or parsing control-spec :return: a ``Deferred`` which will callback with a dict containing the keys you asked for. If you want to avoid the parsing into a dict, you can use get_info_raw instead. """ lines = yield self.get_info_raw(*args) rtn = {} key = None for line in lines.split('\n'): if line.split('=', 1)[0] in args: key = line.split('=', 1)[0] rtn[key] = line.split('=', 1)[1] else: rtn[key] = rtn[key] + '\n' + line defer.returnValue(rtn) def get_conf(self, *args): """ Uses GETCONF to obtain configuration values from Tor. :param args: any number of strings which are keys to get. To get all valid configuraiton names, you can call: ``get_info('config/names')`` :return: a Deferred which callbacks with one or many configuration values (depends on what you asked for). See control-spec for valid keys (you can also use TorConfig which will come set up with all the keys that are valid). The value will be a dict. Note that Tor differentiates between an empty value and a default value; in the raw protocol one looks like '250 MyFamily' versus '250 MyFamily=' where the latter is set to the empty string and the former is a default value. We differentiate these by setting the value in the dict to DEFAULT_VALUE for the default value case, or an empty string otherwise. """ d = self.queue_command('GETCONF %s' % ' '.join(args)) d.addCallback(parse_keywords).addErrback(log.err) return d def get_conf_raw(self, *args): """ Same as get_conf, except that the results are not parsed into a dict """ return self.queue_command('GETCONF %s' % ' '.join(args)) def set_conf(self, *args): """ set configuration values. see control-spec for valid keys. args is treated as a list containing name then value pairs. For example, ``set_conf('foo', 'bar')`` will (attempt to) set the key 'foo' to value 'bar'. :return: a ``Deferred`` that will callback with the response ('OK') or errback with the error code and message (e.g. ``"552 Unrecognized option: Unknown option 'foo'. Failing."``) """ if len(args) % 2: d = defer.Deferred() d.errback(RuntimeError("Expected an even number of arguments.")) return d strargs = [str(x) for x in args] keys = [strargs[i] for i in range(0, len(strargs), 2)] values = [strargs[i] for i in range(1, len(strargs), 2)] def maybe_quote(s): if ' ' in s: return '"%s"' % s return s values = [maybe_quote(v) for v in values] args = ' '.join(map(lambda x, y: '%s=%s' % (x, y), keys, values)) return self.queue_command('SETCONF ' + args) def signal(self, nm): """ Issues a signal to Tor. See control-spec or :attr:`txtorcon.TorControlProtocol.valid_signals` for which ones are available and their return values. :return: a ``Deferred`` which callbacks with Tor's response (``OK`` or something like ``552 Unrecognized signal code "foo"``). """ if nm not in self.valid_signals: raise RuntimeError("Invalid signal " + nm) return self.queue_command('SIGNAL %s' % nm) def add_event_listener(self, evt, callback): """:param evt: event name, see also :var:`txtorcon.TorControlProtocol.events` .keys() Add a listener to an Event object. This may be called multiple times for the same event. If it's the first listener, a new SETEVENTS call will be initiated to Tor. Currently the callback is any callable that takes a single argument, that is the text collected for the event from the tor control protocol. For more information on the events supported, see `control-spec section 4.1 `_ .. note:: this is a low-level interface; if you want to follow circuit or stream creation etc. see TorState and methods like add_circuit_listener :Return: ``None`` .. todo:: need an interface for the callback show how to tie in Stem parsing if you want """ if evt not in self.valid_events.values(): try: evt = self.valid_events[evt] except: raise RuntimeError("Unknown event type: " + evt) if evt.name not in self.events: self.events[evt.name] = evt self.queue_command('SETEVENTS %s' % ' '.join(self.events.keys())) evt.listen(callback) return None def remove_event_listener(self, evt, cb): if evt not in self.valid_events.values(): # this lets us pass a string or a real event-object try: evt = self.valid_events[evt] except: raise RuntimeError("Unknown event type: " + evt) evt.unlisten(cb) if len(evt.callbacks) == 0: # note there's a slight window here for an event of this # type to come in before the SETEVENTS succeeds; see # _handle_notify which explicitly ignore this case. del self.events[evt.name] self.queue_command('SETEVENTS %s' % ' '.join(self.events.keys())) def protocolinfo(self): """ :return: a Deferred which will give you PROTOCOLINFO; see control-spec """ return self.queue_command("PROTOCOLINFO 1") def authenticate(self, passphrase): """Call the AUTHENTICATE command.""" return self.queue_command('AUTHENTICATE ' + passphrase.encode("hex")) def quit(self): """ Sends the QUIT command, which asks Tor to hang up on this controller connection. If you've taken ownership of the Tor to which you're connected, this should also cause it to exit. Otherwise, it won't. """ return self.queue_command('QUIT') def queue_command(self, cmd, arg=None): """ returns a Deferred which will fire with the response data when we get it Note that basically every request is ultimately funelled through this command. """ d = defer.Deferred() self.commands.append((d, cmd, arg)) self._maybe_issue_command() return d # the remaining methods are internal API implementations, # callbacks and state-tracking methods -- you shouldn't have any # need to call them. def lineReceived(self, line): """ :api:`twisted.protocols.basic.LineOnlyReceiver` API """ self.debuglog.write(line + '\n') self.debuglog.flush() self.fsm.process(line) def connectionMade(self): "Protocol API" txtorlog.msg('got connection, authenticating') d = self.protocolinfo() d.addCallback(self._do_authenticate) d.addErrback(self._auth_failed) def connectionLost(self, reason): "Protocol API" txtorlog.msg('connection terminated: ' + str(reason)) if self.on_disconnect.callbacks: if reason.check(ConnectionDone): self.on_disconnect.callback(self) else: self.on_disconnect.errback(reason) self.on_disconnect = None return None def _handle_notify(self, code, rest): """ Internal method to deal with 600-level responses. """ firstline = rest[:rest.find('\n')] args = firstline.split() if args[0] in self.events: self.events[args[0]].got_update(rest[len(args[0]) + 1:]) return # not considering this an error, as there's a slight window # after remove_event_listener is called (so the handler is # deleted) but the SETEVENTS command has not yet succeeded def _maybe_issue_command(self): """ If there's at least one command queued and we're not currently processing a command, this will issue the next one on the wire. """ if self.command: return if len(self.commands): self.command = self.commands.pop(0) (d, cmd, cmd_arg) = self.command self.defer = d self.debuglog.write(cmd + '\n') self.debuglog.flush() data = cmd + '\r\n' txtorlog.msg("cmd: {}".format(data.strip())) self.transport.write(data.encode('utf8')) def _auth_failed(self, fail): """ Errback if authentication fails. """ self.post_bootstrap.errback(fail) return None def _safecookie_authchallenge(self, reply): """ Callback on AUTHCHALLENGE SAFECOOKIE """ kw = parse_keywords(reply.replace(' ', '\n')) server_hash = base64.b16decode(kw['SERVERHASH']) server_nonce = base64.b16decode(kw['SERVERNONCE']) # FIXME put string in global. or something. expected_server_hash = hmac_sha256( "Tor safe cookie authentication server-to-controller hash", self.cookie_data + self.client_nonce + server_nonce ) if not compare_via_hash(expected_server_hash, server_hash): raise RuntimeError( 'Server hash not expected; wanted "%s" and got "%s".' % (base64.b16encode(expected_server_hash), base64.b16encode(server_hash)) ) client_hash = hmac_sha256( "Tor safe cookie authentication controller-to-server hash", self.cookie_data + self.client_nonce + server_nonce ) client_hash_hex = base64.b16encode(client_hash) return self.queue_command('AUTHENTICATE %s' % client_hash_hex) def _do_authenticate(self, protoinfo): """ Callback on PROTOCOLINFO to actually authenticate once we know what's supported. """ methods = None for line in protoinfo.split('\n'): if line[:5] == 'AUTH ': kw = parse_keywords(line[5:].replace(' ', '\n')) methods = kw['METHODS'].split(',') if not methods: raise RuntimeError( "Didn't find AUTH line in PROTOCOLINFO response." ) if 'SAFECOOKIE' in methods: cookie = re.search('COOKIEFILE="(.*)"', protoinfo).group(1) self.cookie_data = open(cookie, 'r').read() if len(self.cookie_data) != 32: raise RuntimeError( "Expected authentication cookie to be 32 bytes, got %d" % len(self.cookie_data) ) txtorlog.msg("Using SAFECOOKIE authentication", cookie, len(self.cookie_data), "bytes") self.client_nonce = os.urandom(32) cmd = 'AUTHCHALLENGE SAFECOOKIE ' + \ base64.b16encode(self.client_nonce) d = self.queue_command(cmd) d.addCallback(self._safecookie_authchallenge) d.addCallback(self._bootstrap) d.addErrback(self._auth_failed) return elif 'COOKIE' in methods: cookie = re.search('COOKIEFILE="(.*)"', protoinfo).group(1) with open(cookie, 'r') as cookiefile: data = cookiefile.read() if len(data) != 32: raise RuntimeError( "Expected authentication cookie to be 32 " "bytes, got %d instead." % len(data) ) txtorlog.msg("Using COOKIE authentication", cookie, len(data), "bytes") d = self.authenticate(data) d.addCallback(self._bootstrap) d.addErrback(self._auth_failed) return if self.password_function: d = defer.maybeDeferred(self.password_function) d.addCallback(self._do_password_authentication) d.addErrback(self._auth_failed) return raise RuntimeError( "The Tor I connected to doesn't support SAFECOOKIE nor COOKIE" " authentication and I have no password_function specified." ) def _do_password_authentication(self, passwd): if not passwd: raise RuntimeError("No password available.") d = self.authenticate(passwd) d.addCallback(self._bootstrap) d.addErrback(self._auth_failed) def _set_valid_events(self, events): "used as a callback; see _bootstrap" self.valid_events = {} for x in events.split(): self.valid_events[x] = Event(x) @defer.inlineCallbacks def _bootstrap(self, *args): """ The inlineCallbacks decorator allows us to make this method look synchronous; see the Twisted docs. Each yeild is for a Deferred after which the method continues. When this method finally exits, we're set up and do the post_bootstrap callback. """ try: self.valid_signals = yield self.get_info('signal/names') self.valid_signals = self.valid_signals['signal/names'] except TorProtocolError: self.valid_signals = ["RELOAD", "DUMP", "DEBUG", "NEWNYM", "CLEARDNSCACHE"] self.version = yield self.get_info('version') self.version = self.version['version'] txtorlog.msg("Connected to a Tor with VERSION", self.version) eventnames = yield self.get_info('events/names') eventnames = eventnames['events/names'] self._set_valid_events(eventnames) yield self.queue_command('USEFEATURE EXTENDED_EVENTS') self.post_bootstrap.callback(self) defer.returnValue(self) # State Machine transitions and matchers. See the __init__ method # for a way to output a GraphViz dot diagram of the machine. def _is_end_line(self, line): "for FSM" return line.strip() == '.' def _is_not_end_line(self, line): "for FSM" return not self._is_end_line(line) def _is_single_line_response(self, line): "for FSM" try: code = int(line[:3]) except: return False sl = len(line) > 3 and line[3] == ' ' # print "single line?",line,sl if sl: self.code = code return True return False def _start_command(self, line): "for FSM" # print "startCommand",self.code,line self.code = int(line[:3]) # print "startCommand:",self.code if self.command and self.command[2] is not None: self.command[2](line[4:]) else: self.response = line[4:] + '\n' return None def _is_continuation_line(self, line): "for FSM" # print "isContinuationLine",self.code,line code = int(line[:3]) if self.code and self.code != code: raise RuntimeError("Unexpected code %d, wanted %d" % (code, self.code)) return line[3] == '-' def _is_multi_line(self, line): "for FSM" # print "isMultiLine",self.code,line,line[3] == '+' code = int(line[:3]) if self.code and self.code != code: raise RuntimeError("Unexpected code %d, wanted %d" % (code, self.code)) return line[3] == '+' def _accumulate_multi_response(self, line): "for FSM" if self.command and self.command[2] is not None: self.command[2](line) else: self.response += (line + '\n') return None def _accumulate_response(self, line): "for FSM" if self.command and self.command[2] is not None: self.command[2](line[4:]) else: self.response += (line[4:] + '\n') return None def _is_finish_line(self, line): "for FSM" # print "isFinish",line if len(line) < 1: return False if line[0] == '.': return True if len(line) > 3 and line[3] == ' ': return True return False def _broadcast_response(self, line): "for FSM" # print "BCAST",line if len(line) > 3: if self.code >= 200 and self.code < 300 and \ self.command and self.command[2] is not None: self.command[2](line[4:]) resp = '' else: resp = self.response + line[4:] else: resp = self.response self.response = '' if self.code >= 200 and self.code < 300: if self.defer is None: raise RuntimeError( 'Got a response, but didn\'t issue a command: "%s"' % resp ) if resp.endswith('\nOK'): resp = resp[:-3] self.defer.callback(resp) elif self.code >= 500 and self.code < 600: err = TorProtocolError(self.code, resp) self.defer.errback(err) elif self.code >= 600 and self.code < 700: self._handle_notify(self.code, resp) self.code = None return elif self.code is None: raise RuntimeError("No code set yet in broadcast response.") else: raise RuntimeError( "Unknown code in broadcast response %d." % self.code ) # note: we don't do this for 600-level responses self.command = None self.code = None self.defer = None self._maybe_issue_command() return None txtorcon-0.14.2/txtorcon/util.py0000644000175000017500000002011612627744056016576 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import with_statement import glob import os import hmac import hashlib import shutil import socket import subprocess import struct from twisted.internet import defer from twisted.internet.interfaces import IProtocolFactory from twisted.internet.endpoints import serverFromString from zope.interface import implementer try: import GeoIP as _GeoIP GeoIP = _GeoIP except ImportError: GeoIP = None city = None country = None asn = None # XXX probably better to depend on and use "six" for py2/3 stuff? try: unicode except NameError: py3k = True basestring = str else: py3k = False basestring = basestring def create_geoip(fname): # It's more "pythonic" to just wait for the exception, # but GeoIP prints out "Can't open..." messages for you, # which isn't desired here if not os.path.isfile(fname): raise IOError("Can't find %s" % fname) if GeoIP is None: return None # just letting any errors make it out return GeoIP.open(fname, GeoIP.GEOIP_STANDARD) def maybe_create_db(path): try: return create_geoip(path) except IOError: return None city, asn, country = list(map(maybe_create_db, ("/usr/share/GeoIP/GeoLiteCity.dat", "/usr/share/GeoIP/GeoIPASNum.dat", "/usr/share/GeoIP/GeoIP.dat"))) try: import ipaddr as _ipaddr ipaddr = _ipaddr except ImportError: ipaddr = None def is_executable(path): """Checks if the given path points to an existing, executable file""" return os.path.isfile(path) and os.access(path, os.X_OK) def find_tor_binary(globs=('/usr/sbin/', '/usr/bin/', '/Applications/TorBrowser_*.app/Contents/MacOS/'), system_tor=True): """ Tries to find the tor executable using the shell first or in in the paths whose glob-patterns is in the given 'globs'-tuple. :param globs: A tuple of shell-style globs of directories to use to find tor (TODO consider making that globs to actual tor binary?) :param system_tor: This controls whether bash is used to seach for 'tor' or not. If False, we skip that check and use only the 'globs' tuple. """ # Try to find the tor executable using the shell if system_tor: try: proc = subprocess.Popen( ('which tor'), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True ) except OSError: pass else: stdout, _ = proc.communicate() if proc.poll() == 0 and stdout != '': return stdout.strip() # the shell may not provide type and tor is usually not on PATH when using # the browser-bundle. Look in specific places for pattern in globs: for path in glob.glob(pattern): torbin = os.path.join(path, 'tor') if is_executable(torbin): return torbin return None def maybe_ip_addr(addr): """ Tries to return an IPAddress, otherwise returns a string. TODO consider explicitly checking for .exit or .onion at the end? """ if ipaddr is not None: try: return ipaddr.IPAddress(addr) except ValueError: pass return str(addr) def find_keywords(args, key_filter=lambda x: not x.startswith("$")): """ This splits up strings like name=value, foo=bar into a dict. Does NOT deal with quotes in value (e.g. key="value with space" will not work By default, note that it takes OUT any key which starts with $ (i.e. a single dollar sign) since for many use-cases the way Tor encodes nodes with "$hash=name" looks like a keyword argument (but it isn't). If you don't want this, override the "key_filter" argument to this method. :return: a dict of key->value (both strings) of all name=value type keywords found in args. """ filtered = [x for x in args if '=' in x and key_filter(x.split('=')[0])] return dict(x.split('=', 1) for x in filtered) def delete_file_or_tree(*args): """ For every path in args, try to delete it as a file or a directory tree. Ignores deletion errors. """ for f in args: try: os.unlink(f) except OSError: shutil.rmtree(f, ignore_errors=True) def ip_from_int(ip): """ Convert long int back to dotted quad string """ return socket.inet_ntoa(struct.pack('>I', ip)) def process_from_address(addr, port, torstate=None): """ Determines the PID from the address/port provided by using lsof and returns it as an int (or None if it couldn't be determined). In the special case the addr is '(Tor_internal)' then the PID of the Tor process (as gotten from the torstate object) is returned (or 0 if unavailable, e.g. a Tor which doesn't implement 'GETINFO process/pid'). In this case if no TorState instance is given, None is returned. """ if addr is None: return None if "(tor_internal)" == str(addr).lower(): if torstate is None: return None return int(torstate.tor_pid) proc = subprocess.Popen(['lsof', '-i', '4tcp@%s:%s' % (addr, port)], stdout=subprocess.PIPE) (stdout, stderr) = proc.communicate() lines = stdout.split('\n') if len(lines) > 1: return int(lines[1].split()[1]) def hmac_sha256(key, msg): """ Adapted from rransom's tor-utils git repository. Returns the digest (binary) of an HMAC with SHA256 over msg with key. """ return hmac.new(key, msg, hashlib.sha256).digest() CRYPTOVARIABLE_EQUALITY_COMPARISON_NONCE = os.urandom(32) def compare_via_hash(x, y): """ Taken from rransom's tor-utils git repository, to compare two hashes in something resembling constant time (or at least, not leaking timing info?) """ return (hmac_sha256(CRYPTOVARIABLE_EQUALITY_COMPARISON_NONCE, x) == hmac_sha256(CRYPTOVARIABLE_EQUALITY_COMPARISON_NONCE, y)) class NetLocation: """ Represents the location of an IP address, either city or country level resolution depending on what GeoIP database was loaded. If the ASN database is available you get that also. """ def __init__(self, ipaddr): "ipaddr should be a dotted-quad" self.ip = ipaddr self.latlng = (None, None) self.countrycode = None self.city = None self.asn = None if self.ip is None or self.ip == 'unknown': return if city: try: r = city.record_by_addr(self.ip) except: r = None if r is not None: self.countrycode = r['country_code'] self.latlng = (r['latitude'], r['longitude']) try: self.city = (r['city'], r['region_code']) except KeyError: self.city = (r['city'], r['region_name']) elif country: self.countrycode = country.country_code_by_addr(ipaddr) else: self.countrycode = '' if asn: try: self.asn = asn.org_by_addr(self.ip) except: self.asn = None @implementer(IProtocolFactory) class NoOpProtocolFactory: """ This is an IProtocolFactory that does nothing. Used for testing, and for :method:`available_tcp_port` """ def noop(self, *args, **kw): pass buildProtocol = noop doStart = noop doStop = noop @defer.inlineCallbacks def available_tcp_port(reactor): """ Returns a Deferred firing an available TCP port on localhost. It does so by listening on port 0; then stopListening and fires the assigned port number. """ endpoint = serverFromString(reactor, 'tcp:0:interface=127.0.0.1') port = yield endpoint.listen(NoOpProtocolFactory()) address = port.getHost() yield port.stopListening() defer.returnValue(address.port) txtorcon-0.14.2/txtorcon/log.py0000644000175000017500000000107112605552415016371 0ustar mikemike00000000000000# -*- coding: utf-8 -*- """ This module handles txtorcon debug messages. """ from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals from __future__ import with_statement from twisted.python import log as twlog __all__ = ['txtorlog'] txtorlog = twlog.LogPublisher() def debug_logging(): stdobserver = twlog.PythonLoggingObserver('txtorcon') fileobserver = twlog.FileLogObserver(open('txtorcon.log', 'w')) txtorlog.addObserver(stdobserver.emit) txtorlog.addObserver(fileobserver.emit) txtorcon-0.14.2/txtorcon/interface.py0000644000175000017500000002746112605552415017563 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals from __future__ import with_statement from zope.interface import implementer from zope.interface import Interface, Attribute class IStreamListener(Interface): """ Notifications about changes to a :class:`txtorcon.Stream`. If you wish for your listener to be added to *all* new streams, see :meth:`txtorcon.TorState.add_stream_listener`. """ def stream_new(stream): "a new stream has been created" def stream_succeeded(stream): "stream has succeeded" def stream_attach(stream, circuit): "the stream has been attached to a circuit" def stream_detach(stream, **kw): """ the stream has been detached from its circuit :param kw: provides any flags for this event, which will include at least REASON (but may include anything). See control-spec. """ def stream_closed(stream, **kw): """ stream has been closed (won't be in controller's list anymore). :param kw: provides any flags for this event, which will include at least REASON (but may include anything). See control-spec. """ def stream_failed(stream, **kw): """ stream failed for some reason (won't be in controller's list anymore). :param kw: a dict of all the flags for the stream failure; see control-spec but these will include REASON and sometimes REMOTE_REASON (if the remote Tor closed the connection). Both an all-uppercase and all-lowercase version of each keyword is supplied (by the library; Tor provides all-uppercase only). Others may include BUILD_FLAGS, PURPOSE, HS_STATE, REND_QUERY, TIME_CREATED (or anything else). """ @implementer(IStreamListener) class StreamListenerMixin(object): """ Implements all of :class:`txtorcon.IStreamListener` with no-op methods. You may subclass from this if you don't care about most of the notifications. """ def stream_new(self, stream): pass def stream_succeeded(self, stream): pass def stream_attach(self, stream, circuit): pass def stream_detach(self, stream, **kw): pass def stream_closed(self, stream, **kw): pass def stream_failed(self, stream, **kw): pass class IStreamAttacher(Interface): """ Used by :class:`txtorcon.TorState` to map streams to circuits (see :meth:`txtorcon.TorState.set_attacher`). Each time a new :class:`txtorcon.Stream` is created, this interface will be queried to find out which :class:`txtorcon.Circuit` it should be attached to. """ def attach_stream(stream, circuits): """ :param stream: The stream to attach, which will be in NEW or NEWRESOLVE state. :param circuits: all currently available :class:`txtorcon.Circuit` objects in the :class:`txtorcon.TorState` in a dict indexed by id. Note they are *not* limited to BUILT circuits. You should return a :class:`txtorcon.Circuit` instance which should be at state BUILT in the currently running Tor. You may also return a Deferred which will callback with the desired circuit. In this case, you will probably need to be aware that the callback from :meth:`txtorcon.TorState.build_circuit` does not wait for the circuit to be in BUILT state. See :ref:`attach_streams_by_country.py` for a complete example of using a Deferred in an IStreamAttacher. Alternatively, you may return None in which case the Tor controller will be told to choose a circuit itself. Note that Tor will refuse to attach to any circuit not in BUILT state; see ATTACHSTREAM in control-spec.txt Note also that although you get a request to attach a stream that ends in .onion Tor doesn't currently let you specify how to attach .onion addresses and will always give a 551 error. """ class ICircuitContainer(Interface): """ An interface that contains a bunch of Circuit objects and can look them up by id. """ def find_circuit(id): ":return: a circuit for the id, or exception." def close_circuit(circuit, **kwargs): """ Close a circuit. :return: a Deferred which callbacks when the closing process is started (not necessarily finished inside Tor). """ # FIXME do we need an IStreamContainer that Stream instances get? # (Currently, they get an ICircuitContainer...) def close_stream(stream, **kwargs): """ Close a stream. :return: a Deferred which callbacks when the closing process is started (not necessarily finished inside Tor). """ class ICircuitListener(Interface): """ An interface to listen for updates to Circuits. """ def circuit_new(circuit): """A new circuit has been created. You'll always get one of these for every Circuit even if it doesn't go through the "launched" state.""" def circuit_launched(circuit): "A new circuit has been started." def circuit_extend(circuit, router): "A circuit has been extended to include a new router hop." def circuit_built(circuit): """ A circuit has been extended to all hops (usually 3 for user circuits). """ def circuit_closed(circuit, **kw): """ A circuit has been closed cleanly (won't be in controller's list any more). :param kw: A dict of additional args. REASON is alsways included, and often REMOTE_REASON also. See the control-spec documentation. As of this writing, REASON is one of the following strings: MISC, RESOLVEFAILED, CONNECTREFUSED, EXITPOLICY, DESTROY, DONE, TIMEOUT, NOROUTE, HIBERNATING, INTERNAL,RESOURCELIMIT, CONNRESET, TORPROTOCOL, NOTDIRECTORY, END, PRIVATE_ADDR. However, don't depend on that: it could be anything. To facilitate declaring args you want in the method (e.g. circuit_failed(self, circuit, reason=None, remote_reason=None, **kw)) lower-case versions of all the keys are also provided (pointing to the same -- usually UPPERCASE -- strings as the upper-case keys). """ def circuit_failed(circuit, **kw): """ A circuit has been closed because something went wrong. The circuit won't be in the TorState's list anymore. :param kw: A dict of additional args. REASON is alsways included, and often REMOTE_REASON also. See the control-spec documentation. As of this writing, REASON is one of the following strings: MISC, RESOLVEFAILED, CONNECTREFUSED, EXITPOLICY, DESTROY, DONE, TIMEOUT, NOROUTE, HIBERNATING, INTERNAL,RESOURCELIMIT, CONNRESET, TORPROTOCOL, NOTDIRECTORY, END, PRIVATE_ADDR. However, don't depend on that: it could be anything. To facilitate declaring args you want in the method (e.g. circuit_failed(self, circuit, reason=None, remote_reason=None, **kw)) lower-case versions of all the keys are also provided (pointing to the same -- usually UPPERCASE -- strings as the upper-case keys). """ @implementer(ICircuitListener) class CircuitListenerMixin(object): """ Implements all of ICircuitListener with no-op methods. Subclass from this if you don't care about most of the notifications. """ def circuit_new(self, circuit): pass def circuit_launched(self, circuit): pass def circuit_extend(self, circuit, router): pass def circuit_built(self, circuit): pass def circuit_closed(self, circuit, **kw): pass def circuit_failed(self, circuit, **kw): pass class ITorControlProtocol(Interface): """ This defines the API to the TorController object. This is the usual entry-point to this library, and you shouldn't need to call methods outside this interface. """ def get_info(info): """ :return: a Deferred which will callback with the info keys you asked for. For values ones, see control-spec. """ def get_conf(*args): """ Returns one or many configuration values via Deferred. See control-spec for valid keys. The value will be a dictionary. """ def signal(signal_name): """ Issues a signal to Tor. See control-spec or .valid_signals for which ones are available and their return values. """ def build_circuit(routers): """ Builds a circuit consisting of exactly the routers specified, in order. This issues a series of EXTENDCIRCUIT calls to Tor; the deferred returned from this is for the final EXTEND. FIXME: should return the Circuit instance, but currently returns final extend message 'EXTEND 1234' for example. """ def close_circuit(circuit): """ Asks Tor to close the circuit. Note that the Circuit instance is only removed as a result of the next CIRC CLOSED event. The Deferred returned from this method callbacks when the CLOSECIRCUIT command has successfully executed, not when the circuit is actually gone. If you wish to know when this circuit is actually gone, add an ICircuitListener and wait for circuit_closed() """ def add_circuit_listener(icircuitlistener): """ Add an implementor of :class:`txtorcon.interface.ICircuitListener` which will be added to all new circuits as well as all existing ones (you won't, however, get circuit_new calls for the existing ones) """ def add_stream_listener(istreamlistener): """ Add an implementor of :class:`txtorcon.interface.IStreamListener` which will be added to all new circuits as well as all existing ones (you won't, however, get stream_new calls for the existing ones) """ def add_event_listener(evt, callback): """ Add a listener to an Event object. This may be called multiple times for the same event. Every time the event happens, the callback method will be called. The callback has one argument (a string, the contents of the event, minus the '650' and the name of the event) FIXME: should have an interface for the callback. """ class IRouterContainer(Interface): unique_routers = Attribute("contains a list of all the Router instances") def router_from_id(routerid): """ Note that this method MUST always return a Router instance -- if you ask for a router ID that didn't yet exist, it is created (although without IP addresses and such because it wasn't in the consensus). You may find out if a Router came from the 'GETINFO ns/all' list by checking the from_consensus attribute. This is to simplify code like in Circuit.update() that needs to handle the case where an EXTENDED circuit event is the only time we've seen a Router -- it's possible for Tor to do things with routers not in the consensus (like extend circuits to them). :return: a router by its ID. """ class IAddrListener(Interface): def addrmap_added(addr): """ A new address was added to the address map. """ def addrmap_expired(name): """ An address has expired from the address map. """ txtorcon-0.14.2/txtorcon/torstate.py0000644000175000017500000010405112627744056017467 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import with_statement import collections import datetime import os import stat import types import warnings from twisted.python import log from twisted.internet import defer from twisted.internet.endpoints import TCP4ClientEndpoint from twisted.internet.endpoints import UNIXClientEndpoint from twisted.internet.interfaces import IReactorCore from twisted.internet.interfaces import IStreamClientEndpoint from zope.interface import implementer from txtorcon import TorProtocolFactory from txtorcon.stream import Stream from txtorcon.circuit import Circuit from txtorcon.router import Router, hashFromHexId from txtorcon.addrmap import AddrMap from txtorcon.torcontrolprotocol import parse_keywords from txtorcon.log import txtorlog from txtorcon.torcontrolprotocol import TorProtocolError from txtorcon.interface import ITorControlProtocol from txtorcon.interface import IRouterContainer from txtorcon.interface import ICircuitListener from txtorcon.interface import ICircuitContainer from txtorcon.interface import IStreamListener from txtorcon.interface import IStreamAttacher from .spaghetti import FSM, State, Transition from .util import basestring def _build_state(proto): state = TorState(proto) return state.post_bootstrap def _wait_for_proto(proto): return proto.post_bootstrap def build_tor_connection(connection, build_state=True, wait_for_proto=True, password_function=lambda: None): """ This is used to build a valid TorState (which has .protocol for the TorControlProtocol). For example:: from twisted.internet import reactor from twisted.internet.endpoints import TCP4ClientEndpoint import txtorcon def example(state): print "Fully bootstrapped state:",state print " with bootstrapped protocol:",state.protocol d = txtorcon.build_tor_connection(TCP4ClientEndpoint(reactor, "localhost", 9051)) d.addCallback(example) reactor.run() :param password_function: See :class:`txtorcon.TorControlProtocol` :param build_state: If True (the default) a TorState object will be built as well. If False, just a TorControlProtocol will be returned via the Deferred. :return: a Deferred that fires with a TorControlProtocol or, if you specified build_state=True, a TorState. In both cases, the object has finished bootstrapping (i.e. TorControlProtocol.post_bootstrap or TorState.post_bootstap has fired, as needed) """ if IStreamClientEndpoint.providedBy(connection): endpoint = connection elif isinstance(connection, tuple): if len(connection) == 2: reactor, socket = connection if (os.path.exists(socket) and os.stat(socket).st_mode & (stat.S_IRGRP | stat.S_IRUSR | stat.S_IROTH)): endpoint = UNIXClientEndpoint(reactor, socket) else: raise ValueError('Can\'t use "%s" as a socket' % (socket, )) elif len(connection) == 3: endpoint = TCP4ClientEndpoint(*connection) else: raise TypeError('Expected either a (reactor, socket)- or a ' '(reactor, host, port)-tuple for argument ' '"connection", got %s' % (connection, )) else: raise TypeError('Expected a (reactor, socket)- or a (reactor, host, ' 'port)-tuple or an object implementing IStreamClient' 'Endpoint for argument "connection", got %s' % (connection, )) d = endpoint.connect( TorProtocolFactory( password_function=password_function ) ) if build_state: d.addCallback(build_state if isinstance(build_state, collections.Callable) else _build_state) elif wait_for_proto: d.addCallback(wait_for_proto if isinstance(wait_for_proto, collections.Callable) else _wait_for_proto) return d def build_local_tor_connection(reactor, host='127.0.0.1', port=9051, socket='/var/run/tor/control', *args, **kwargs): """ This builds a connection to a local Tor, either via 127.0.0.1:9051 (which is tried first) or /var/run/tor/control (by default). See also :meth:`build_tor_connection ` for other key-word arguments that are accepted here also. :param host: An IP address to find Tor at. Corresponds to the ControlListenAddress torrc option. :param port: The port to use with the address when trying to contact Tor. This corresponds to the ControlPort option in torrc (default is 9051). """ try: return build_tor_connection((reactor, socket), *args, **kwargs) except: return build_tor_connection((reactor, host, port), *args, **kwargs) def flags_from_dict(kw): """ This turns a dict with keys that are flags (e.g. for CLOSECIRCUIT, CLOSESTREAM) only if the values are true. """ if len(kw) == 0: return '' flags = '' for (k, v) in kw.items(): if v: flags += ' ' + str(k) # note that we want the leading space if there's at least one # flag. return flags @implementer(ICircuitListener) @implementer(ICircuitContainer) @implementer(IRouterContainer) @implementer(IStreamListener) class TorState(object): """ This tracks the current state of Tor using a TorControlProtocol. On setup it first queries the initial state of streams and circuits. It then asks for updates via the listeners. It requires an ITorControlProtocol instance. The control protocol doesn't need to be bootstrapped yet. The Deferred .post_boostrap is driggered when the TorState instance is fully ready to go. The easiest way is to use the helper method :func:`txtorcon.build_tor_connection`. For details, see the implementation of that. You may add an :class:`txtorcon.interface.IStreamAttacher` to provide a custom mapping for Strams to Circuits (by default Tor picks by itself). This is also a good example of the various listeners, and acts as an :class:`txtorcon.interface.ICircuitContainer` and :class:`txtorcon.interface.IRouterContainer`. :cvar DO_NOT_ATTACH: Constant to return from an IAttacher indicating you don't want to attach this stream at all. """ @classmethod def from_protocol(cls, protocol, **kw): ''' Create a new, boot-strapped TorState from a TorControlProtocol instance. :return: a Deferred that fires with a TorState instance ''' state = TorState(protocol, bootstrap=True) return state.post_bootstrap def __init__(self, protocol, bootstrap=True): self.protocol = ITorControlProtocol(protocol) # fixme could use protocol.on_disconnect to re-connect; see issue #3 # could override these to get your own Circuit/Stream subclasses # to track these things self.circuit_factory = Circuit self.stream_factory = Stream self.attacher = None """If set, provides :class:`txtorcon.interface.IStreamAttacher` to attach new streams we hear about.""" self.tor_binary = 'tor' self.circuit_listeners = [] self.stream_listeners = [] self.addrmap = AddrMap() self.circuits = {} # keys on id (integer) self.streams = {} # keys on id (integer) self.all_routers = set() # list of unique routers self.routers = {} # keys by hexid (string) and by unique names self.routers_by_name = {} # keys on name, value always list (many duplicate "Unnamed" routers, for example) self.routers_by_hash = {} # keys by hexid (string) self.guards = {} # potentially-usable as entry guards, I think? (any router with 'Guard' flag) self.entry_guards = {} # from GETINFO entry-guards, our current entry guards self.unusable_entry_guards = [] # list of entry guards we didn't parse out self.authorities = {} # keys by name self.cleanup = None # see set_attacher class die(object): __name__ = 'die' # FIXME? just to ease spagetti.py:82's pain def __init__(self, msg): self.msg = msg def __call__(self, *args): raise RuntimeError(self.msg % tuple(args)) waiting_r = State("waiting_r") waiting_w = State("waiting_w") waiting_p = State("waiting_p") waiting_s = State("waiting_s") def ignorable_line(x): x = x.strip() return x in ['.', 'OK', ''] or x.startswith('ns/') waiting_r.add_transition(Transition(waiting_r, ignorable_line, None)) waiting_r.add_transition(Transition(waiting_s, lambda x: x.startswith('r '), self._router_begin)) # FIXME use better method/func than die!! waiting_r.add_transition(Transition(waiting_r, lambda x: not x.startswith('r '), die('Expected "r " while parsing routers not "%s"'))) waiting_s.add_transition(Transition(waiting_w, lambda x: x.startswith('s '), self._router_flags)) waiting_s.add_transition(Transition(waiting_s, lambda x: x.startswith('a '), self._router_address)) waiting_s.add_transition(Transition(waiting_r, ignorable_line, None)) waiting_s.add_transition(Transition(waiting_r, lambda x: not x.startswith('s ') and not x.startswith('a '), die('Expected "s " while parsing routers not "%s"'))) waiting_s.add_transition(Transition(waiting_r, lambda x: x.strip() == '.', None)) waiting_w.add_transition(Transition(waiting_p, lambda x: x.startswith('w '), self._router_bandwidth)) waiting_w.add_transition(Transition(waiting_r, ignorable_line, None)) waiting_w.add_transition(Transition(waiting_s, lambda x: x.startswith('r '), self._router_begin)) # "w" lines are optional waiting_w.add_transition(Transition(waiting_r, lambda x: not x.startswith('w '), die('Expected "w " while parsing routers not "%s"'))) waiting_w.add_transition(Transition(waiting_r, lambda x: x.strip() == '.', None)) waiting_p.add_transition(Transition(waiting_r, lambda x: x.startswith('p '), self._router_policy)) waiting_p.add_transition(Transition(waiting_r, ignorable_line, None)) waiting_p.add_transition(Transition(waiting_s, lambda x: x.startswith('r '), self._router_begin)) # "p" lines are optional waiting_p.add_transition(Transition(waiting_r, lambda x: x[:2] != 'p ', die('Expected "p " while parsing routers not "%s"'))) waiting_p.add_transition(Transition(waiting_r, lambda x: x.strip() == '.', None)) self._network_status_parser = FSM([waiting_r, waiting_s, waiting_w, waiting_p]) self.post_bootstrap = defer.Deferred() if bootstrap: self.protocol.post_bootstrap.addCallback(self._bootstrap) self.protocol.post_bootstrap.addErrback(self.post_bootstrap.errback) def _router_begin(self, data): args = data.split() self._router = Router(self.protocol) self._router.from_consensus = True self._router.update( args[1], # nickname args[2], # idhash args[3], # orhash args[4] + ' ' + args[5], # modified (like '%Y-%m-%f %H:%M:%S') args[6], # ip address args[7], # ORPort args[8], # DirPort ) if self._router.id_hex in self.routers: # FIXME should I do an update() on this one?? self._router = self.routers[self._router.id_hex] return if self._router.name in self.routers_by_name: self.routers_by_name[self._router.name].append(self._router) else: self.routers_by_name[self._router.name] = [self._router] if self._router.name in self.routers: self.routers[self._router.name] = None else: self.routers[self._router.name] = self._router self.routers[self._router.id_hex] = self._router self.routers_by_hash[self._router.id_hex] = self._router self.all_routers.add(self._router) def _router_flags(self, data): args = data.split() self._router.flags = args[1:] if 'guard' in self._router.flags: self.guards[self._router.id_hex] = self._router if 'authority' in self._router.flags: self.authorities[self._router.name] = self._router def _router_address(self, data): """only for IPv6 addresses""" self._router.ip_v6.append(data.split()[1].strip()) def _router_bandwidth(self, data): args = data.split() self._router.bandwidth = int(args[1].split('=')[1]) def _router_policy(self, data): args = data.split() self._router.policy = args[1:] self._router = None @defer.inlineCallbacks def _bootstrap(self, arg=None): "This takes an arg so we can use it as a callback (see __init__)." # update list of routers (must be before we do the # circuit-status) note that we're feeding each line # incrementally to a state-machine called # _network_status_parser, set up in constructor. "ns" should # be the empty string, but we call _update_network_status for # the de-duplication of named routers ns = yield self.protocol.get_info_incremental( 'ns/all', self._network_status_parser.process ) self._update_network_status(ns) # update list of existing circuits cs = yield self.protocol.get_info_raw('circuit-status') self._circuit_status(cs) # update list of streams ss = yield self.protocol.get_info_raw('stream-status') self._stream_status(ss) # update list of existing address-maps key = 'address-mappings/all' am = yield self.protocol.get_info_raw(key) # strip addressmappsings/all= and OK\n from raw data am = am[len(key) + 1:] for line in am.split('\n'): if len(line.strip()) == 0: continue # FIXME self.addrmap.update(line) self._add_events() entries = yield self.protocol.get_info_raw("entry-guards") for line in entries.split('\n')[1:]: if len(line.strip()) == 0 or line.strip() == 'OK': # XXX does this ever really happen? continue args = line.split() (name, status) = args[:2] name = name[:41] # this is sometimes redundant, as a missing entry guard # usually means it won't be in our list of routers right # now, but just being on the safe side if status.lower() != 'up': self.unusable_entry_guards.append(line) continue try: self.entry_guards[name] = self.router_from_id(name) except KeyError: self.unusable_entry_guards.append(line) # in case process/pid doesn't exist and we don't know the PID # because we own it, we just leave it as 0 (previously # guessed using psutil, but that only works if there's # exactly one tor running anyway) try: pid = yield self.protocol.get_info_raw("process/pid") except TorProtocolError: pid = None self.tor_pid = 0 if pid: try: pid = parse_keywords(pid)['process/pid'] self.tor_pid = int(pid) except: self.tor_pid = 0 if not self.tor_pid and self.protocol.is_owned: self.tor_pid = self.protocol.is_owned self.post_bootstrap.callback(self) self.post_boostrap = None def undo_attacher(self): """ Shouldn't Tor handle this by turning this back to 0 if the controller that twiddled it disconnects? """ return self.protocol.set_conf("__LeaveStreamsUnattached", 0) def set_attacher(self, attacher, myreactor): """ Provide an :class:`txtorcon.interface.IStreamAttacher` to associate streams to circuits. This won't get turned on until after bootstrapping is completed. ('__LeaveStreamsUnattached' needs to be set to '1' and the existing circuits list needs to be populated). """ react = IReactorCore(myreactor) if attacher: self.attacher = IStreamAttacher(attacher) else: self.attacher = None if self.attacher is None: d = self.undo_attacher() if self.cleanup: react.removeSystemEventTrigger(self.cleanup) self.cleanup = None else: d = self.protocol.set_conf("__LeaveStreamsUnattached", "1") self.cleanup = react.addSystemEventTrigger('before', 'shutdown', self.undo_attacher) return d stream_close_reasons = { 'REASON_MISC': 1, # (catch-all for unlisted reasons) 'REASON_RESOLVEFAILED': 2, # (couldn't look up hostname) 'REASON_CONNECTREFUSED': 3, # (remote host refused connection) [*] 'REASON_EXITPOLICY': 4, # (OR refuses to connect to host or port) 'REASON_DESTROY': 5, # (Circuit is being destroyed) 'REASON_DONE': 6, # (Anonymized TCP connection was closed) 'REASON_TIMEOUT': 7, # (Connection timed out, or OR timed out while connecting) 'REASON_NOROUTE': 8, # (Routing error while attempting to contact destination) 'REASON_HIBERNATING': 9, # (OR is temporarily hibernating) 'REASON_INTERNAL': 10, # (Internal error at the OR) 'REASON_RESOURCELIMIT': 11, # (OR has no resources to fulfill request) 'REASON_CONNRESET': 12, # (Connection was unexpectedly reset) 'REASON_TORPROTOCOL': 13, # (Sent when closing connection because of Tor protocol violations.) 'REASON_NOTDIRECTORY': 14} # (Client sent RELAY_BEGIN_DIR to a non-directory relay.) def close_stream(self, stream, reason='REASON_MISC', **kwargs): """ This sends a STREAMCLOSE command, using the specified reason (either an int or one of the 14 strings in section 6.3 of tor-spec.txt if the argument is a string). Any kwards are passed through as flags if they evaluated to true (e.g. "SomeFlag=True"). Currently there are none that Tor accepts. """ if type(stream) != int: # assume it's a Stream instance stream = stream.id try: reason = int(reason) except ValueError: try: reason = TorState.stream_close_reasons[reason] except KeyError: raise ValueError( 'Unknown stream close reason "%s"' % str(reason) ) flags = flags_from_dict(kwargs) # stream is now an ID no matter what we passed in cmd = 'CLOSESTREAM %d %d%s' % (stream, reason, flags) return self.protocol.queue_command(cmd) def close_circuit(self, circid, **kwargs): """ This sends a CLOSECIRCUIT command, using any keyword arguments passed as the Flags (currently, that is just 'IfUnused' which means to only close the circuit when it is no longer used by any streams). :param circid: Either a circuit-id (int) or a Circuit instance :return: a Deferred which callbacks with the result of queuing the command to Tor (usually "OK"). If you want to instead know when the circuit is actually-gone, see :meth:`Circuit.close ` """ if type(circid) != int: # assume it's a Circuit instance circid = circid.id flags = flags_from_dict(kwargs) return self.protocol.queue_command( 'CLOSECIRCUIT %s%s' % (circid, flags) ) def add_circuit_listener(self, icircuitlistener): listen = ICircuitListener(icircuitlistener) for circ in self.circuits.values(): circ.listen(listen) self.circuit_listeners.append(listen) def add_stream_listener(self, istreamlistener): listen = IStreamListener(istreamlistener) for stream in self.streams.values(): stream.listen(listen) self.stream_listeners.append(listen) def _find_circuit_after_extend(self, x): ex, circ_id = x.split() if ex != 'EXTENDED': raise RuntimeError('Expected EXTENDED, got "%s"' % x) circ_id = int(circ_id) circ = self._maybe_create_circuit(circ_id) circ.update([str(circ_id), 'EXTENDED']) return circ def build_circuit(self, routers=None, using_guards=True): """ Builds a circuit consisting of exactly the routers specified, in order. This issues an EXTENDCIRCUIT call to Tor with all the routers specified. :param routers: a list of Router instances which is the path desired. To allow Tor to choose the routers itself, pass None (the default) for routers. :param using_guards: A warning is issued if the first router isn't in self.entry_guards. :return: A Deferred that will callback with a Circuit instance (with the .id member being valid, and probably nothing else). """ if routers is None or routers == []: cmd = "EXTENDCIRCUIT 0" else: if using_guards and routers[0] not in self.entry_guards.values(): warnings.warn( "Circuit doesn't start with a guard: %s" % routers, RuntimeWarning ) cmd = "EXTENDCIRCUIT 0 " first = True for router in routers: if first: first = False else: cmd += ',' if isinstance(router, basestring) and len(router) == 40 \ and hashFromHexId(router): cmd += router else: cmd += router.id_hex[1:] d = self.protocol.queue_command(cmd) d.addCallback(self._find_circuit_after_extend) return d DO_NOT_ATTACH = object() def _maybe_attach(self, stream): """ If we've got a custom stream-attachment instance (see set_attacher) this will ask it for the appropriate circuit. Note that we ignore .exit URIs and let Tor deal with those (by passing circuit ID 0). The stream attacher is allowed to return a Deferred which will callback with the desired circuit. You may return the special object DO_NOT_ATTACH which will cause the circuit attacher to simply ignore the stream (neither attaching it, nor telling Tor to attach it). """ if self.attacher is None: return None if stream.target_host is not None \ and '.exit' in stream.target_host: # we want to totally ignore .exit URIs as these are # used to specify a particular exit node, and trying # to do STREAMATTACH on them will fail with an error # from Tor anyway. txtorlog.msg("ignore attacher:", stream) return # handle async or sync .attach() the same circ_d = defer.maybeDeferred( self.attacher.attach_stream, stream, self.circuits, ) # actually do the attachment logic; .attach() can return 3 things: # 1. None: let Tor do whatever it wants # 2. DO_NOT_ATTACH: don't attach the stream at all # 3. Circuit instance: attach to the provided circuit def issue_stream_attach(circ): txtorlog.msg("circuit:", circ) if circ is None: # tell Tor to do what it likes return self.protocol.queue_command("ATTACHSTREAM %d 0" % stream.id) elif circ is self.DO_NOT_ATTACH: # do nothing; don't attach the stream return else: # should get a Circuit instance; check it for suitability if not isinstance(circ, Circuit): raise RuntimeError( "IStreamAttacher.attach() must return a Circuit instance " "(or None or DO_NOT_ATTACH): %s" ) if circ.id not in self.circuits: raise RuntimeError( "Attacher returned a circuit unknown to me." ) if circ.state != 'BUILT': raise RuntimeError( "Can only attach to BUILT circuits; %d is in %s." % (circ.id, circ.state) ) # we've got a valid Circuit instance; issue the command return self.protocol.queue_command( "ATTACHSTREAM %d %d" % (stream.id, circ.id) ) circ_d.addCallback(issue_stream_attach) circ_d.addErrback(self._attacher_error) return circ_d def _attacher_error(self, fail): """ not ideal, but there's not really a good way to let the caller handler errors :/ since we ultimately call this due to an async request from Tor. Mostly these errors will be logic or syntax errors in the caller's code anyway. tests monkey-patch this to reduce spew """ print("Failure while attaching stream:", fail) return fail def _circuit_status(self, data): """Used internally as a callback for updating Circuit information""" data = data[len('circuit-status='):].split('\n') # sometimes there's a newline after circuit-status= and # sometimes not, so we get rid of it if len(data) and len(data[0].strip()) == 0: data = data[1:] for line in data: self._circuit_update(line) def _stream_status(self, data): "Used internally as a callback for updating Stream information" # there's a slight issue with a single-stream vs >= 2 streams, # in that in the latter case we have a line by itself with # "stream-status=" on it followed by the streams EXCEPT in the # single-stream case which has "stream-status=123 blahblah" # (i.e. the key + value on one line) lines = data.split('\n') if len(lines) == 1: d = lines[0][len('stream-status='):] # if there are actually 0 streams, then there's nothing # left to parse if len(d): self._stream_update(d) else: [self._stream_update(line) for line in lines[1:]] def _update_network_status(self, data): """ Used internally as a callback for updating Router information from NS and NEWCONSENSUS events. """ self.all_routers = set() for line in data.split('\n'): self._network_status_parser.process(line) txtorlog.msg(len(self.routers_by_name), "named routers found.") # remove any names we added that turned out to have dups for (k, v) in self.routers.items(): if v is None: txtorlog.msg(len(self.routers_by_name[k]), "dups:", k) del self.routers[k] txtorlog.msg(len(self.guards), "GUARDs") def _maybe_create_circuit(self, circ_id): if circ_id not in self.circuits: c = self.circuit_factory(self) c.listen(self) [c.listen(x) for x in self.circuit_listeners] else: c = self.circuits[circ_id] return c def _circuit_update(self, line): """ Used internally as a callback to update Circuit information from CIRC events. """ # print("circuit_update", line) args = line.split() circ_id = int(args[0]) c = self._maybe_create_circuit(circ_id) c.update(args) def _stream_update(self, line): """ Used internally as a callback to update Stream information from STREAM events. """ # print("stream_update", line) if line.strip() == 'stream-status=': # this happens if there are no active streams return args = line.split() assert len(args) >= 3 stream_id = int(args[0]) wasnew = False if stream_id not in self.streams: stream = self.stream_factory(self) self.streams[stream_id] = stream stream.listen(self) [stream.listen(x) for x in self.stream_listeners] wasnew = True self.streams[stream_id].update(args) # if the update closed the stream, it won't be in our list # anymore. FIXME: how can we ever hit such a case as the # first update being a CLOSE? if wasnew and stream_id in self.streams: self._maybe_attach(self.streams[stream_id]) def _addr_map(self, addr): "Internal callback to update DNS cache. Listens to ADDRMAP." txtorlog.msg(" --> addr_map", addr) self.addrmap.update(addr) event_map = {'STREAM': _stream_update, 'CIRC': _circuit_update, 'NS': _update_network_status, 'NEWCONSENSUS': _update_network_status, 'ADDRMAP': _addr_map} """event_map used by add_events to map event_name -> unbound method""" @defer.inlineCallbacks def _add_events(self): """ Add listeners for all the events the controller is interested in. """ for (event, func) in self.event_map.items(): # the map contains unbound methods, so we bind them # to self so they call the right thing yield self.protocol.add_event_listener( event, types.MethodType(func, self, TorState) ) # ICircuitContainer def find_circuit(self, circid): "ICircuitContainer API" return self.circuits[circid] # IRouterContainer def router_from_id(self, routerid): """IRouterContainer API""" try: return self.routers[routerid[:41]] except KeyError: if routerid[0] != '$': raise # just re-raise the KeyError router = Router(self.protocol) idhash = routerid[1:41] nick = '' is_named = False if len(routerid) > 41: nick = routerid[42:] is_named = routerid[41] == '=' router.update(nick, hashFromHexId(idhash), '0' * 27, 'unknown', 'unknown', '0', '0') router.name_is_unique = is_named self.routers[router.id_hex] = router return router # implement IStreamListener def stream_new(self, stream): "IStreamListener: a new stream has been created" txtorlog.msg("stream_new", stream) def stream_succeeded(self, stream): "IStreamListener: stream has succeeded" txtorlog.msg("stream_succeeded", stream) def stream_attach(self, stream, circuit): """ IStreamListener: the stream has been attached to a circuit. It seems you get an attach to None followed by an attach to real circuit fairly frequently. Perhaps related to __LeaveStreamsUnattached? """ txtorlog.msg("stream_attach", stream.id, stream.target_host, " -> ", circuit) def stream_detach(self, stream, **kw): """ IStreamListener """ txtorlog.msg("stream_detach", stream.id) def stream_closed(self, stream, **kw): """ IStreamListener: stream has been closed (won't be in controller's list anymore) """ txtorlog.msg("stream_closed", stream.id) del self.streams[stream.id] def stream_failed(self, stream, **kw): """ IStreamListener: stream failed for some reason (won't be in controller's list anymore) """ txtorlog.msg("stream_failed", stream.id) del self.streams[stream.id] # implement ICircuitListener def circuit_launched(self, circuit): "ICircuitListener API" txtorlog.msg("circuit_launched", circuit) self.circuits[circuit.id] = circuit def circuit_extend(self, circuit, router): "ICircuitListener API" txtorlog.msg("circuit_extend:", circuit.id, router) def circuit_built(self, circuit): "ICircuitListener API" txtorlog.msg( "circuit_built:", circuit.id, "->".join("%s.%s" % (x.name, x.location.countrycode) for x in circuit.path), circuit.streams ) def circuit_new(self, circuit): "ICircuitListener API" txtorlog.msg("circuit_new:", circuit.id) self.circuits[circuit.id] = circuit def circuit_destroy(self, circuit): "Used by circuit_closed and circuit_failed (below)" txtorlog.msg("circuit_destroy:", circuit.id) for d in circuit._when_built: d.errback(Exception("Destroying circuit; will never hit BUILT")) del self.circuits[circuit.id] def circuit_closed(self, circuit, **kw): "ICircuitListener API" txtorlog.msg("circuit_closed", circuit) self.circuit_destroy(circuit) def circuit_failed(self, circuit, **kw): "ICircuitListener API" txtorlog.msg("circuit_failed", circuit, str(kw)) self.circuit_destroy(circuit) txtorcon-0.14.2/txtorcon/circuit.py0000644000175000017500000002252512613266556017270 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals from __future__ import with_statement import time import datetime from twisted.python import log from twisted.internet import defer from .interface import IRouterContainer from txtorcon.util import find_keywords # look like "2014-01-25T02:12:14.593772" TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' class Circuit(object): """ Used by :class:`txtorcon.TorState` to represent one of Tor's circuits. This is kept up-to-date by the :class`txtorcon.TorState` that owns it, and individual circuits can be listened to for updates (or listen to every one using :meth:`txtorcon.TorState.add_circuit_listener`) :ivar path: contains a list of :class:`txtorcon.Router` objects representing the path this Circuit takes. Mostly this will be 3 or 4 routers long. Note that internally Tor uses single-hop paths for some things. See also the *purpose* instance-variable. :ivar streams: contains a list of Stream objects representing all streams currently attached to this circuit. :ivar state: contains a string from Tor describing the current state of the stream. From control-spec.txt section 4.1.2, these are: - NEW: New request to connect - NEWRESOLVE: New request to resolve an address - REMAP: Address re-mapped to another - SENTCONNECT: Sent a connect cell along a circuit - SENTRESOLVE: Sent a resolve cell along a circuit - SUCCEEDED: Received a reply; stream established - FAILED: Stream failed and not retriable - CLOSED: Stream closed - DETACHED: Detached from circuit; still retriable :ivar purpose: The reason this circuit was built. Values can currently be one of (but see control-spec.txt 4.1.1): - GENERAL - HS_CLIENT_INTRO - HS_CLIENT_REND - HS_SERVICE_INTRO - HS_SERVICE_REND - TESTING - CONTROLLER For most purposes, you'll want to look at GENERAL circuits only. :ivar id: The ID of this circuit, a number (or None if unset). """ def __init__(self, routercontainer): """ :param routercontainer: should implement :class:`txtorcon.interface.IRouterContainer`. """ self.listeners = [] self.router_container = IRouterContainer(routercontainer) self.torstate = routercontainer self.path = [] self.streams = [] self.purpose = None self.id = None self.state = 'UNKNOWN' self.build_flags = [] self.flags = {} # this is used to hold a Deferred that will callback() when # this circuit is being CLOSED or FAILED. self._closing_deferred = None # caches parsed value for time_created() self._time_created = None # all notifications for when_built self._when_built = [] # XXX backwards-compat for old .is_built for now @property def is_built(self): return self.when_built() def when_built(self): """ Returns a Deferred that is callback()'d (with this Circuit instance) when this circuit hits BUILT. If it's already BUILT when this is called, you get an already-successful Deferred; otherwise, the state must change to BUILT. """ d = defer.Deferred() if self.state == 'BUILT': d.callback(self) else: self._when_built.append(d) return d @property def time_created(self): if self._time_created is not None: return self._time_created if 'TIME_CREATED' in self.flags: # strip off milliseconds t = self.flags['TIME_CREATED'].split('.')[0] tstruct = time.strptime(t, TIME_FORMAT) self._time_created = datetime.datetime(*tstruct[:7]) return self._time_created def listen(self, listener): if listener not in self.listeners: self.listeners.append(listener) def unlisten(self, listener): self.listeners.remove(listener) def close(self, **kw): """ This asks Tor to close the underlying circuit object. See :meth:`txtorcon.torstate.TorState.close_circuit` for details. You may pass keyword arguments to take care of any Flags Tor accepts for the CLOSECIRCUIT command. Currently, this is only "IfUnused". So for example: circ.close(IfUnused=True) :return: Deferred which callbacks with this Circuit instance ONLY after Tor has confirmed it is gone (not simply that the CLOSECIRCUIT command has been queued). This could be a while if you included IfUnused. """ self._closing_deferred = defer.Deferred() def close_command_is_queued(*args): return self._closing_deferred d = self.torstate.close_circuit(self.id, **kw) d.addCallback(close_command_is_queued) return self._closing_deferred def age(self, now=datetime.datetime.utcnow()): """ Returns an integer which is the difference in seconds from 'now' to when this circuit was created. Returns None if there is no created-time. """ if not self.time_created: return None return (now - self.time_created).seconds def _create_flags(self, kw): """ this clones the kw dict, adding a lower-case version of every key (duplicated in stream.py; put in util?) """ flags = {} for k in kw.keys(): flags[k] = kw[k] flags[k.lower()] = kw[k] return flags def update(self, args): # print "Circuit.update:",args if self.id is None: self.id = int(args[0]) [x.circuit_new(self) for x in self.listeners] else: if int(args[0]) != self.id: raise RuntimeError("Update for wrong circuit.") self.state = args[1] kw = find_keywords(args) self.flags = kw if 'PURPOSE' in kw: self.purpose = kw['PURPOSE'] if 'BUILD_FLAGS' in kw: self.build_flags = kw['BUILD_FLAGS'].split(',') if self.state == 'LAUNCHED': self.path = [] [x.circuit_launched(self) for x in self.listeners] else: if self.state != 'FAILED' and self.state != 'CLOSED': if len(args) > 2: self.update_path(args[2].split(',')) if self.state == 'BUILT': [x.circuit_built(self) for x in self.listeners] for d in self._when_built: d.callback(self) self._when_built = [] elif self.state == 'CLOSED': if len(self.streams) > 0: # FIXME it seems this can/does happen if a remote # router crashes or otherwise shuts down a circuit # with streams on it still log.err(RuntimeError("Circuit is %s but still has %d streams" % (self.state, len(self.streams)))) flags = self._create_flags(kw) self.maybe_call_closing_deferred() [x.circuit_closed(self, **flags) for x in self.listeners] elif self.state == 'FAILED': if len(self.streams) > 0: log.err(RuntimeError("Circuit is %s but still has %d streams" % (self.state, len(self.streams)))) flags = self._create_flags(kw) self.maybe_call_closing_deferred() [x.circuit_failed(self, **flags) for x in self.listeners] def maybe_call_closing_deferred(self): """ Used internally to callback on the _closing_deferred if it exists. """ if self._closing_deferred: self._closing_deferred.callback(self) self._closing_deferred = None def update_path(self, path): """ There are EXTENDED messages which don't include any routers at all, and any of the EXTENDED messages may have some arbitrary flags in them. So far, they're all upper-case and none start with $ luckily. The routers in the path should all be LongName-style router names (this depends on them starting with $). For further complication, it's possible to extend a circuit to a router which isn't in the consensus. nickm via #tor thought this might happen in the case of hidden services choosing a rendevouz point not in the current consensus. """ oldpath = self.path self.path = [] for p in path: if p[0] != '$': break # this will create a Router if we give it a router # LongName that doesn't yet exist router = self.router_container.router_from_id(p) self.path.append(router) if len(self.path) > len(oldpath): [x.circuit_extend(self, router) for x in self.listeners] oldpath = self.path def __str__(self): path = ' '.join([x.ip for x in self.path]) return "" % (self.id, self.state, path, self.purpose) txtorcon-0.14.2/txtorcon/torinfo.py0000644000175000017500000002244612605552415017301 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals from __future__ import with_statement import functools from twisted.internet import defer from txtorcon.interface import ITorControlProtocol class MagicContainer(object): """ This merely contains 1 or more methods or further MagicContainer instances; see _do_setup in TorInfo. Once _setup_complete() is called, this behaves differently so that one can get nicer access to GETINFO things from TorInfo -- specifically dir() and so forth pretend that there are only methods/attributes that pertain to actual Tor GETINFO keys. See TorInfo. """ def __init__(self, n): self._txtorcon_name = n self.attrs = {} self._setup = False def _setup_complete(self): self._setup = True def _add_attribute(self, n, v): self.attrs[n] = v def __repr__(self): return object.__getattribute__(self, '_txtorcon_name') def __getitem__(self, idx): return list(object.__getattribute__(self, 'attrs').items())[idx][1] def __len__(self): return len(object.__getattribute__(self, 'attrs')) def __dir__(self): return list(object.__getattribute__(self, 'attrs').keys()) def __getattribute__(self, name): sup = super(MagicContainer, self) if sup.__getattribute__('_setup') is False: return sup.__getattribute__(name) attrs = sup.__getattribute__('attrs') if name == '__members__': return list(attrs.keys()) else: if name.startswith('__'): return sup.__getattribute__(name) try: return attrs[name] except KeyError: if name in ['dump']: return object.__getattribute__(self, name) raise AttributeError(name) def dump(self, prefix): prefix = prefix + '.' + object.__getattribute__(self, '_txtorcon_name') for x in list(object.__getattribute__(self, 'attrs').values()): x.dump(prefix) class ConfigMethod(object): def __init__(self, info_key, protocol, takes_arg=False): self.info_key = info_key self.proto = protocol self.takes_arg = takes_arg def dump(self, prefix): n = self.info_key.replace('/', '.') n = n.replace('-', '_') s = '%s(%s)' % (n, 'arg' if self.takes_arg else '') return s def __call__(self, *args): if self.takes_arg: if len(args) != 1: raise TypeError( '"%s" takes exactly one argument' % self.info_key ) req = '%s/%s' % (self.info_key, str(args[0])) else: if len(args) != 0: raise TypeError('"%s" takes no arguments' % self.info_key) req = self.info_key def stripper(key, arg): # strip "keyname=" # sometimes keyname= is followed by a newline, so final .strip() return arg.strip()[len(key) + 1:].strip() d = self.proto.get_info_raw(req) d.addCallback(functools.partial(stripper, req)) return d def __str__(self): arg = '' if self.takes_arg: arg = 'arg' return '%s(%s)' % (self.info_key.replace('-', '_'), arg) class TorInfo(object): """Implements some attribute magic over top of TorControlProtocol so that all the available GETINFO values are gettable in a little easier fashion. Dashes are replaced by underscores (since dashes aren't valid in method/attribute names for Python). Some of the magic methods will take a single string argument if the corresponding Tor GETINFO would take one (in 'GETINFO info/names' it will end with '/*', and the same in torspec). In either case, the method returns a Deferred which will callback with the requested value, always a string. For example (see also examples/tor_info.py): proto = TorControlProtocol() #... def cb(arg): print arg info = TorInfo(proto) info.traffic.written().addCallback(cb) info.ip_to_country('8.8.8.8').addCallback(cb) For interactive use -- or even checking things progammatically -- TorInfo pretends it only has attributes that coorespond to valid GETINFO calls. So for example, dir(info) will only return all the currently valid top-level things. In the above example this might be ['traffic', 'ip_to_country'] (of course in practice this is a much longer list). And "dir(info.traffic)" might return ['read', 'written'] For something similar to this for configuration (GETCONF, SETCONF) see TorConfig which is quite a lot more complicated (internally) since you can set config items. NOTE that 'GETINFO config/*' is not supported as it's the only case that's not a leaf, but theoretically a method. """ def __init__(self, control, errback=None): self._setup = False self.attrs = {} '''After _setup is True, these are all we show as attributes.''' self.protocol = ITorControlProtocol(control) self.errback = errback self.post_bootstrap = defer.Deferred() if self.protocol.post_bootstrap: self.protocol.post_bootstrap.addCallback(self.bootstrap) else: self.bootstrap() def _add_attribute(self, n, v): self.attrs[n] = v # iterator protocol def __getitem__(self, idx): sup = super(TorInfo, self) if sup.__getattribute__('_setup') is True: return list(object.__getattribute__(self, 'attrs').items())[idx][1] raise TypeError("No __getitem__ until we've setup.") def __len__(self): sup = super(TorInfo, self) if sup.__getattribute__('_setup') is True: return len(object.__getattribute__(self, 'attrs')) raise TypeError("No length until we're setup.") # change our attribute behavior based on the value of _setup def __dir__(self): sup = super(TorInfo, self) if sup.__getattribute__('_setup') is True: return list(sup.__getattribute__('attrs').keys()) return list(sup.__getattribute__('__dict__').keys()) def __getattribute__(self, name): sup = super(TorInfo, self) if sup.__getattribute__('_setup') is False: return sup.__getattribute__(name) attrs = sup.__getattribute__('attrs') if name == '__members__': return list(attrs.keys()) else: try: return attrs[name] except KeyError: if name == 'dump': return object.__getattribute__(self, name) raise AttributeError(name) def bootstrap(self, *args): d = self.protocol.get_info_raw("info/names") d.addCallback(self._do_setup) if self.errback: d.addErrback(self.errback) d.addCallback(self._setup_complete) return d def dump(self): for x in object.__getattribute__(self, 'attrs').values(): x.dump('') def _do_setup(self, data): # FIXME figure out why network-status doesn't work (get # nothing back from Tor it seems, although stem does get an # answer). this is a space-separated list of ~2500 OR id's; # could it be that LineReceiver can't handle it? added_magic = [] for line in data.split('\n'): if line == "info/names=" or line.strip() == '': continue (name, documentation) = line.split(' ', 1) # FIXME think about this -- this is the only case where # there's something that's a directory # (i.e. MagicContainer) AND needs to be a ConfigMethod as # well...but doesn't really seem very useful. Somewhat # simpler to not support this case for now... if name == 'config/*': continue if name.endswith('/*'): # this takes an arg, so make a method bits = name[:-2].split('/') takes_arg = True else: bits = name.split('/') takes_arg = False mine = self for bit in bits[:-1]: bit = bit.replace('-', '_') if bit in mine.attrs: mine = mine.attrs[bit] if not isinstance(mine, MagicContainer): raise RuntimeError( "Already had something: %s for %s" % (bit, name) ) else: c = MagicContainer(bit) added_magic.append(c) mine._add_attribute(bit, c) mine = c n = bits[-1].replace('-', '_') if n in mine.attrs: raise RuntimeError( "Already had something: %s for %s" % (n, name) ) mine._add_attribute(n, ConfigMethod('/'.join(bits), self.protocol, takes_arg)) for c in added_magic: c._setup_complete() return None def _setup_complete(self, *args): pb = self.post_bootstrap self._setup = True pb.callback(self) txtorcon-0.14.2/txtorcon/stream.py0000644000175000017500000002420312605552415017105 0ustar mikemike00000000000000# -*- coding: utf-8 -*- """ Contains an implementation of a :class:`Stream abstraction used by :class:`TorState to represent all streams in Tor's state. There is also an interface called :class:`interface.IStreamListener` for listening for stream updates (see also :meth:`TorState.add_stream_listener`) and the interface called :class:interface.IStreamAttacher` used by :class:`TorState` as a way to attach streams to circuits "by hand" """ from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals from __future__ import with_statement from twisted.python import log from twisted.internet import defer from txtorcon.interface import ICircuitContainer, IStreamListener from txtorcon.util import find_keywords, maybe_ip_addr class Stream(object): """ Represents an active stream in Tor's state (:class:`txtorcon.TorState`). :ivar circuit: Streams will generally be attached to circuits pretty quickly. If they are attached, circuit will be a :class:`txtorcon.Circuit` instance or None if this stream isn't yet attached to a circuit. :ivar state: Tor's idea of the stream's state, one of: - NEW: New request to connect - NEWRESOLVE: New request to resolve an address - REMAP: Address re-mapped to another - SENTCONNECT: Sent a connect cell along a circuit - SENTRESOLVE: Sent a resolve cell along a circuit - SUCCEEDED: Received a reply; stream established - FAILED: Stream failed and not retriable - CLOSED: Stream closed - DETACHED: Detached from circuit; still retriable :ivar target_host: Something like www.example.com -- the host the stream is destined for. :ivar target_port: The port the stream will exit to. :ivar target_addr: Target address, looked up (usually) by Tor (e.g. 127.0.0.1). :ivar id: The ID of this stream, a number (or None if unset). """ def __init__(self, circuitcontainer): """ :param circuitcontainer: an object which implements :class:`interface.ICircuitContainer` """ self.circuit_container = ICircuitContainer(circuitcontainer) # FIXME: Sphinx doesn't seem to understand these variable # docstrings, so consolidate with above if Sphinx is the # answer -- actually it does, so long as the :ivar: things # are never mentioned it seems. self.id = None """An int, Tor's ID for this :class:`txtorcon.Circuit`""" self.state = None """A string, Tor's idea of the state of this :class:`txtorcon.Stream`""" self.target_host = None """Usually a hostname, but sometimes an IP address (e.g. when we query existing state from Tor)""" self.target_addr = None """If available, the IP address we're connecting to (if None, see target_host instead).""" self.target_port = 0 """The port we're connecting to.""" self.circuit = None """If we've attached to a :class:`txtorcon.Circuit`, this will be an instance of :class:`txtorcon.Circuit` (otherwise None).""" self.listeners = [] """A list of all connected :class:`txtorcon.interface.ICircuitListener` instances.""" self.source_addr = None """If available, the address from which this Stream originated (e.g. local process, etc). See get_process() also.""" self.source_port = 0 """If available, the port from which this Stream originated. See get_process() also.""" self.flags = {} """All flags from last update to this Stream. str->str""" self._closing_deferred = None """Internal. Holds Deferred that will callback when this stream is CLOSED, FAILED (or DETACHED??)""" def listen(self, listen): """ Attach an :class:`txtorcon.interface.IStreamListener` to this stream. See also :meth:`txtorcon.TorState.add_stream_listener` to listen to all streams. :param listen: something that knows :class:`txtorcon.interface.IStreamListener` """ listener = IStreamListener(listen) if listener not in self.listeners: self.listeners.append(listener) def unlisten(self, listener): self.listeners.remove(listener) def close(self, **kw): """ This asks Tor to close the underlying stream object. See :meth:`txtorcon.interface.ITorControlProtocol.close_stream` for details. Although Tor currently takes no flags, it allows you to; any keyword arguments are passed through as flags. NOTE that the callback delivered from this method only callbacks after the underlying stream is really destroyed (*not* just when the CLOSESTREAM command has successfully completed). """ self._closing_deferred = defer.Deferred() def close_command_is_queued(*args): return self._closing_deferred d = self.circuit_container.close_stream(self, **kw) d.addCallback(close_command_is_queued) return self._closing_deferred def _create_flags(self, kw): """ this clones the kw dict, adding a lower-case version of every key (duplicated in circuit.py; consider putting in util?) """ flags = {} for k in kw.keys(): flags[k] = kw[k] flags[k.lower()] = flags[k] return flags def update(self, args): # print "update",self.id,args if self.id is None: self.id = int(args[0]) else: if self.id != int(args[0]): raise RuntimeError("Update for wrong stream.") kw = find_keywords(args) self.flags = kw if 'SOURCE_ADDR' in kw: last_colon = kw['SOURCE_ADDR'].rfind(':') self.source_addr = kw['SOURCE_ADDR'][:last_colon] if self.source_addr != '(Tor_internal)': self.source_addr = maybe_ip_addr(self.source_addr) self.source_port = int(kw['SOURCE_ADDR'][last_colon + 1:]) self.state = args[1] # XXX why not using the state-machine stuff? ;) if self.state in ['NEW', 'NEWRESOLVE', 'SUCCEEDED']: if self.target_host is None: last_colon = args[3].rfind(':') self.target_host = args[3][:last_colon] self.target_port = int(args[3][last_colon + 1:]) self.target_port = int(self.target_port) if self.state == 'NEW': if self.circuit is not None: log.err(RuntimeError("Weird: circuit valid in NEW")) [x.stream_new(self) for x in self.listeners] else: [x.stream_succeeded(self) for x in self.listeners] elif self.state == 'REMAP': self.target_addr = maybe_ip_addr(args[3][:args[3].rfind(':')]) elif self.state == 'CLOSED': if self.circuit: self.circuit.streams.remove(self) self.circuit = None self.maybe_call_closing_deferred() flags = self._create_flags(kw) [x.stream_closed(self, **flags) for x in self.listeners] elif self.state == 'FAILED': if self.circuit: self.circuit.streams.remove(self) self.circuit = None self.maybe_call_closing_deferred() # build lower-case version of all flags flags = self._create_flags(kw) [x.stream_failed(self, **flags) for x in self.listeners] elif self.state == 'SENTCONNECT': pass # print 'SENTCONNECT',self,args elif self.state == 'DETACHED': if self.circuit: self.circuit.streams.remove(self) self.circuit = None # FIXME does this count as closed? # self.maybe_call_closing_deferred() flags = self._create_flags(kw) [x.stream_detach(self, **flags) for x in self.listeners] elif self.state in ['NEWRESOLVE', 'SENTRESOLVE']: pass # print self.state, self, args else: raise RuntimeError("Unknown state: %s" % self.state) # see if we attached to a circuit. I believe this only happens # on a SENTCONNECT or REMAP. DETACHED is excluded so we don't # immediately re-add the circuit we just detached from if self.state not in ['CLOSED', 'FAILED', 'DETACHED']: cid = int(args[2]) if cid == 0: if self.circuit and self in self.circuit.streams: self.circuit.streams.remove(self) self.circuit = None else: if self.circuit is None: self.circuit = self.circuit_container.find_circuit(cid) if self not in self.circuit.streams: self.circuit.streams.append(self) for x in self.listeners: x.stream_attach(self, self.circuit) else: if self.circuit.id != cid: log.err( RuntimeError( 'Circuit ID changed from %d to %d.' % (self.circuit.id, cid) ) ) def maybe_call_closing_deferred(self): """ Used internally to callback on the _closing_deferred if it exists. """ if self._closing_deferred: self._closing_deferred.callback(self) self._closing_deferred = None def __str__(self): c = '' if self.circuit: c = 'on %d ' % self.circuit.id return " %s port %d>" % (self.state, self.id, c, self.target_host, str(self.target_addr), self.target_port) txtorcon-0.14.2/txtorcon/__init__.py0000644000175000017500000000533012627745146017362 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals from __future__ import with_statement # for now, this needs to be changed in setup.py also until I find a # better solution __version__ = '0.14.2' __author__ = 'meejah' __contact__ = 'meejah@meejah.ca' __url__ = 'https://github.com/meejah/txtorcon' __license__ = 'MIT' __copyright__ = 'Copyright 2012-2015' from txtorcon.router import Router from txtorcon.circuit import Circuit from txtorcon.stream import Stream from txtorcon.torcontrolprotocol import TorControlProtocol from txtorcon.torcontrolprotocol import TorProtocolError from txtorcon.torcontrolprotocol import TorProtocolFactory from txtorcon.torcontrolprotocol import DEFAULT_VALUE from txtorcon.torstate import TorState from txtorcon.torstate import build_tor_connection from txtorcon.torstate import build_local_tor_connection from txtorcon.torconfig import TorConfig from txtorcon.torconfig import HiddenService from txtorcon.torconfig import TorProcessProtocol from txtorcon.torconfig import launch_tor from txtorcon.torconfig import TorNotFound from txtorcon.torinfo import TorInfo from txtorcon.addrmap import AddrMap from txtorcon.endpoints import TorOnionAddress from txtorcon.endpoints import TorOnionListeningPort from txtorcon.endpoints import TCPHiddenServiceEndpoint from txtorcon.endpoints import TCPHiddenServiceEndpointParser from txtorcon.endpoints import TorClientEndpoint from txtorcon.endpoints import TorClientEndpointStringParser from txtorcon.endpoints import IHiddenService from txtorcon.endpoints import IProgressProvider from txtorcon.endpoints import get_global_tor from . import util from . import interface from txtorcon.interface import * __all__ = ["Router", "Circuit", "Stream", "TorControlProtocol", "TorProtocolError", "TorProtocolFactory", "TorState", "DEFAULT_VALUE", "TorInfo", "build_tor_connection", "build_local_tor_connection", "launch_tor", "TorNotFound", "TorConfig", "HiddenService", "TorProcessProtocol", "TorInfo", "TCPHiddenServiceEndpoint", "TCPHiddenServiceEndpointParser", "TorClientEndpoint", "TorClientEndpointStringParser", "IHiddenService", "IProgressProvider", "TorOnionAddress", "TorOnionListeningPort", "get_global_tor", "AddrMap", "util", "interface", "ITorControlProtocol", "IStreamListener", "IStreamAttacher", "StreamListenerMixin", "ICircuitContainer", "ICircuitListener", "CircuitListenerMixin", "IRouterContainer", "IAddrListener", "IProgressProvider", "IHiddenService", ] txtorcon-0.14.2/txtorcon/addrmap.py0000644000175000017500000000757512605552415017237 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals from __future__ import with_statement from txtorcon.interface import IAddrListener from txtorcon.util import maybe_ip_addr from twisted.internet.interfaces import IReactorTime from twisted.internet import reactor import datetime import shlex class Addr(object): """ One address mapping (e.g. example.com -> 127.0.0.1) """ def __init__(self, map): """ map is an AddrMap instance, used for scheduling expiries and updating the map. """ self.map = map self.ip = None self.name = None self.expiry = None self.expires = None self.created = None def update(self, *args): """ deals with an update from Tor; see parsing logic in torcontroller """ gmtexpires = None (name, ip, expires) = args[:3] for arg in args: if arg.lower().startswith('expires='): gmtexpires = arg[8:] if gmtexpires is None: if len(args) == 3: gmtexpires = expires else: if args[2] == 'NEVER': gmtexpires = args[2] else: gmtexpires = args[3] self.name = name # "www.example.com" self.ip = maybe_ip_addr(ip) # IPV4Address instance, or string if self.ip == '': self._expire() return fmt = "%Y-%m-%d %H:%M:%S" # if we already have expiry times, etc then we want to # properly delay our timeout oldexpires = self.expires if gmtexpires.upper() == 'NEVER': # FIXME can I just select a date 100 years in the future instead? self.expires = None else: self.expires = datetime.datetime.strptime(gmtexpires, fmt) self.created = datetime.datetime.utcnow() if self.expires is not None: if oldexpires is None: if self.expires <= self.created: diff = datetime.timedelta(seconds=0) else: diff = self.expires - self.created self.expiry = self.map.scheduler.callLater(diff.seconds, self._expire) else: diff = self.expires - oldexpires self.expiry.delay(diff.seconds) def _expire(self): """ callback done via callLater """ del self.map.addr[self.name] self.map.notify("addrmap_expired", *[self.name], **{}) class AddrMap(object): """ A collection of Addr objects mapping domains to addresses, with automatic expiry. FIXME: need listener interface, so far: addrmap_added(Addr) addrmap_expired(name) """ def __init__(self): self.addr = {} self.scheduler = IReactorTime(reactor) self.listeners = [] def update(self, update): """ Deal with an update from Tor; either creates a new Addr object or find existing one and calls update() on it. """ params = shlex.split(update) if params[0] in self.addr: self.addr[params[0]].update(*params) else: a = Addr(self) self.addr[params[0]] = a a.update(*params) self.notify("addrmap_added", *[a], **{}) def find(self, name_or_ip): "FIXME should make this class a dict-like (or subclass?)" return self.addr[name_or_ip] def notify(self, method, *args, **kwargs): for listener in self.listeners: getattr(listener, method)(*args, **kwargs) def add_listener(self, listener): if listener not in self.listeners: self.listeners.append(IAddrListener(listener)) txtorcon-0.14.2/txtorcon/router.py0000644000175000017500000001513612611263616017136 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import unicode_literals from __future__ import with_statement from datetime import datetime from .util import NetLocation from .util import basestring def hexIdFromHash(thehash): """ From the base-64 encoded hashes Tor uses, this produces the longer hex-encoded hashes. """ return "$" + (thehash + "=").decode("base64").encode("hex").upper() def hashFromHexId(hexid): """ From a hex fingerprint id, convert back to base-64 encoded value. """ if hexid[0] == '$': hexid = hexid[1:] return hexid.decode("hex").encode("base64")[:-2] class PortRange(object): """ Represents a range of ports for Router policies. """ def __init__(self, a, b): self.min = a self.max = b def __cmp__(self, b): if b >= self.min and b <= self.max: return 0 return 1 def __str__(self): return "%d-%d" % (self.min, self.max) class Router(object): """ Represents a Tor Router, including location. The controller you pass in is really only used to do get_info calls for ip-to-country/IP in case the :class:`txtorcon.util.NetLocation` stuff fails to find a country. After an .update() call, the id_hex attribute contains a hex-encoded long hash (suitable, for example, to use in a ``GETINFO ns/id/*`` call). After setting the policy property you may call accepts_port() to find out if the router will accept a given port. This works with the reject or accept based policies. """ def __init__(self, controller): self.controller = controller self._flags = [] self.bandwidth = 0 self.name_is_unique = False self.accepted_ports = None self.rejected_ports = None self.id_hex = None self._location = None self.from_consensus = False self.ip = 'unknown' self.ip_v6 = [] # most routers have no IPv6 addresses unique_name = property(lambda x: x.name_is_unique and x.name or x.id_hex) "has the hex id if this router's name is not unique, or its name otherwise" @property def modified(self): if self._modified is None: self._modified = datetime.strptime( self._modified_unparsed, '%Y-%m-%f %H:%M:%S' ) return self._modified def update(self, name, idhash, orhash, modified, ip, orport, dirport): self.name = name self.id_hash = idhash self.or_hash = orhash # modified is lazy-parsed, approximately doubling router-parsing time self._modified_unparsed = modified self._modified = None self.ip = ip self.or_port = orport self.dir_port = dirport self._location = None self.id_hex = hexIdFromHash(self.id_hash) @property def location(self): """ A NetLocation instance with some GeoIP or pygeoip information about location, asn, city (if available). """ if self._location: return self._location if self.ip != 'unknown': self._location = NetLocation(self.ip) else: self._location = NetLocation(None) if not self._location.countrycode and self.ip != 'unknown': # see if Tor is magic and knows more... d = self.controller.get_info_raw('ip-to-country/' + self.ip) d.addCallback(self._set_country) # ignore errors (e.g. "GeoIP Information not loaded") d.addErrback(lambda _: None) return self._location @property def flags(self): """ A list of all the flags for this Router, each one an all-lower-case string. """ return self._flags @flags.setter def flags(self, flags): """ It might be nice to make flags not a list of strings. This is made harder by the control-spec: `...controllers MUST tolerate unrecognized flags and lines...` There is some current work in Twisted for open-ended constants (enums) support however, it seems. """ if isinstance(flags, basestring): flags = flags.split() self._flags = [x.lower() for x in flags] self.name_is_unique = 'named' in self._flags @property def bandwidth(self): """The reported bandwidth of this Router.""" return self._bandwidth @bandwidth.setter def bandwidth(self, bw): self._bandwidth = int(bw) @property def policy(self): """ Port policies for this Router. :return: a string describing the policy """ if self.accepted_ports: return 'accept ' + ','.join(map(str, self.accepted_ports)) elif self.rejected_ports: return 'reject ' + ','.join(map(str, self.rejected_ports)) else: return '' @policy.setter def policy(self, args): """ setter for the policy descriptor """ word = args[0] if word == 'reject': self.accepted_ports = None self.rejected_ports = [] target = self.rejected_ports elif word == 'accept': self.accepted_ports = [] self.rejected_ports = None target = self.accepted_ports else: raise RuntimeError("Don't understand policy word \"%s\"" % word) for port in args[1].split(','): if '-' in port: (a, b) = port.split('-') target.append(PortRange(int(a), int(b))) else: target.append(int(port)) def accepts_port(self, port): """ Query whether this Router will accept the given port. """ if self.rejected_ports is None and self.accepted_ports is None: raise RuntimeError("policy hasn't been set yet") if self.rejected_ports: for x in self.rejected_ports: if port == x: return False return True for x in self.accepted_ports: if port == x: return True return False def _set_country(self, c): """ callback if we used Tor's GETINFO ip-to-country """ self.location.countrycode = c.split()[0].split('=')[1].strip().upper() def __repr__(self): n = self.id_hex if self.name_is_unique: n = self.name return "" % (n, self.location.countrycode, self.policy) txtorcon-0.14.2/txtorcon/torconfig.py0000644000175000017500000013270512627744056017623 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import with_statement import os import sys import functools import tempfile import warnings from io import StringIO import shlex if sys.platform in ('linux2', 'darwin'): import pwd from twisted.python import log from twisted.python.failure import Failure from twisted.internet import defer, error, protocol from twisted.internet.interfaces import IReactorTime from twisted.internet.endpoints import TCP4ClientEndpoint from txtorcon.torcontrolprotocol import parse_keywords, TorProtocolFactory from txtorcon.util import delete_file_or_tree, find_keywords, find_tor_binary from txtorcon.log import txtorlog from txtorcon.interface import ITorControlProtocol class TorNotFound(RuntimeError): """ Raised by launch_tor() in case the tor binary was unspecified and could not be found by consulting the shell. """ class TorProcessProtocol(protocol.ProcessProtocol): def __init__(self, connection_creator, progress_updates=None, config=None, ireactortime=None, timeout=None, kill_on_stderr=True, stdout=None, stderr=None): """ This will read the output from a Tor process and attempt a connection to its control port when it sees any 'Bootstrapped' message on stdout. You probably don't need to use this directly except as the return value from the :func:`txtorcon.launch_tor` method. tor_protocol contains a valid :class:`txtorcon.TorControlProtocol` instance by that point. connection_creator is a callable that should return a Deferred that callbacks with a :class:`txtorcon.TorControlProtocol`; see :func:`txtorcon.launch_tor` for the default one which is a functools.partial that will call ``connect(TorProtocolFactory())`` on an appropriate :api:`twisted.internet.endpoints.TCP4ClientEndpoint` :param connection_creator: A no-parameter callable which returns a Deferred which promises a :api:`twisted.internet.interfaces.IStreamClientEndpoint `. If this is None, we do NOT attempt to connect to the underlying Tor process. :param progress_updates: A callback which received progress updates with three args: percent, tag, summary :param config: a TorConfig object to connect to the TorControlProtocl from the launched tor (should it succeed) :param ireactortime: An object implementing IReactorTime (i.e. a reactor) which needs to be supplied if you pass a timeout. :param timeout: An int representing the timeout in seconds. If we are unable to reach 100% by this time we will consider the setting up of Tor to have failed. Must supply ireactortime if you supply this. :param kill_on_stderr: When True, kill subprocess if we receive anything on stderr :param stdout: Anything subprocess writes to stdout is sent to .write() on this :param stderr: Anything subprocess writes to stderr is sent to .write() on this :ivar tor_protocol: The TorControlProtocol instance connected to the Tor this :api:`twisted.internet.protocol.ProcessProtocol `` is speaking to. Will be valid when the `connected_cb` callback runs. :ivar connected_cb: Triggered when the Tor process we represent is fully bootstrapped """ self.config = config self.tor_protocol = None self.progress_updates = progress_updates if connection_creator: self.connection_creator = connection_creator self.connected_cb = defer.Deferred() else: self.connection_creator = None self.connected_cb = None self.attempted_connect = False self.to_delete = [] self.kill_on_stderr = kill_on_stderr self.stderr = stderr self.stdout = stdout self.collected_stdout = StringIO() self._setup_complete = False self._did_timeout = False self._timeout_delayed_call = None if timeout: if not ireactortime: raise RuntimeError( 'Must supply an IReactorTime object when supplying a ' 'timeout') ireactortime = IReactorTime(ireactortime) self._timeout_delayed_call = ireactortime.callLater( timeout, self.timeout_expired) def outReceived(self, data): """ :api:`twisted.internet.protocol.ProcessProtocol ` API """ if self.stdout: self.stdout.write(data) # minor hack: we can't try this in connectionMade because # that's when the process first starts up so Tor hasn't # opened any ports properly yet. So, we presume that after # its first output we're good-to-go. If this fails, we'll # reset and try again at the next output (see this class' # tor_connection_failed) txtorlog.msg(data) if not self.attempted_connect and self.connection_creator \ and 'Bootstrap' in data: self.attempted_connect = True d = self.connection_creator() d.addCallback(self.tor_connected) d.addErrback(self.tor_connection_failed) def timeout_expired(self): """ A timeout was supplied during setup, and the time has run out. """ try: self.transport.signalProcess('TERM') except error.ProcessExitedAlready: self.transport.loseConnection() self._did_timeout = True def errReceived(self, data): """ :api:`twisted.internet.protocol.ProcessProtocol ` API """ if self.stderr: self.stderr.write(data) if self.kill_on_stderr: self.transport.loseConnection() raise RuntimeError( "Received stderr output from slave Tor process: " + data) def cleanup(self): """ Clean up my temporary files. """ all([delete_file_or_tree(f) for f in self.to_delete]) self.to_delete = [] def processEnded(self, status): """ :api:`twisted.internet.protocol.ProcessProtocol ` API """ self.cleanup() if status.value.exitCode is None: if self._did_timeout: err = RuntimeError("Timeout waiting for Tor launch..") else: err = RuntimeError( "Tor was killed (%s)." % status.value.signal) else: err = RuntimeError( "Tor exited with error-code %d" % status.value.exitCode) log.err(err) if self.connected_cb: self.connected_cb.errback(err) self.connected_cb = None def progress(self, percent, tag, summary): """ Can be overridden or monkey-patched if you want to get progress updates yourself. """ if self.progress_updates: self.progress_updates(percent, tag, summary) # the below are all callbacks def tor_connection_failed(self, failure): # FIXME more robust error-handling please, like a timeout so # we don't just wait forever after 100% bootstrapped (that # is, we're ignoring these errors, but shouldn't do so after # we'll stop trying) self.attempted_connect = False def status_client(self, arg): args = shlex.split(arg) if args[1] != 'BOOTSTRAP': return kw = find_keywords(args) prog = int(kw['PROGRESS']) tag = kw['TAG'] summary = kw['SUMMARY'] self.progress(prog, tag, summary) if prog == 100: if self._timeout_delayed_call: self._timeout_delayed_call.cancel() self._timeout_delayed_call = None if self.connected_cb: self.connected_cb.callback(self) self.connected_cb = None def tor_connected(self, proto): txtorlog.msg("tor_connected %s" % proto) self.tor_protocol = proto if self.config is not None: self.config._update_proto(proto) self.tor_protocol.is_owned = self.transport.pid self.tor_protocol.post_bootstrap.addCallback( self.protocol_bootstrapped).addErrback( self.tor_connection_failed) def protocol_bootstrapped(self, proto): txtorlog.msg("Protocol is bootstrapped") self.tor_protocol.add_event_listener( 'STATUS_CLIENT', self.status_client) # FIXME: should really listen for these to complete as well # as bootstrap etc. For now, we'll be optimistic. self.tor_protocol.queue_command('TAKEOWNERSHIP') self.tor_protocol.queue_command('RESETCONF __OwningControllerProcess') def launch_tor(config, reactor, tor_binary=None, progress_updates=None, connection_creator=None, timeout=None, kill_on_stderr=True, stdout=None, stderr=None): """launches a new Tor process with the given config. There may seem to be a ton of options, but don't panic: this method should be easy to use and most options can be ignored except for advanced use-cases. Calling with a completely empty TorConfig should Just Work:: config = TorConfig() d = launch_tor(config, reactor) d.addCallback(...) Note that the incoming TorConfig instance is examined and several config options are acted upon appropriately: ``DataDirectory``: if supplied, a tempdir is not created, and the one supplied is not deleted. ``ControlPort``: if 0 (zero), a control connection is NOT established (and ``connection_creator`` is ignored). In this case we can't wait for Tor to bootstrap, and **you must kill the tor** yourself. ``User``: if this exists, we attempt to set ownership of the tempdir to this user (but only if our effective UID is 0). This method may set the following options on the supplied TorConfig object: ``DataDirectory, ControlPort, CookieAuthentication, __OwningControllerProcess`` and WILL call :meth:`txtorcon.TorConfig.save` :param config: an instance of :class:`txtorcon.TorConfig` with any configuration values you want. If ``ControlPort`` isn't set, 9052 is used; if ``DataDirectory`` isn't set, tempdir is used to create one (in this case, it will be deleted upon exit). :param reactor: a Twisted IReactorCore implementation (usually twisted.internet.reactor) :param tor_binary: path to the Tor binary to run. Tries to find the tor binary if unset. :param progress_updates: a callback which gets progress updates; gets as args: percent, tag, summary (FIXME make an interface for this). :param kill_on_stderr: When True (the default), if Tor prints anything on stderr we kill off the process, close the TorControlProtocol and raise an exception. :param stdout: a file-like object to which we write anything that Tor prints on stdout (just needs to support write()). :param stderr: a file-like object to which we write anything that Tor prints on stderr (just needs .write()). Note that we kill Tor off by default if anything appears on stderr; pass "no_kill=True" if you don't like the behavior. :param connection_creator: is mostly available to ease testing, so you probably don't want to supply this. If supplied, it is a callable that should return a Deferred that delivers an :api:`twisted.internet.interfaces.IProtocol ` or ConnectError. See :api:`twisted.internet.interfaces.IStreamClientEndpoint`.connect Note that this parameter is ignored if config.ControlPort == 0 :return: a Deferred which callbacks with a TorProcessProtocol connected to the fully-bootstrapped Tor; this has a :class:`txtorcon.TorControlProtocol` instance as `.tor_protocol`. In Tor, ``__OwningControllerProcess`` will be set and TAKEOWNERSHIP will have been called, so if you close the TorControlProtocol the Tor should exit also (see `control-spec `_ 3.23). Note that if ControlPort was 0, we don't connect at all and therefore don't wait for Tor to be bootstrapped. In this case, it's up to you to kill off the Tor you created. HACKS: 1. It's hard to know when Tor has both (completely!) written its authentication cookie file AND is listening on the control port. It seems that waiting for the first 'bootstrap' message on stdout is sufficient. Seems fragile...and doesn't work 100% of the time, so FIXME look at Tor source. """ # We have a slight problem with the approach: we need to pass a # few minimum values to a torrc file so that Tor will start up # enough that we may connect to it. Ideally, we'd be able to # start a Tor up which doesn't really do anything except provide # "AUTHENTICATE" and "GETINFO config/names" so we can do our # config validation. # the other option here is to simply write a torrc version of our # config and get Tor to load that...which might be the best # option anyway. # actually, can't we pass them all as command-line arguments? # could be pushing some limits for giant configs... if tor_binary is None: tor_binary = find_tor_binary() if tor_binary is None: # We fail right here instead of waiting for the reactor to start raise TorNotFound('Tor binary could not be found') # make sure we got things that have write() for stderr, stdout # kwargs for arg in [stderr, stdout]: if arg and not getattr(arg, "write", None): raise RuntimeError( 'File-like object needed for stdout or stderr args.') try: data_directory = config.DataDirectory user_set_data_directory = True except KeyError: user_set_data_directory = False data_directory = tempfile.mkdtemp(prefix='tortmp') config.DataDirectory = data_directory # Set ownership on the temp-dir to the user tor will drop privileges to # when executing as root. try: user = config.User except KeyError: pass else: if sys.platform in ('linux2', 'darwin') and os.geteuid() == 0: os.chown(data_directory, pwd.getpwnam(user).pw_uid, -1) try: control_port = config.ControlPort except KeyError: control_port = 9052 # FIXME choose a random, unoccupied one? config.ControlPort = control_port # so, we support passing in ControlPort=0 -- not really sure if # this is a good idea (since then the caller has to kill the tor # off, etc), but at least one person has requested it :/ if control_port != 0: config.CookieAuthentication = 1 config.__OwningControllerProcess = os.getpid() if connection_creator is None: connection_creator = functools.partial( TCP4ClientEndpoint(reactor, 'localhost', control_port).connect, TorProtocolFactory() ) else: connection_creator = None # NOTE well, that if we don't pass "-f" then Tor will merrily load # it's default torrc, and apply our options over top... :/ config_args = ['-f', '/non-existant', '--ignore-missing-torrc'] # ...now add all our config options on the command-line. This # avoids writing a temporary torrc. for (k, v) in config.config_args(): config_args.append(k) config_args.append(v) # txtorlog.msg('Running with config:\n', ' '.join(config_args)) process_protocol = TorProcessProtocol( connection_creator, progress_updates, config, reactor, timeout, kill_on_stderr, stdout, stderr ) # we set both to_delete and the shutdown events because this # process might be shut down way before the reactor, but if the # reactor bombs out without the subprocess getting closed cleanly, # we'll want the system shutdown events triggered so the temporary # files get cleaned up either way # we don't want to delete the user's directories, just temporary # ones this method created. if not user_set_data_directory: process_protocol.to_delete = [data_directory] reactor.addSystemEventTrigger( 'before', 'shutdown', functools.partial(delete_file_or_tree, data_directory) ) try: log.msg('Spawning tor process with DataDirectory', data_directory) args = [tor_binary] + config_args transport = reactor.spawnProcess( process_protocol, tor_binary, args=args, env={'HOME': data_directory}, path=data_directory ) # FIXME? don't need rest of the args: uid, gid, usePTY, childFDs) transport.closeStdin() except RuntimeError as e: return defer.fail(e) if process_protocol.connected_cb: return process_protocol.connected_cb return defer.succeed(process_protocol) class TorConfigType(object): """ Base class for all configuration types, which function as parsers and un-parsers. """ def parse(self, s): """ Given the string s, this should return a parsed representation of it. """ return s def validate(self, s, instance, name): """ If s is not a valid type for this object, an exception should be thrown. The validated object should be returned. """ return s class Boolean(TorConfigType): def parse(self, s): if int(s): return True return False class Boolean_Auto(TorConfigType): """ weird class-name, but see the parser for these which is *mostly* just the classname <==> string from Tor, except for something called Boolean+Auto which is replace()d to be Boolean_Auto """ def parse(self, s): if s == 'auto' or int(s) < 0: return -1 if int(s): return 1 return 0 class Integer(TorConfigType): def parse(self, s): return int(s) class SignedInteger(Integer): pass class Port(Integer): pass class TimeInterval(Integer): pass # not actually used? class TimeMsecInterval(TorConfigType): pass class DataSize(Integer): pass class Float(TorConfigType): def parse(self, s): return float(s) # unused also? class Time(TorConfigType): pass class CommaList(TorConfigType): def parse(self, s): return [x.strip() for x in s.split(',')] # FIXME: in latest master; what is it? # Tor source says "A list of strings, separated by commas and optional # whitespace, representing intervals in seconds, with optional units" class TimeIntervalCommaList(CommaList): pass # FIXME: is this really a comma-list? class RouterList(CommaList): pass class String(TorConfigType): pass class Filename(String): pass class LineList(TorConfigType): def parse(self, s): if isinstance(s, list): return [str(x).strip() for x in s] return [x.strip() for x in s.split('\n')] def validate(self, obj, instance, name): if not isinstance(obj, list): raise ValueError("Not valid for %s: %s" % (self.__class__, obj)) return _ListWrapper( obj, functools.partial(instance.mark_unsaved, name)) config_types = [Boolean, Boolean_Auto, LineList, Integer, SignedInteger, Port, TimeInterval, TimeMsecInterval, DataSize, Float, Time, CommaList, String, LineList, Filename, RouterList, TimeIntervalCommaList] def is_list_config_type(klass): return 'List' in klass.__name__ or klass.__name__ in ['HiddenServices'] def _wrapture(orig): """ Returns a new method that wraps orig (the original method) with something that first calls on_modify from the instance. _ListWrapper uses this to wrap all methods that modify the list. """ # @functools.wraps(orig) def foo(*args): obj = args[0] obj.on_modify() return orig(*args) return foo class _ListWrapper(list): """ Do some voodoo to wrap lists so that if you do anything to modify it, we mark the config as needing saving. FIXME: really worth it to preserve attribute-style access? seems to be okay from an exterior API perspective.... """ def __init__(self, thelist, on_modify_cb): list.__init__(self, thelist) self.on_modify = on_modify_cb __setitem__ = _wrapture(list.__setitem__) __setslice__ = _wrapture(list.__setslice__) append = _wrapture(list.append) extend = _wrapture(list.extend) insert = _wrapture(list.insert) remove = _wrapture(list.remove) pop = _wrapture(list.pop) def __repr__(self): return '_ListWrapper' + super(_ListWrapper, self).__repr__() class HiddenServiceClientAuth(object): """ Encapsulates a single client-authorization, as parsed from a HiddenServiceDir's "client_keys" file if you have stealth or basic authentication turned on. :param name: the name you gave it in the HiddenServiceAuthorizeClient line :param cookie: random password :param key: RSA private key, or None if this was basic auth """ def __init__(self, name, cookie, key=None): self.name = name self.cookie = cookie self.key = parse_rsa_blob(key) if key else None class HiddenService(object): """ Because hidden service configuration is handled specially by Tor, we wrap the config in this class. This corresponds to the HiddenServiceDir, HiddenServicePort, HiddenServiceVersion and HiddenServiceAuthorizeClient lines from the config. If you want multiple HiddenServicePort lines, simply append more strings to the ports member. To create an additional hidden service, append a new instance of this class to the config (ignore the conf argument):: state.hiddenservices.append(HiddenService('/path/to/dir', ['80 127.0.0.1:1234'])) """ def __init__(self, config, thedir, ports, auth=[], ver=2, group_readable=0): """ config is the TorConfig to which this will belong, thedir corresponds to 'HiddenServiceDir' and will ultimately contain a 'hostname' and 'private_key' file, ports is a list of lines corresponding to HiddenServicePort (like '80 127.0.0.1:1234' to advertise a hidden service at port 80 and redirect it internally on 127.0.0.1:1234). auth corresponds to the HiddenServiceAuthenticateClient lines and can be either a string or a list of strings (like 'basic client0,client1' or 'stealth client5,client6') and ver corresponds to HiddenServiceVersion and is always 2 right now. XXX FIXME can we avoid having to pass the config object somehow? Like provide a factory-function on TorConfig for users instead? """ self.conf = config self.dir = thedir self.version = ver self.group_readable = group_readable # HiddenServiceAuthorizeClient is a list # in case people are passing '' for the auth if not auth: auth = [] elif not isinstance(auth, list): auth = [auth] self.authorize_client = _ListWrapper( auth, functools.partial( self.conf.mark_unsaved, 'HiddenServices' ) ) # there are three magic attributes, "hostname" and # "private_key" are gotten from the dir if they're still None # when accessed. "client_keys" parses out any client # authorizations. Note that after a SETCONF has returned '250 # OK' it seems from tor code that the keys will always have # been created on disk by that point if not isinstance(ports, list): ports = [ports] self.ports = _ListWrapper(ports, functools.partial( self.conf.mark_unsaved, 'HiddenServices')) def __setattr__(self, name, value): """ We override the default behavior so that we can mark HiddenServices as unsaved in our TorConfig object if anything is changed. """ watched_params = ['dir', 'version', 'authorize_client', 'ports'] if name in watched_params and self.conf: self.conf.mark_unsaved('HiddenServices') if isinstance(value, list): value = _ListWrapper(value, functools.partial( self.conf.mark_unsaved, 'HiddenServices')) self.__dict__[name] = value def __getattr__(self, name): if name in ('hostname', 'private_key'): with open(os.path.join(self.dir, name)) as f: self.__dict__[name] = f.read().strip() elif name == 'client_keys': fname = os.path.join(self.dir, name) keys = [] if os.path.exists(fname): with open(fname) as f: keys = parse_client_keys(f) self.__dict__[name] = keys return self.__dict__[name] def config_attributes(self): """ Helper method used by TorConfig when generating a torrc file. """ rtn = [('HiddenServiceDir', str(self.dir))] if self.conf._supports['HiddenServiceDirGroupReadable'] \ and self.group_readable: rtn.append(('HiddenServiceDirGroupReadable', str(1))) for x in self.ports: rtn.append(('HiddenServicePort', str(x))) if self.version: rtn.append(('HiddenServiceVersion', str(self.version))) for authline in self.authorize_client: rtn.append(('HiddenServiceAuthorizeClient', str(authline))) return rtn def parse_rsa_blob(lines): return ''.join(lines[1:-1]) def parse_client_keys(stream): ''' This parses a hidden-service "client_keys" file, either stealth or basic (they're the same, except "stealth" includes a "client-key"). Returns a list of HiddenServiceClientAuth() instances. Note that the key does NOT include the "----BEGIN ---" markers, nor *any* embedded whitespace. It is *just* the key blob. ''' def parse_error(data): raise RuntimeError("Parse error at: " + data) class ParserState(object): def __init__(self): self.keys = [] self.reset() def reset(self): self.name = None self.cookie = None self.key = [] def create_key(self): if self.name is not None: self.keys.append(HiddenServiceClientAuth(self.name, self.cookie, self.key)) self.reset() def set_name(self, name): self.create_key() self.name = name.split()[1] def set_cookie(self, cookie): self.cookie = cookie.split()[1] if self.cookie.endswith('=='): self.cookie = self.cookie[:-2] def add_key_line(self, line): self.key.append(line) from txtorcon.spaghetti import FSM, State, Transition init = State('init') got_name = State('got_name') got_cookie = State('got_cookie') reading_key = State('got_key') parser_state = ParserState() # initial state; we want "client-name" or it's an error init.add_transitions([ Transition(got_name, lambda line: line.startswith('client-name '), parser_state.set_name), Transition(init, lambda line: not line.startswith('client-name '), parse_error), ]) # next up is "descriptor-cookie" or it's an error got_name.add_transitions([ Transition(got_cookie, lambda line: line.startswith('descriptor-cookie '), parser_state.set_cookie), Transition(init, lambda line: not line.startswith('descriptor-cookie '), parse_error), ]) # the "interesting bit": there's either a client-name if we're a # "basic" file, or an RSA key (with "client-key" before it) got_cookie.add_transitions([ Transition(reading_key, lambda line: line.startswith('client-key'), None), Transition(got_name, lambda line: line.startswith('client-name '), parser_state.set_name), ]) # if we're reading an RSA key, we accumulate it in current_key.key # until we hit a line starting with "client-name" reading_key.add_transitions([ Transition(reading_key, lambda line: not line.startswith('client-name'), parser_state.add_key_line), Transition(got_name, lambda line: line.startswith('client-name '), parser_state.set_name), ]) # create our FSM and parse the data fsm = FSM([init, got_name, got_cookie, reading_key]) for line in stream.readlines(): fsm.process(line.strip()) parser_state.create_key() # make sure we get the "last" one return parser_state.keys class TorConfig(object): """This class abstracts out Tor's config, and can be used both to create torrc files from nothing and track live configuration of a Tor instance. Also, it gives easy access to all the configuration options present. This is initialized at "bootstrap" time, providing attribute-based access thereafter. Note that after you set some number of items, you need to do a save() before these are sent to Tor (and then they will be done as one SETCONF). You may also use this class to construct a configuration from scratch (e.g. to give to :func:`txtorcon.launch_tor`). In this case, values are reflected right away. (If we're not bootstrapped to a Tor, this is the mode). Note that you do not need to call save() if you're just using TorConfig to create a .torrc file or for input to launch_tor(). This class also listens for CONF_CHANGED events to update the cached data in the event other controllers (etc) changed it. There is a lot of magic attribute stuff going on in here (which might be a bad idea, overall) but the *intent* is that you can just set Tor options and it will all Just Work. For config items that take multiple values, set that to a list. For example:: conf = TorConfig(...) conf.SOCKSPort = [9050, 1337] conf.HiddenServices.append(HiddenService(...)) (Incoming objects, like lists, are intercepted and wrapped). FIXME: when is CONF_CHANGED introduced in Tor? Can we do anything like it for prior versions? FIXME: - HiddenServiceOptions is special: GETCONF on it returns several (well, two) values. Besides adding the two keys 'by hand' do we need to do anything special? Can't we just depend on users doing 'conf.hiddenservicedir = foo' AND 'conf.hiddenserviceport = bar' before a save() ? - once I determine a value is default, is there any way to actually get what this value is? """ def __init__(self, control=None): self.config = {} '''Current configuration, by keys.''' if control is None: self._protocol = None self.__dict__['_slutty_'] = None else: self._protocol = ITorControlProtocol(control) self.unsaved = {} '''Configuration that has been changed since last save().''' self.parsers = {} '''Instances of the parser classes, subclasses of TorConfigType''' self.list_parsers = set(['hiddenservices']) '''All the names (keys from .parsers) that are a List of something.''' # during bootstrapping we decide whether we support the # following features. A thing goes in here if TorConfig # behaves differently depending upon whether it shows up in # "GETINFO config/names" self._supports = dict( HiddenServiceDirGroupReadable=False ) self.post_bootstrap = defer.Deferred() if self.protocol: if self.protocol.post_bootstrap: self.protocol.post_bootstrap.addCallback( self.bootstrap).addErrback(log.err) else: self.bootstrap() else: self.do_post_bootstrap(self) self.__dict__['_setup_'] = None # FIXME should re-name this to "tor_protocol" to be consistent # with other things? Or rename the other things? """ read-only access to TorControlProtocol. Call attach_protocol() to set it, which can only be done if we don't already have a protocol. """ def _get_protocol(self): return self.__dict__['_protocol'] protocol = property(_get_protocol) def attach_protocol(self, proto): """ returns a Deferred that fires once we've set this object up to track the protocol. Fails if we already have a protocol. """ if self._protocol is not None: raise RuntimeError("Already have a protocol.") # make sure we have nothing in self.unsaved self.save() self.__dict__['_protocol'] = proto # FIXME some of this is duplicated from ctor del self.__dict__['_slutty_'] self.__dict__['post_bootstrap'] = defer.Deferred() if proto.post_bootstrap: proto.post_bootstrap.addCallback(self.bootstrap) return self.__dict__['post_bootstrap'] def _update_proto(self, proto): """ internal method, used by launch_tor to update the protocol after we're set up. """ self.__dict__['_protocol'] = proto def __setattr__(self, name, value): """ we override this so that we can provide direct attribute access to our config items, and move them into self.unsaved when they've been changed. hiddenservices have to be special unfortunately. the _setup_ thing is so that we can set up the attributes we need in the constructor without uusing __dict__ all over the place. """ has_setup_attr = lambda o: '_setup_' in o.__dict__ has_slutty_attr = lambda o: '_slutty_' in o.__dict__ is_hidden_services = lambda s: s.lower() == "hiddenservices" if has_setup_attr(self): name = self._find_real_name(name) if not has_slutty_attr(self) and not is_hidden_services(name): value = self.parsers[name].validate(value, self, name) if isinstance(value, list): value = _ListWrapper( value, functools.partial(self.mark_unsaved, name)) name = self._find_real_name(name) self.unsaved[name] = value else: super(TorConfig, self).__setattr__(name, value) def _maybe_create_listwrapper(self, rn): if rn.lower() in self.list_parsers and rn not in self.config: self.config[rn] = _ListWrapper([], functools.partial( self.mark_unsaved, rn)) def __getattr__(self, name): """ on purpose, we don't return self.unsaved if the key is in there because I want the config to represent the running Tor not ``things which might get into the running Tor if save() were to be called'' """ rn = self._find_real_name(name) if '_slutty_' in self.__dict__ and rn in self.unsaved: return self.unsaved[rn] self._maybe_create_listwrapper(rn) return self.config[rn] def __contains__(self, item): if item in self.unsaved and '_slutty_' in self.__dict__: return True return item in self.config def __iter__(self): ''' FIXME needs proper iterator tests in test_torconfig too ''' for x in self.config.__iter__(): yield x for x in self.__dict__['unsaved'].__iter__(): yield x def get_type(self, name): """ return the type of a config key. :param: name the key FIXME can we do something more-clever than this for client code to determine what sort of thing a key is? """ if name.lower() == 'hiddenservices': return HiddenService return type(self.parsers[name]) def _conf_changed(self, arg): """ internal callback. from control-spec: 4.1.18. Configuration changed The syntax is: StartReplyLine *(MidReplyLine) EndReplyLine StartReplyLine = "650-CONF_CHANGED" CRLF MidReplyLine = "650-" KEYWORD ["=" VALUE] CRLF EndReplyLine = "650 OK" Tor configuration options have changed (such as via a SETCONF or RELOAD signal). KEYWORD and VALUE specify the configuration option that was changed. Undefined configuration options contain only the KEYWORD. """ conf = parse_keywords(arg, multiline_values=False) for (k, v) in conf.items(): # v will be txtorcon.DEFAULT_VALUE already from # parse_keywords if it was unspecified self.config[self._find_real_name(k)] = v def bootstrap(self, arg=None): ''' This only takes args so it can be used as a callback. Don't pass an arg, it is ignored. ''' try: self.protocol.add_event_listener( 'CONF_CHANGED', self._conf_changed) except RuntimeError: # for Tor versions which don't understand CONF_CHANGED # there's nothing we can really do. log.msg( "Can't listen for CONF_CHANGED event; won't stay up-to-date " "with other clients.") d = self.protocol.get_info_raw("config/names") d.addCallback(self._do_setup) d.addCallback(self.do_post_bootstrap) d.addErrback(self.do_post_errback) def do_post_errback(self, f): self.post_bootstrap.errback(f) return None def do_post_bootstrap(self, arg): if not self.post_bootstrap.called: self.post_bootstrap.callback(self) return self def needs_save(self): return len(self.unsaved) > 0 def mark_unsaved(self, name): name = self._find_real_name(name) if name in self.config and name not in self.unsaved: self.unsaved[name] = self.config[self._find_real_name(name)] def save(self): """ Save any outstanding items. This returns a Deferred which will errback if Tor was unhappy with anything, or callback with this TorConfig object on success. """ if not self.needs_save(): return defer.succeed(self) args = [] directories = [] for (key, value) in self.unsaved.items(): if key == 'HiddenServices': self.config['HiddenServices'] = value for hs in value: for (k, v) in hs.config_attributes(): if k == 'HiddenServiceDir': if v not in directories: directories.append(v) args.append(k) args.append(v) else: raise RuntimeError("Trying to add hidden service with same HiddenServiceDir: %s" % v) else: args.append(k) args.append(v) continue if isinstance(value, list): for x in value: args.append(key) args.append(str(x)) else: args.append(key) args.append(value) # FIXME in future we should wait for CONF_CHANGED and # update then, right? self.config[self._find_real_name(key)] = value # FIXME might want to re-think this, but currently there's no # way to put things into a config and get them out again # nicely...unless you just don't assign a protocol if self.protocol: d = self.protocol.set_conf(*args) d.addCallback(self._save_completed) return d else: self._save_completed() return defer.succeed(self) def _save_completed(self, *args): '''internal callback''' self.__dict__['unsaved'] = {} return self def _find_real_name(self, name): keys = list(self.__dict__['parsers'].keys()) + list(self.__dict__['config'].keys()) for x in keys: if x.lower() == name.lower(): return x return name @defer.inlineCallbacks def _do_setup(self, data): for line in data.split('\n'): if line == "config/names=": continue (name, value) = line.split() if name in self._supports: self._supports[name] = True if name == 'HiddenServiceOptions': # set up the "special-case" hidden service stuff servicelines = yield self.protocol.get_conf_raw( 'HiddenServiceOptions') self._setup_hidden_services(servicelines) continue if value == 'Dependant': continue # there's a thing called "Boolean+Auto" which is -1 for # auto, 0 for false and 1 for true. could be nicer if it # was called AutoBoolean or something, but... value = value.replace('+', '_') inst = None # FIXME: put parser classes in dict instead? for cls in config_types: if cls.__name__ == value: inst = cls() if not inst: raise RuntimeError("Don't have a parser for: " + value) v = yield self.protocol.get_conf(name) v = v[name] rn = self._find_real_name(name) self.parsers[rn] = inst if is_list_config_type(inst.__class__): self.list_parsers.add(rn) parsed = self.parsers[rn].parse(v) self.config[rn] = _ListWrapper( parsed, functools.partial(self.mark_unsaved, rn)) else: self.config[rn] = self.parsers[rn].parse(v) # can't just return in @inlineCallbacks-decorated methods defer.returnValue(self) def _setup_hidden_services(self, servicelines): def maybe_add_hidden_service(): if directory is not None: if directory not in directories: directories.append(directory) hs.append( HiddenService( self, directory, ports, auth, ver, group_read ) ) else: raise RuntimeError("Trying to add hidden service with same HiddenServiceDir: %s" % directory) hs = [] directory = None directories = [] ports = [] ver = None group_read = None auth = None for line in servicelines.split('\n'): if not len(line.strip()): continue if line == 'HiddenServiceOptions': continue k, v = line.split('=') if k == 'HiddenServiceDir': maybe_add_hidden_service() directory = v _directory = directory directory = os.path.abspath(directory) if directory != _directory: warnings.warn( "Directory path: %s changed to absolute path: %s" % (_directory, directory), RuntimeWarning ) ports = [] ver = None auth = [] group_read = 0 elif k == 'HiddenServicePort': ports.append(v) elif k == 'HiddenServiceVersion': ver = int(v) elif k == 'HiddenServiceAuthorizeClient': auth.append(v) elif k == 'HiddenServiceDirGroupReadable': group_read = int(v) else: raise RuntimeError("Can't parse HiddenServiceOptions: " + k) maybe_add_hidden_service() name = 'HiddenServices' self.config[name] = _ListWrapper( hs, functools.partial(self.mark_unsaved, name)) def config_args(self): ''' Returns an iterator of 2-tuples (config_name, value), one for each configuration option in this config. This is more-or-less and internal method, but see, e.g., launch_tor()'s implementation if you thing you need to use this for something. See :meth:`txtorcon.TorConfig.create_torrc` which returns a string which is also a valid ``torrc`` file ''' for (k, v) in list(self.config.items()) + list(self.unsaved.items()): if type(v) is _ListWrapper: if k.lower() == 'hiddenservices': for x in v: for (kk, vv) in x.config_attributes(): yield (str(kk), str(vv)) else: # FIXME actually, is this right? don't we want ALL # the values in one string?! for x in v: yield (str(k), str(x)) else: yield (str(k), str(v)) def create_torrc(self): rtn = StringIO() for (k, v) in self.config_args(): rtn.write(u'%s %s\n' % (k, v)) return rtn.getvalue() txtorcon-0.14.2/examples/0000755000175000017500000000000012627745451015205 5ustar mikemike00000000000000txtorcon-0.14.2/examples/list_circuits.py0000644000175000017500000000145312536657072020442 0ustar mikemike00000000000000#!/usr/bin/env python from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks import txtorcon @inlineCallbacks def main(reactor): state = yield txtorcon.build_local_tor_connection(reactor) for circuit in state.circuits.values(): first_relay = circuit.path[0] print "Circuit {} first hop: {}".format(circuit.id, first_relay.ip) # x = yield state.protocol.get_info('desc/id/B69D45E2AC49A81E014425FF6E07C7435C9F89B0') # x = yield state.protocol.get_info('desc/id/EAA0E6A2CD95F5AFF46CDF042623DE79C19E028A') x = yield state.protocol.get_info('desc/id/EAA0E6A2CD95F5AFF46CDF042623DE79C19E028A') print x if False: from guppy import hpy print "heap:" print hpy().heap() if __name__ == '__main__': react(main) txtorcon-0.14.2/examples/launch_tor_with_simplehttpd.py0000755000175000017500000001105412515516430023356 0ustar mikemike00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- '''Create a new tor node and add a simple http server to it, serving a given directory over http. The server is single-threaded and very limited. There are two arguments that can be passed via the commandline: -p\tThe internet-facing port the hidden service should listen on -d\tThe directory to serve via http Example: ./launch_tor_with_simplehttpd.py -p 8080 -d /opt/files/ ''' import SimpleHTTPServer import SocketServer import functools import getopt import os import sys import tempfile import thread from twisted.internet import reactor import txtorcon def print_help(): print __doc__ def print_tor_updates(prog, tag, summary): # Prints some status messages while booting tor print 'Tor booting [%d%%]: %s' % (prog, summary) def start_httpd(httpd): # Create a new thread to serve requests print 'Starting httpd...' return thread.start_new_thread(httpd.serve_forever, ()) def stop_httpd(httpd): # Kill the httpd print 'Stopping httpd...' httpd.shutdown() def setup_complete(config, port, proto): # Callback from twisted when tor has booted. # We create a reference to this function via functools.partial that # provides us with a reference to 'config' and 'port', twisted then adds # the 'proto' argument print '\nTor is now running. The hidden service is available at' print '\n\thttp://%s:%i\n' % (config.HiddenServices[0].hostname, port) # This is probably more secure than any other httpd... print '### DO NOT RELY ON THIS SERVER TO TRANSFER FILES IN A SECURE WAY ###' def setup_failed(arg): # Callback from twisted if tor could not boot. Nothing to see here, move # along. print 'Failed to launch tor', arg reactor.stop() def main(): # Parse the commandline-options try: opts, args = getopt.getopt(sys.argv[1:], 'hd:p:') except getopt.GetoptError as excp: print str(excp) print_help() return 1 serve_directory = '.' # The default directory to serve files from hs_public_port = 8011 # The default port the hidden service is available on web_port = 4711 # The real server's local port web_host = '127.0.0.1' # The real server is bound to localhost for o, a in opts: if o == '-d': serve_directory = a elif o == '-p': hs_public_port = int(a) elif o == '-h': print_help() return else: print 'Unknown option "%s"' % (o, ) return 1 # Sanitize path and set working directory there (for SimpleHTTPServer) serve_directory = os.path.abspath(serve_directory) if not os.path.exists(serve_directory): print 'Path "%s" does not exists, can\'t serve from there...' % \ (serve_directory, ) return 1 os.chdir(serve_directory) # Create a new SimpleHTTPServer and serve it from another thread. # We create a callback to Twisted to shut it down when we exit. print 'Serving "%s" on %s:%i' % (serve_directory, web_host, web_port) httpd = SocketServer.TCPServer((web_host, web_port), SimpleHTTPServer.SimpleHTTPRequestHandler) start_httpd(httpd) reactor.addSystemEventTrigger('before', 'shutdown', stop_httpd, httpd=httpd) # Create a directory to hold our hidden service. Twisted will unlink it # when we exit. hs_temp = tempfile.mkdtemp(prefix='torhiddenservice') reactor.addSystemEventTrigger('before', 'shutdown', functools.partial(txtorcon.util.delete_file_or_tree, hs_temp)) # Add the hidden service to a blank configuration config = txtorcon.TorConfig() config.SOCKSPort = 0 config.ORPort = 9089 config.HiddenServices = [txtorcon.HiddenService(config, hs_temp, ['%i %s:%i' % (hs_public_port, web_host, web_port)])] config.save() # Now launch tor # Notice that we use a partial function as a callback so we have a # reference to the config object when tor is fully running. tordeferred = txtorcon.launch_tor(config, reactor, progress_updates=print_tor_updates) tordeferred.addCallback(functools.partial(setup_complete, config, hs_public_port)) tordeferred.addErrback(setup_failed) reactor.run() if __name__ == '__main__': sys.exit(main()) txtorcon-0.14.2/examples/attach_streams_by_country.py0000755000175000017500000002013512515516430023027 0ustar mikemike00000000000000#!/usr/bin/env python # # This uses a custom txtorcon.IStreamAttacher to force streams to use # circuits that exit in the same country (as supplied by GeoIP) and # builds such a circuit if one isn't available yet. # # Note that you can do something very similar to this with Tor's # config file as well by setting something like: # # ExitNodes {us},{ca} # # ...in your torrc. The above just exits from those countries, not # the one in which the Web server is located, however. So, this is a # little redundant, but gives you the idea of how to do these sorts # of things. # # Another thing to note is that the DNS lookup is a stream before the # name is looked up, so the DNS lookup may occur from whatever stream # Tor chose for that (we return None, which causes the attacher to # tell Tor to attach that stream itself). This presents a problem for # sites which optimize the server they deliver based on DNS -- if you # lookup from X you'll get a server near/in X, which for our next # step will make "the site" appear to be there. # # The only "solution" for this would be to do the lookup locally, but # that defeats the purpose of Tor. # import random from twisted.python import log from twisted.internet import reactor, defer from zope.interface import implements import txtorcon class MyStreamListener(txtorcon.StreamListenerMixin): def stream_new(self, stream): print "new stream:", stream.id, stream.target_host def stream_succeeded(self, stream): print "successful stream:", stream.id, stream.target_host def stream_attach(self, stream, circuit): print "stream", stream.id, " attached to circuit", circuit.id, print "with path:", '->'.join(map(lambda x: x.location.countrycode, circuit.path)) class MyAttacher(txtorcon.CircuitListenerMixin): implements(txtorcon.IStreamAttacher) def __init__(self, state): # pointer to our TorState object self.state = state # circuits for which we are awaiting completion so we can # finish our attachment to them. self.waiting_circuits = [] def waiting_on(self, circuit): for (circid, d, stream_cc) in self.waiting_circuits: if circuit.id == circid: return True return False def circuit_extend(self, circuit, router): "ICircuitListener" if circuit.purpose != 'GENERAL': return # only output for circuits we're waiting on if self.waiting_on(circuit): path = '->'.join(map(lambda x: x.location.countrycode, circuit.path)) print " circuit %d (%s). Path now %s" % (circuit.id, router.id_hex, path) def circuit_built(self, circuit): "ICircuitListener" if circuit.purpose != 'GENERAL': return path = '->'.join(map(lambda r: r.location.countrycode, circuit.path)) print "circuit built", circuit.id, path for (circid, d, stream_cc) in self.waiting_circuits: if circid == circuit.id: self.waiting_circuits.remove((circid, d, stream_cc)) d.callback(circuit) def circuit_failed(self, circuit, kw): if self.waiting_on(circuit): print "A circuit we requested", circuit.id, print "has failed. Reason:", kw['REASON'] circid, d, stream_cc = None, None, None for x in self.waiting_circuits: if x[0] == circuit.id: circid, d, stream_cc = x if d is None: raise Exception("Expected to find circuit.") self.waiting_circuits.remove((circid, d, stream_cc)) print "Trying a new circuit build for", circid self.request_circuit_build(stream_cc, d) def attach_stream(self, stream, circuits): """ IStreamAttacher API """ if stream.target_host not in self.state.addrmap.addr: print "No AddrMap entry for", stream.target_host, print "so I don't know where it exits; get Tor to attach stream." return None ip = str(self.state.addrmap.addr[stream.target_host].ip) stream_cc = txtorcon.util.NetLocation(ip).countrycode print "Stream to", ip, "exiting in", stream_cc if stream_cc is None: # returning None tells TorState to ask Tor to select a # circuit instead print " unknown country, Tor will assign stream" return None for circ in circuits.values(): if circ.state != 'BUILT' or circ.purpose != 'GENERAL': continue circuit_cc = circ.path[-1].location.countrycode if circuit_cc is None: print "warning: don't know where circuit", circ.id, "exits" if circuit_cc == stream_cc: print " found suitable circuit:", circ return circ # if we get here, we haven't found a circuit that exits in # the country GeoIP claims our target server is in, so we # need to build one. print "Didn't find a circuit, building one" # we need to return a Deferred which will callback with our # circuit, however built_circuit only callbacks with the # message from Tor saying it heard about our request. So when # that happens, we push our real Deferred into the # waiting_circuits list which will get pop'd at some point # when the circuit_built() listener callback happens. d = defer.Deferred() self.request_circuit_build(stream_cc, d) return d def request_circuit_build(self, stream_cc, deferred_to_callback): # for exits, we can select from any router that's in the # correct country. last = filter(lambda x: x.location.countrycode == stream_cc, self.state.routers.values()) # start with an entry guard, put anything in the middle and # put one of our exits at the end. path = [random.choice(self.state.entry_guards.values()), random.choice(self.state.routers.values()), random.choice(last)] print " requesting a circuit:", '->'.join(map(lambda r: r.location.countrycode, path)) class AppendWaiting: def __init__(self, attacher, d, stream_cc): self.attacher = attacher self.d = d self.stream_cc = stream_cc 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. """ print " my circuit is in progress", circ.id self.attacher.waiting_circuits.append((circ.id, self.d, self.stream_cc)) d = self.state.build_circuit(path) d.addCallback(AppendWaiting(self, deferred_to_callback, stream_cc)) d.addErrback(log.err) return d def do_setup(state): print "Connected to a Tor version", state.protocol.version attacher = MyAttacher(state) state.set_attacher(attacher, reactor) state.add_circuit_listener(attacher) state.add_stream_listener(MyStreamListener()) print "Existing state when we connected:" print "Streams:" for s in state.streams.values(): print ' ', s print print "General-purpose circuits:" for c in filter(lambda x: x.purpose == 'GENERAL', state.circuits.values()): print ' ', c.id, '->'.join(map(lambda x: x.location.countrycode, c.path)) def setup_failed(arg): print "SETUP FAILED", arg reactor.stop() d = txtorcon.build_local_tor_connection(reactor) d.addCallback(do_setup).addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/launch_tor_with_hiddenservice.py0000755000175000017500000000550412572755166023656 0ustar mikemike00000000000000#!/usr/bin/env python # Here we set up a Twisted Web server and then launch a slave tor # with a configured hidden service directed at the Web server we set # up. import tempfile import functools from twisted.internet import reactor from twisted.internet.endpoints import TCP4ServerEndpoint from twisted.web import server, resource import txtorcon class Simple(resource.Resource): isLeaf = True def render_GET(self, request): return "Hello, world! I'm a hidden service!" def updates(prog, tag, summary): print "%d%%: %s" % (prog, summary) def setup_complete(config, proto): print "Protocol completed" onion_address = config.HiddenServices[0].hostname print "I have a hidden (web) service running at:" print "http://%s (port %d)" % (onion_address, hs_public_port) print "The temporary directory for it is at:", config.HiddenServices[0].dir print print "For example, you should be able to visit it via:" print " torsocks lynx http://%s" % onion_address def setup_failed(arg): print "SETUP FAILED", arg reactor.stop() hs_port = 9876 hs_public_port = 80 hs_temp = tempfile.mkdtemp(prefix='torhiddenservice') # register something to clean up our tempdir reactor.addSystemEventTrigger( 'before', 'shutdown', functools.partial( txtorcon.util.delete_file_or_tree, hs_temp ) ) # configure the hidden service we want. # obviously, we'd want a more-persistent place to keep the hidden # service directory for a "real" setup. If the directory is empty at # startup as here, Tor creates new keys etcetera (which IS the .onion # address). That is, every time you run this script you get a new # hidden service URI, which is probably not what you want. # The launch_tor method adds other needed config directives to give # us a minimal config. config = txtorcon.TorConfig() config.SOCKSPort = 0 config.ORPort = 9089 config.HiddenServices = [ txtorcon.HiddenService( config, hs_temp, ["%d 127.0.0.1:%d" % (hs_public_port, hs_port)] ) ] config.save() # next we set up our service to listen on hs_port which is forwarded # (via the HiddenService options) from the hidden service address on # port hs_public_port site = server.Site(Simple()) hs_endpoint = TCP4ServerEndpoint(reactor, hs_port, interface='127.0.0.1') hs_endpoint.listen(site) # we've got our Twisted service listening locally and our options # ready to go, so we now launch Tor. Once it's done (see above # callbacks) we print out the .onion URI and then do "nothing" # (i.e. let the Web server do its thing). Note that the way we've set # up the slave Tor process, when we close the connection to it tor # will exit. d = txtorcon.launch_tor(config, reactor, progress_updates=updates) d.addCallback(functools.partial(setup_complete, config)) d.addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/dump_config.py0000755000175000017500000000322312572755166020057 0ustar mikemike00000000000000#!/usr/bin/env python # Simple usage example of TorConfig import sys import types from twisted.internet import reactor from txtorcon import build_local_tor_connection, TorConfig, DEFAULT_VALUE def setup_complete(config): print "Got config" keys = config.config.keys() keys.sort() defaults = [] for k in keys: if k == 'HiddenServices': for hs in config.config[k]: for xx in ['dir', 'version', 'authorize_client']: if getattr(hs, xx): print 'HiddenService%s %s' % (xx.capitalize(), getattr(hs, xx)) for port in hs.ports: print 'HiddenServicePort', port continue v = getattr(config, k) if isinstance(v, types.ListType): for val in v: if val != DEFAULT_VALUE: print k, val elif v == DEFAULT_VALUE: defaults.append(k) else: print k, v if 'defaults' in sys.argv: print "Set to default value:" for k in defaults: print "# %s" % k reactor.stop() def setup_failed(arg): print "SETUP FAILED", arg reactor.stop() def bootstrap(c): conf = TorConfig(c) conf.post_bootstrap.addCallback(setup_complete).addErrback(setup_failed) print "Connection is live, bootstrapping state..." d = build_local_tor_connection(reactor, build_state=False, wait_for_proto=False) # do not use addCallbacks() here, in case bootstrap has an error d.addCallback(bootstrap).addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/systemd.service0000644000175000017500000000223012347476251020252 0ustar mikemike00000000000000# see http://twistedmatrix.com/documents/current/core/howto/systemd.html # started from that, and changed a few options [Unit] Description=Hidden-Service Web Server [Service] ExecStart=/srv/hiddenservice/venv/bin/twistd \ --nodaemon \ # recommended by Twisted --pidfile= \ # systemd doesn't need a PID-file web --port onion:80:hiddenServiceDir=/srv/hiddenservice/venv/hostkeys --path . WorkingDirectory=/srv/hiddenservice/html User=nobody Group=nobody Restart=always [Install] WantedBy=multi-user.target Wants=network-online.target # usage (using root as needed): # # mkdir -p /srv/hiddenservice/html # >copy all web content to ^^^^^^^: # # cd /srv/hiddenservice # virtualenv venv # ./venv/bin/pip install txtorcon # # >put this file (that you're reading) in: # /etc/systemd/system/hidden-service-name.service # # >test: # systemctl daemon-reload # systemctl start hidden-service-name # systemctl status hidden-service-name # <-- check if it's running # # >enable at boot: # systemctl enable hidden-service-name.service # ln -s '/etc/systemd/system/hidden-service-name.service' '/etc/systemd/system/multi-user.target.wants/hidden-service-name.service' txtorcon-0.14.2/examples/hidden-service-systemd.service0000644000175000017500000000172012347473041023136 0ustar mikemike00000000000000# see http://twistedmatrix.com/documents/current/core/howto/systemd.html # started from that, and changed a few options [Unit] Description=Hidden-Service Web Server [Service] ExecStart=/srv/hiddenservice/venv/bin/twistd \ --nodaemon \ # recommended by Twisted --pidfile= \ # systemd doesn't need a PID-file web --port onion:80:hiddenServiceDir=/srv/hiddenservice/venv/hostkeys --path . WorkingDirectory=/srv/hiddenservice/html User=nobody Group=nobody Restart=always [Install] WantedBy=multi-user.target Wants=network-online.target # usage, as root: # # put this in /etc/systemd/system/hidden-service-name.service # # test: # systemctl daemon-reload # systemctl start hidden-service-name # systemctl status hidden-service-name # <-- check if it's running # # enable at boot: # systemctl enable hidden-service-name.service # ln -s '/etc/systemd/system/hidden-service-name.service' '/etc/systemd/system/multi-user.target.wants/hidden-service-name.service' txtorcon-0.14.2/examples/launch_tor.py0000755000175000017500000000205112572755166017721 0ustar mikemike00000000000000#!/usr/bin/env python # Launch a slave Tor by first making a TorConfig object. from sys import stdout from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks import txtorcon @inlineCallbacks def main(reactor): config = txtorcon.TorConfig() config.OrPort = 1234 config.SocksPort = 9999 try: yield txtorcon.launch_tor(config, reactor, stdout=stdout) except RuntimeError as e: print "Error:", e return proto = config.protocol print "Connected to Tor version", proto.version state = yield txtorcon.TorState.from_protocol(proto) print "This Tor has PID", state.tor_pid print "This Tor has the following %d Circuits:" % len(state.circuits) for c in state.circuits.values(): print c print "Changing our config (SOCKSPort=9876)" config.SOCKSPort = 9876 yield config.save() print "Querying to see it changed:" socksport = yield proto.get_conf("SOCKSPort") print "SOCKSPort", socksport if __name__ == '__main__': react(main) txtorcon-0.14.2/examples/stem_relay_descriptor.py0000755000175000017500000000254312627744056022170 0ustar mikemike00000000000000#!/usr/bin/env python # This shows how to get the detailed information about a # relay descriptor and parse it into Stem's Relay Descriptor # class. More about the class can be read from # # https://stem.torproject.org/api/descriptor/server_descriptor.html#stem.descriptor.server_descriptor.RelayDescriptor # # We need to pass the nickname or the fingerprint of the onion # router for which we need the the descriptor information, from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks import txtorcon @inlineCallbacks def main(reactor): proto = yield txtorcon.build_local_tor_connection(reactor, build_state=False) or_nickname = "moria1" print "Trying to get decriptor information about", or_nickname # If the fingerprint is used in place of nickname then, desc/id/ # should be used. descriptor_info = yield proto.get_info('desc/name/' + or_nickname) descriptor_info = descriptor_info['desc/name/' + or_nickname] try: from stem.descriptor.server_descriptor import RelayDescriptor relay_info = RelayDescriptor(descriptor_info) print "The relay's fingerprint is:", relay_info.fingerprint print "Time in UTC when the descriptor was made:", relay_info.published except ImportError as e: print "Error:", e if __name__ == '__main__': react(main) txtorcon-0.14.2/examples/launch_tor_endpoint.py0000755000175000017500000000506612572755166021632 0ustar mikemike00000000000000#!/usr/bin/env python # Here we set up a Twisted Web server and then launch a slave tor # with a configured hidden service directed at the Web server we set # up. This uses serverFromString to translate the "onion" endpoint descriptor # into a TCPHiddenServiceEndpoint object... from twisted.internet import reactor from twisted.web import server, resource from twisted.internet.endpoints import serverFromString import txtorcon class Simple(resource.Resource): isLeaf = True def render_GET(self, request): return "Hello, world! I'm a hidden service!" def setup_failed(arg): print "SETUP FAILED", arg def setup_complete(port): # the port we get back should implement this (as well as IListeningPort) port = txtorcon.IHiddenService(port) print "I have set up a hidden service, advertised at:", print "http://%s:%d" % (port.getHost().onion_uri, port.getHost().onion_port) print "locally listening on", port.local_address.getHost() print "Will stop in 60 seconds..." def blam(x): print "%d..." % x reactor.callLater(50, blam, 10) reactor.callLater(55, blam, 5) reactor.callLater(56, blam, 4) reactor.callLater(57, blam, 3) reactor.callLater(58, blam, 2) reactor.callLater(59, blam, 1) reactor.callLater(60, reactor.stop) def progress(percent, tag, message): bar = int(percent / 10) print '[%s%s] %s' % ('#' * bar, '.' * (10 - bar), message) # several ways to proceed here and what they mean: # # ep0: # launch a new Tor instance, configure a hidden service on some # port and pubish descriptor for port 80 # ep1: # connect to existing Tor via control-port 9051, configure a hidden # service listening locally on 8080, publish a descriptor for port # 80 and use an explicit hiddenServiceDir (where "hostname" and # "private_key" files are put by Tor). We set SOCKS port # explicitly, too. # ep2: # all the same as ep1, except we launch a new Tor (because no # "controlPort=9051") # ep0 = "onion:80" ep1 = "onion:80:controlPort=9051:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv" ep2 = "onion:80:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv" hs_endpoint = serverFromString(reactor, ep0) txtorcon.IProgressProvider(hs_endpoint).add_progress_listener(progress) # create our Web server and listen on the endpoint; this does the # actual launching of (or connecting to) tor. site = server.Site(Simple()) d = hs_endpoint.listen(site) d.addCallback(setup_complete) d.addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/stream_circuit_logger.py0000755000175000017500000000514312572755166022144 0ustar mikemike00000000000000#!/usr/bin/env python # This uses an IStreamListener and an ICircuitListener to log all # built circuits and all streams that succeed. import sys from twisted.python import log from twisted.internet import reactor import txtorcon def logCircuit(circuit): path = '->'.join(map(lambda x: str(x.location.countrycode), circuit.path)) log.msg('Circuit %d (%s) is %s for purpose "%s"' % (circuit.id, path, circuit.state, circuit.purpose)) def logStream(stream, state): circ = '' if stream.circuit: path = '->'.join(map(lambda x: x.location.countrycode, stream.circuit.path)) circ = ' via circuit %d (%s)' % (stream.circuit.id, path) proc = txtorcon.util.process_from_address( stream.source_addr, stream.source_port, state ) if proc: proc = ' from process "%s"' % (proc, ) elif stream.source_addr == '(Tor_internal)': proc = ' for Tor internal use' else: proc = ' from remote "%s:%s"' % (str(stream.source_addr), str(stream.source_port)) log.msg('Stream %d to %s:%d attached%s%s' % (stream.id, stream.target_host, stream.target_port, circ, proc)) class StreamCircuitLogger(txtorcon.StreamListenerMixin, txtorcon.CircuitListenerMixin): def __init__(self, state): self.state = state def stream_attach(self, stream, circuit): logStream(stream, self.state) def stream_failed(self, stream, reason='', remote_reason='', **kw): print 'Stream %d failed because "%s"' % (stream.id, remote_reason) def circuit_built(self, circuit): logCircuit(circuit) def circuit_failed(self, circuit, **kw): log.msg('Circuit %d failed "%s"' % (circuit.id, kw['REASON'])) def setup(state): log.msg('Connected to a Tor version %s' % state.protocol.version) listener = StreamCircuitLogger(state) state.add_circuit_listener(listener) state.add_stream_listener(listener) state.protocol.add_event_listener('STATUS_GENERAL', log.msg) state.protocol.add_event_listener('STATUS_SERVER', log.msg) state.protocol.add_event_listener('STATUS_CLIENT', log.msg) log.msg('Existing state when we connected:') for s in state.streams.values(): logStream(s, state) log.msg('Existing circuits:') for c in state.circuits.values(): logCircuit(c) def setup_failed(arg): print "SETUP FAILED", arg log.err(arg) reactor.stop() log.startLogging(sys.stdout) d = txtorcon.build_local_tor_connection(reactor) d.addCallback(setup).addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/multiple-socks-ports.py0000644000175000017500000000217112464055637021677 0ustar mikemike00000000000000#!/usr/bin/env python # This connects to the system Tor (by default on control port 9151) # and adds a new hidden service configuration to it. import os import functools import shutil from twisted.internet import reactor, defer from twisted.internet.endpoints import TCP4ClientEndpoint, TCP4ServerEndpoint from twisted.web import server, resource from twisted.internet.task import react import txtorcon def setup_failed(arg): print "SETUP FAILED", arg reactor.stop() @defer.inlineCallbacks def main(reactor): # Connect to the system Tor. Since we leave the # "build_state=True" default, this callbacks with a TorState # object. Otherwise, it would be a TorControlProtocol object. ep = TCP4ClientEndpoint(reactor, "localhost", 9051) state = yield txtorcon.build_tor_connection(ep) config = txtorcon.TorConfig(state.protocol) yield config.post_bootstrap if False: config.socksport = [5432, 4321] else: config.socksport.append(5555) config.socksport.append(4444) yield config.save() # run the reactor main loop until the last callback from main() fires react(main) txtorcon-0.14.2/examples/hidden_echo.py0000644000175000017500000000065512464631406020007 0ustar mikemike00000000000000from __future__ import print_function from twisted.internet import protocol, reactor, endpoints class Echo(protocol.Protocol): def dataReceived(self, data): self.transport.write(data) class EchoFactory(protocol.Factory): def buildProtocol(self, addr): return Echo() d = endpoints.serverFromString(reactor, "onion:1234").listen(EchoFactory()) d.addCallback(lambda x: print(x.getHost())) reactor.run() txtorcon-0.14.2/examples/circuit_for_next_stream.py0000755000175000017500000000744412515516430022501 0ustar mikemike00000000000000#!/usr/bin/env python # # This allows you to create a particular circuit, which is then used # for the very next (non-Tor-internal) stream created. The use-case # here might be something like, "I'm going to connect a long-lived # stream in a moment *cough*IRC*cough*, so I'd like a circuit through # high-uptime nodes" # import sys import functools import random from twisted.python import log from twisted.internet import reactor from zope.interface import implements import txtorcon class MyStreamListener(txtorcon.StreamListenerMixin): def stream_new(self, stream): print "new stream:", stream.id, stream.target_host def stream_succeeded(self, stream): print "successful stream:", stream.id, stream.target_host class MyAttacher(txtorcon.CircuitListenerMixin, txtorcon.StreamListenerMixin): implements(txtorcon.IStreamAttacher) def __init__(self, state): self.state = state # the circuit which we will use to attach the next stream to self.circuit = None def set_circuit(self, circuit): self.circuit = circuit def circuit_built(self, circuit): "ICircuitListener" if self.circuit is None: return if circuit != self.circuit: return print "Circuit built, awaiting next stream." def attach_stream(self, stream, circuits): """ IStreamAttacher API """ if self.circuit is not None: print "Attaching", stream, "to", self.circuit return self.circuit # let Tor connect this stream how it likes return None def stream_attach(self, stream, circuit): print "stream", stream.id, "attached to circuit", circuit.id, print "with path:", '->'.join(map(lambda x: x.location.countrycode, circuit.path)) if self.circuit is circuit: print "...so we're done." reactor.stop() def do_setup(path, state): print "Connected to a Tor version", state.protocol.version attacher = MyAttacher(state) state.set_attacher(attacher, reactor) state.add_circuit_listener(attacher) state.add_stream_listener(attacher) print "Existing state when we connected:" print "Streams:" for s in state.streams.values(): print ' ', s print print "General-purpose circuits:" for c in filter(lambda x: x.purpose == 'GENERAL', state.circuits.values()): path = '->'.join(map(lambda x: x.location.countrycode, c.path)) print ' ', c.id, path print "Building our Circuit:", path real_path = [] try: for name in path: print name if name == 'X': if len(real_path) == 0: g = random.choice(state.entry_guards.values()) real_path.append(g) else: g = random.choice(state.routers.values()) real_path.append(g) else: real_path.append(state.routers[name]) except KeyError, e: print "Couldn't find router:", e sys.exit(1) print "...using routers:", real_path d = state.build_circuit(real_path) d.addCallback(attacher.set_circuit).addErrback(log.err) return d def setup_failed(arg): print "Setup Failed:", arg.getErrorMessage() reactor.stop() if len(sys.argv) == 1: print "usage: %s router [router] [router] ..." % sys.argv[0] print print " You may use X for a router name, in which case a random one will" print " be selected (a random one of your entry guards if its in the first" print " position)." sys.exit(1) path = sys.argv[1:] d = txtorcon.build_local_tor_connection(reactor) d.addCallback(functools.partial(do_setup, path)).addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/hello_darkweb.py0000755000175000017500000000205412515516430020352 0ustar mikemike00000000000000#!/usr/bin/env python # This shows how to leverage the endpoints API to get a new hidden # service up and running quickly. You can pass along this API to your # users by accepting endpoint strings as per Twisted recommendations. # # http://twistedmatrix.com/documents/current/core/howto/endpoints.html#maximizing-the-return-on-your-endpoint-investment # # note that only the progress-updates needs the "import txtorcon" -- # you do still need it installed so that Twisted finds the endpoint # parser plugin but code without knowledge of txtorcon can still # launch a Tor instance using it. cool! from __future__ import print_function from twisted.internet import reactor, endpoints from twisted.web import server, static import txtorcon res = static.Data("Hello, hidden-service world!", 'text/html') ep = endpoints.serverFromString(reactor, "onion:80") txtorcon.IProgressProvider(ep).add_progress_listener(lambda p, tag, msg: print(msg)) ep.listen(server.Site(res)).addCallback(lambda port: print(str(port.getHost()))).addErrback(print) reactor.run() txtorcon-0.14.2/examples/launch_tor_endpoint2.py0000755000175000017500000000263312572755166021711 0ustar mikemike00000000000000#!/usr/bin/env python # Here we set up a Twisted Web server and then launch a slave tor # with a configured hidden service directed at the Web server we set # up. This uses serverFromString to translate the "onion" endpoint descriptor # into a TCPHiddenServiceEndpoint object... import shutil from twisted.internet import reactor from twisted.web import server, resource from twisted.internet.endpoints import serverFromString import txtorcon class Simple(resource.Resource): isLeaf = True def render_GET(self, request): return "Hello, world! I'm a hidden service!" site = server.Site(Simple()) def setup_failed(arg): print "SETUP FAILED", arg def setup_complete(port): local = txtorcon.IHiddenService(port).local_address.getHost() print "Hidden serivce:", port.getHost() print " locally at:", local def progress(percent, tag, message): bar = int(percent / 10) print '[%s%s] %s' % ('#' * bar, '.' * (10 - bar), message) hs_endpoint1 = serverFromString(reactor, "onion:80") hs_endpoint2 = serverFromString(reactor, "onion:80") txtorcon.IProgressProvider(hs_endpoint1).add_progress_listener(progress) txtorcon.IProgressProvider(hs_endpoint2).add_progress_listener(progress) d1 = hs_endpoint1.listen(site) d2 = hs_endpoint2.listen(site) d1.addCallback(setup_complete).addErrback(setup_failed) d2.addCallback(setup_complete).addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/tor_info.py0000755000175000017500000000476612572755166017421 0ustar mikemike00000000000000#!/usr/bin/env python # Simple usage example of TorInfo. This class does some magic so that # once it's set up, all the attributes it has (or appears to) are # GETINFO ones, in a heirarchy. So where GETINFO specifies # "net/listeners/dns" TorInfo will have a "net" attribute that # contains at least "listeners", etcetera. The leaves are all methods # which return a Deferred. If the corresponding GETINFO takes an # argument, so does the leaf. # # Go straight to "setup_complete" for the goods -- this is called # after TorInfo and the underlying TorControlProtocol are set up. # # If you want to issue multiple GETINFO calls in one network # transaction, you'll have to use TorControlProtocol's get_info # instead. import sys from twisted.internet import reactor, defer from txtorcon import TorInfo, build_local_tor_connection def error(x): print "ERROR", x return x @defer.inlineCallbacks def recursive_dump(indent, obj, depth=0): if callable(obj): try: print "%s: " % obj, sys.stdout.flush() if obj.takes_arg: v = yield obj('arrrrrg') v = yield obj() v = v.replace('\n', '\\') if len(v) > 60: v = v[:50] + '...' + v[-7:] except Exception, e: v = 'ERROR: ' + str(e) print v else: indent = indent + ' ' for x in obj: yield recursive_dump(indent, x, depth + 1) @defer.inlineCallbacks def setup_complete(info): print "Top-Level Things:", dir(info) if True: # some examples of getting specific GETINFO callbacks v = yield info.version() ip = yield info.ip_to_country('1.2.3.4') boot_phase = yield info.status.bootstrap_phase() ns = yield info.ns.name('moria1') guards = yield info.entry_guards() print 'version:', v print '1.2.3.4 is in', ip print 'bootstrap-phase:', boot_phase print 'moria1:', ns print 'entry guards:', guards # now we dump everything, one at a time d = recursive_dump('', info) d.addCallback(lambda x: reactor.stop()) d.addErrback(error) def setup_failed(arg): print "SETUP FAILED", arg reactor.stop() def bootstrap(c): info = TorInfo(c) info.post_bootstrap.addCallback(setup_complete).addErrback(setup_failed) d = build_local_tor_connection(reactor, build_state=False) # do not use addCallbacks() here, in case bootstrap has an error d.addCallback(bootstrap).addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/disallow_streams_by_port.py0000755000175000017500000000320412572755166022676 0ustar mikemike00000000000000#!/usr/bin/env python # # This uses a very simple custom txtorcon.IStreamAttacher to disallow # certain streams based solely on their port; by default it closes # all streams on port 80 or 25 without ever attaching them to a # circuit. # # For a more complex IStreamAttacher example, see # attach_streams_by_country.py # from twisted.python import log from twisted.internet import reactor from zope.interface import implements import txtorcon def stream_closed(x): print "Stream closed:", x class PortFilterAttacher: implements(txtorcon.IStreamAttacher) def __init__(self, state): self.state = state self.disallow_ports = [80, 25] print "Disallowing all streams to ports:", print ",".join(map(str, self.disallow_ports)) def attach_stream(self, stream, circuits): """ IStreamAttacher API """ if stream.target_port in self.disallow_ports: print "Disallowing", stream, "to port", stream.target_port d = self.state.close_stream(stream) d.addCallback(stream_closed) d.addErrback(log.err) return txtorcon.TorState.DO_NOT_ATTACH # Ask Tor to assign stream to a circuit by itself return None def do_setup(state): print "Connected to a Tor version", state.protocol.version state.set_attacher(PortFilterAttacher(), reactor) print "Existing streams:" for s in state.streams.values(): print ' ', s def setup_failed(arg): print "SETUP FAILED", arg reactor.stop() d = txtorcon.build_local_tor_connection(reactor) d.addCallback(do_setup).addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/redirect_streams.py0000644000175000017500000000270212527017754021114 0ustar mikemike00000000000000#!/usr/bin/env python from twisted.python import log from twisted.internet import reactor, defer from zope.interface import implements import txtorcon class MyAttacher(object): implements(txtorcon.IStreamAttacher) def __init__(self, state): # pointer to our TorState object self.state = state @defer.inlineCallbacks def attach_stream(self, stream, circuits): """ IStreamAttacher API """ print "XXXX", stream x = yield self.state.protocol.queue_command('REDIRECTSTREAM %s timaq4ygg2iegci7.onion' % stream.id) print "X", x defer.returnValue(None) # x = yield self.state.protocol.queue_command('ATTACHSTREAM %s 0' % stream.id) # print "X", x def do_setup(state): print "Connected to a Tor version", state.protocol.version attacher = MyAttacher(state) state.set_attacher(attacher, reactor) print "Existing state when we connected:" print "Streams:" for s in state.streams.values(): print ' ', s print print "General-purpose circuits:" for c in filter(lambda x: x.purpose == 'GENERAL', state.circuits.values()): print ' ', c.id, '->'.join(map(lambda x: x.location.countrycode, c.path)) def setup_failed(arg): print "SETUP FAILED", arg reactor.stop() d = txtorcon.build_local_tor_connection(reactor) d.addCallback(do_setup).addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/minimal_endpoint.py0000755000175000017500000000051112515516430021072 0ustar mikemike00000000000000#!/usr/bin/env python from __future__ import print_function from twisted.internet import reactor from twisted.internet.endpoints import serverFromString from twisted.web import server, static serverFromString(reactor, "onion:80").listen(server.Site(static.Data("Hello, world!", "text/plain"))).addCallback(print) reactor.run() txtorcon-0.14.2/examples/schedule_bandwidth.py0000755000175000017500000000435112572755166021410 0ustar mikemike00000000000000#!/usr/bin/env python # Here, we do something possible-useful and schedule changes to the # "BandWidthRate" and optionally "BandWidthBurst" settings in Tor. import datetime from twisted.internet import reactor from twisted.internet.interfaces import IReactorTime from txtorcon import build_local_tor_connection, TorConfig class BandwidthUpdater: def __init__(self, config, scheduler): self.bandwidth = 0 self.config = config self.scheduler = IReactorTime(scheduler) self.generator = self.next_update() def next_update(self): """ Generator that gives out the next time to do a bandwidth update, as well as what the new bandwidth value should be. Here, we toggle the bandwidth every 20 minutes. """ while True: if self.bandwidth: self.bandwidth = 0 self.burst = 0 else: self.bandwidth = 20 * 1024 * 1024 self.burst = self.bandwidth yield (datetime.datetime.now() + datetime.timedelta(minutes=20), self.bandwidth, self.burst) def do_update(self): x = self.generator.next() future = x[0] self.new_bandwidth = x[1] self.new_burst = x[2] tm = (future - datetime.datetime.now()).seconds self.scheduler.callLater(tm, self.really_update) print "waiting", tm, "seconds to adjust bandwidth" def really_update(self): print "setting bandwidth + burst to", self.new_bandwidth, self.new_burst self.config.set_config('BandWidthBurst', self.new_burst, 'BandWidthRate', self.new_bandwidth) self.doUpdate() def setup_complete(conf): print "Connected." bwup = BandwidthUpdater(conf, reactor) bwup.do_update() def setup_failed(arg): print "SETUP FAILED", arg reactor.stop() def bootstrap(proto): config = TorConfig(proto) config.post_bootstrap.addCallback(setup_complete).addErrback(setup_failed) print "Connection is live, bootstrapping config..." d = build_local_tor_connection(reactor, build_state=False, wait_for_proto=False) d.addCallback(bootstrap).addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/circuit_failure_rates.py0000755000175000017500000001775212515516430022132 0ustar mikemike00000000000000#!/usr/bin/env python # # This example uses ICircuitListener to monitor how many circuits have # failed since the monitor started up. If this figure is more than 50%, # a warning-level message is logged. # # Like the :ref:`stream_circuit_logger.py` example, we also log all new # circuits. # import functools import sys import time from twisted.internet import reactor, task from twisted.python import usage import txtorcon class Options(usage.Options): """ command-line options we understand """ optParameters = [ ['failed', 'f', 0, 'Starting value for number of failed circuits.', int], ['built', 'b', 0, 'Starting value for the total number of built cicuits.', int], ['connect', 'c', None, 'Tor control socket to connect to in ' 'host:port format, like "localhost:9051" (the default).'], ['delay', 'n', 60, 'Seconds to wait between status updates.', int]] def __init__(self): usage.Options.__init__(self) self['guards'] = [] self.docs['guard'] = 'Specify the name, built and failed rates ' \ 'like "SomeTorNode,10,42". Can be specified multiple times.' def opt_guard(self, value): name, built, failed = value.split(',') self['guards'].append((name, int(built), int(failed))) class CircuitFailureWatcher(txtorcon.CircuitListenerMixin): built_circuits = 0 failed_circuits = 0 percent = 0.0 failed_circuit_ids = [] per_guard_built = {} per_guard_failed = {} def print_update(self): print time.ctime(reactor.seconds()) + ': ' + self.information() def update_percent(self): self.percent = 100.0 * (float(self.failed_circuits) / float(self.built_circuits + self.failed_circuits)) if self.percent > 50.0: print 'WARNING: %02.1f percent of all routes' % self.percent print ' have failed: %d failed, %d built' % (self.failed_circuits, self.built_circuits) def information(self): rtn = '%02.1f%% of all circuits' % self.percent rtn += 'have failed: %d failed, %d built' % (self.failed_circuits, self.built_circuits) for g in self.per_guard_built.keys(): per_guard_percent = 100.0 * (self.per_guard_failed[g] / (self.per_guard_built[g] + self.per_guard_failed[g])) current = ' ' for guard in self.state.entry_guards.values(): if g == guard.name or g == guard.id_hex: current = '*' break rtn = rtn + '\n %s %s: %d built, %d failed: %02.1f%%' % \ (current, g, self.per_guard_built[g], self.per_guard_failed[g], per_guard_percent) return rtn def circuit_built(self, circuit): """ICircuitListener API""" # older tor versions will have empty build_flags if 'ONEHOP_TUNNEL' in circuit.build_flags: return if circuit.purpose == 'GENERAL': if len(circuit.path) > 0: if circuit.path[0] not in self.state.entry_guards.values(): print "WEIRD: first circuit hop not in entry guards:", print circuit, circuit.path, circuit.purpose return self.built_circuits += 1 self.update_percent() if len(circuit.path) != 3 and len(circuit.path) != 4: print "WEIRD: circuit has odd pathlength:", print circuit, circuit.path try: self.per_guard_built[circuit.path[0].unique_name] += 1.0 except KeyError: self.per_guard_built[circuit.path[0].unique_name] = 1.0 self.per_guard_failed[circuit.path[0].unique_name] = 0.0 def circuit_failed(self, circuit, kw): """ICircuitListener API""" if kw['REASON'] != 'MEASUREMENT_EXPIRED': return # older tor versions will have empty build_flags if 'ONEHOP_TUNNEL' in circuit.build_flags: return if circuit.purpose == 'GENERAL': if len(circuit.path) > 1: if circuit.path[0] not in self.state.entry_guards.values(): # note that single-hop circuits are built for various # internal reasons (and it seems they somtimes use # GENERAL anyway) print "WEIRD: first circuit hop not in entry guards:", print circuit, circuit.path return self.failed_circuits += 1 print "failed", circuit.id if circuit.id not in self.failed_circuit_ids: self.failed_circuit_ids.append(circuit.id) else: print "WARNING: duplicate message for", circuit if len(circuit.path) > 0: try: self.per_guard_failed[circuit.path[0].unique_name] += 1.0 except KeyError: self.per_guard_failed[circuit.path[0].unique_name] = 1.0 self.per_guard_built[circuit.path[0].unique_name] = 0.0 self.update_percent() def setup(options, listener, state): print 'Connected to a Tor version', state.protocol.version, print 'at', state.protocol.transport.addr listener.failed_circuits = int(options['failed']) listener.built_circuits = int(options['built']) listener.state = state # FIXME use ctor (ditto for options, probably) for name, built, failed in options['guards']: listener.per_guard_built[name] = float(built) listener.per_guard_failed[name] = float(failed) for circ in filter(lambda x: x.purpose == 'GENERAL', state.circuits.values()): if circ.state == 'BUILT': listener.circuit_built(circ) state.add_circuit_listener(listener) # print an update every minute task.LoopingCall(listener.print_update).start(options['delay']) def setup_failed(arg): print "SETUP FAILED", arg print arg reactor.stop() options = Options() try: options.parseOptions(sys.argv[1:]) except usage.UsageError: print "This monitors circuit failure rates on multi-hop PURPOSE_GENERAL circuits only." print "Tor internally uses other circuit types or GENERAL single-hop circuits for" print "internal use and we try to ignore these." print print "Every minute, the summary is printed out. For each entry-guard your Tor is" print "currently using, a separate count and summary is printed." print print "Nothing is saved to disc. If you wish to start again with the same totals" print "as a previous run, use the options below. On exit, a command-line suitable" print "to do this is printed." print print options.getUsage() sys.exit(-1) def on_shutdown(listener, *args): print '\nTo carry on where you left off, run:' print ' %s --failed %d --built %d' % (sys.argv[0], listener.failed_circuits, listener.built_circuits), for name in listener.per_guard_built.keys(): print '--guard %s,%d,%d' % (name, listener.per_guard_built[name], listener.per_guard_failed[name]), print listener = CircuitFailureWatcher() reactor.addSystemEventTrigger('before', 'shutdown', functools.partial(on_shutdown, listener)) if options['connect']: host, port = options['connect'].split(':') port = int(port) print 'Connecting to %s:%i...' % (host, port) d = txtorcon.build_local_tor_connection(reactor, host=host, port=port) else: d = txtorcon.build_local_tor_connection(reactor) d.addCallback(functools.partial(setup, options, listener)) d.addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/monitor.py0000755000175000017500000000125212572755166017254 0ustar mikemike00000000000000#!/usr/bin/env python # Just listens for a few EVENTs from Tor (INFO NOTICE WARN ERR) and # prints out the contents, so functions like a log monitor. from twisted.internet import reactor import txtorcon def log(msg): print msg def setup(proto): print "Connected to a Tor version", proto.version for event in ['INFO', 'NOTICE', 'WARN', 'ERR']: proto.add_event_listener(event, log) proto.get_info('status/version/current', 'version').addCallback(log) def setup_failed(arg): print "SETUP FAILED", arg reactor.stop() d = txtorcon.build_local_tor_connection(reactor, build_state=False) d.addCallback(setup).addErrback(setup_failed) reactor.run() txtorcon-0.14.2/examples/ephemeral_endpoint.py0000644000175000017500000000467612613264607021430 0ustar mikemike00000000000000#!/usr/bin/env python # This connects to the system Tor (by default on control port 9151) # and adds a new hidden service configuration to it. import os import functools import shutil from twisted.internet import reactor, defer from twisted.internet.endpoints import TCP4ClientEndpoint, TCP4ServerEndpoint from twisted.web import server, resource from twisted.internet.task import react import txtorcon # Ephemeral endpoints. You own all the bits. securely_stored_keyblob = '''RSA1024:MIICWwIBAAKBgQCsEuaUlvU651/lEl986XfX4QylkQCLhA9Nc19LTTt38oDeHRl3i5VgNsfsXyLrnk2iWapOsc3nmvxMt9vhFFanDB9p/rZTonERnTAw50M7PP4H4E8MDkPm6yZJSES7TPEI9u7WfSdq/HsNk4bsQU9Q3Vndy+hPZtPeGl+rs+3MawIDAQABAoGAGHPDIoBlLs6sWOAIg7almh7X7jsxyaGljwsDEq9R8RSb7XRTJyLFwltmg5dtXfAr9hMp2W745J2olrpV26FJQs4LFQBFawUwytvSV9IanpOew02yjvUQ0zqQUUbuR8rNHhzxJrvfJLDEzCmB8RBb1fE6BcUdv5t8xCu0/BwJdCkCQQDaL1ZJQ4aVHLcqru3IqiAwLsnA62aMNUPOO7twJ4YArX7Q6ZscqOPp8eLLoRzCYpMODcBX7kAOmuHxW8X3AKqnAkEAyeWoW01hlTzSY9kY/rMJOx5GKgDq3yqjyhshbEL1HDBh7mdt+4hTV2+a0L8CNCivfI7bbAS0oFCGRtX/Kgo8nQJBAIsxe+jNjYR/h1NRuh00e8iBcPEEvK1iJdniPZg1fsXb6XW6Mty72nsbd8bVCBXy8UIb/8OZGYC3ysFB/S+xWy0CQGHHtC3j4Cri9hIdhpl0JDhZhSm6oAXNJN4xHZLNKuCoHgXUWdPERnjGOHh4yZxxR+xPU72Q2dn6pc2Qvq+hnZECPzZ4O79cP7Y1ungZSzLZnoh0h1P9pSKDOq8qyyBD7SgW3tDGg04vkprlYnMH5EKp+BLC6rk4KRx0Za3wrY6Thw==''' class Simple(resource.Resource): isLeaf = True def render_GET(self, request): return "Hello, world! I'm a hidden service!" @defer.inlineCallbacks def main(reactor): ep = txtorcon.TCPEphemeralHiddenServiceEndpoint.system_tor( reactor, public_port=8080, control_endpoint=TCP4ClientEndpoint(reactor, 'localhost', 9251), private_key=securely_stored_keyblob, ) print "Starting site" port = yield ep.listen(server.Site(Simple())) addr = port.address print "Site started. Available at http://{}:{}".format(addr.onion_uri, addr.onion_port) print "Private key:\n{}".format(port.private_key) # in 5 seconds, remove the hidden service -- obviously this is # where you'd do your "real work" or whatever. d = defer.Deferred() @defer.inlineCallbacks def remove(): print "Removing the hiddenservice. Private key was" print hs.onion_private_key yield hs.remove_from_tor(tor_protocol) d.callback(None) if False: reactor.callLater(5, remove) print "waiting 5 seconds" else: print "waiting forever" yield d react(main) txtorcon-0.14.2/examples/torflow_path_selection.py0000644000175000017500000000545012464631504022330 0ustar mikemike00000000000000#!/usr/bin/env python # playing around with some ideas as far as bwauth (inside TorFlow) # works, and the path-selection stuff from TorCtl which has been # mentioned as a desireable txtorcon feature. import random from functools import partial from twisted.internet import defer from twisted.internet import task import txtorcon def node_selector(routers, # torstate, node_filter=lambda x: True, chooser=random.choice): nodes = filter(node_filter, routers) if len(nodes) == 0: raise RuntimeError("No nodes left after filtering.") return chooser(nodes) def flag_filter(flag, router): return flag in router.flags def cmp_bandwidth(router_a, router_b): return cmp(router_a.bandwidth, router_b.bandwidth) guard_filter = partial(flag_filter, 'guard') exit_filter = partial(flag_filter, 'exit') def percent_filter(torstate, min_pct, max_pct): if min_pct <= 0.0: raise RuntimeError("Minimum is too low") if max_pct >= 100.0: raise RuntimeError("Maximum is too high") def actual_filter(router): return def create_paths(routers): ''' This is a generator that produces paths the way Tor does: with a single guard node, a single middle node and a single exit node. Every time the generator runs it returns a tuple of Router instances representing the chosen path. FIXME need to consider when underlying router list changes. e.g. between any iteration it's possible all our cached data became stale... ''' guard_selector = lambda: node_selector(routers, node_filter=guard_filter) middle_selector = lambda: node_selector(routers) exit_selector = lambda: node_selector(routers, node_filter=exit_filter) while True: yield (guard_selector(), middle_selector(), exit_selector()) @defer.inlineCallbacks def main(reactor): state = yield txtorcon.build_local_tor_connection(reactor) yield state.post_bootstrap # make a generator for selecting from any possible router g = create_paths(state.all_routers) for i in range(10): path = g.next() print map(lambda r: r.name, path) # same, except we limit the routers to the top N% fastest ones in # each category: guard, middle, exit guards = filter(guard_filter, torstate.all_routers) guards.sort(key=lambda x: x.bandwidth) middles = list(torstate.all_routers) middles.sort(key=lambda x: x.bandwidth) exits = filter(exit_filter, torstate.all_routers) exits.sort(key=lambda x: x.bandwidth) print len(routers_by_bandwidth), "routers" percent = 10.0 top_n = int((percent / 100.0) * len(routers_by_bandwidth)) g = create_paths(routers_by_bandwidth[:top_n]) for i in range(10): path = g.next() print map(lambda r: r.name, path) task.react(main) txtorcon-0.14.2/examples/launch_tor2web.py0000644000175000017500000000241712572755166020504 0ustar mikemike00000000000000#!/usr/bin/env python # launch a tor, and then connect a TorConfig object to it and # re-configure it. This allows us to determine what features the # running tor supports, *without* resorting to looking at version # numbers. import sys from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks, Deferred import txtorcon @inlineCallbacks def main(reactor, tor_binary): config = txtorcon.TorConfig() config.ORPort = 0 config.SOCKSPort = 0 config.Tor2WebMode = 1 # leaving ControlPort unset; launch_tor will choose one print "Launching tor...", tor_binary try: yield txtorcon.launch_tor( config, reactor, tor_binary=tor_binary, stdout=sys.stdout ) print "success! We support Tor2Web mode" except RuntimeError as e: print "There was a problem:", str(e) print "We do NOT support Tor2Web mode" return print "quitting in 5 seconds" reactor.callLater(5, lambda: reactor.stop()) yield Deferred() # wait forever because we never .callback() if __name__ == '__main__': tor_binary = None if len(sys.argv) > 1: tor_binary = sys.argv[1] # Twisted's newer task APIs are nice react(main, (tor_binary,)) txtorcon-0.14.2/examples/webui_server.py0000755000175000017500000000674412572755166020301 0ustar mikemike00000000000000#!/usr/bin/env python from twisted.internet import reactor from nevow.appserver import NevowSite from nevow import loaders, tags, livepage import txtorcon def setup_failed(fail): print "It went sideways!", fail return fail class TorPage(livepage.LivePage): # override for Nevow/twisted.web addSlash = True # defaults for this class continuous_update = True ctx = None torstate = None # Could be done with XHTML 1.0, or a "real" templating language docFactory = loaders.stan( tags.html[ tags.head[ tags.directive('liveglue')], tags.body[ tags.h1["Tor Launching..."], # obviously you might want a javascript library or # something here instead of this hackery... tags.div(id='progress', style='position:abso lute; left:20em; top:10px; width:300px; height:50px; border:2px solid black;background-color:#ffaaaa;')[ tags.div(id='progress_done', style='position:absolute; top:0px; left:0px; width:0%; height: 100%; background-color:#aaffaa;')], # this is where the messages will go tags.div(id='status', style='padding:5px; background-color:#ffaaaa; text-indent:2em; width: 50em; font-weight:bold; border: 2px solid black;')[""]]]) def goingLive(self, ctx, client): ''' Overrides nevow method; not really safe to just save ctx, client in self for multiple clients, but nice and simple. ''' self.ctx = ctx self.client = client def set_tor_state(self, state): self.tor_state = state def tor_update(self, percent, tag, summary): if self.ctx is None: print "I have no Web client yet, but got a Tor update:", percent, tag, summary return point = int(300 * (float(percent) / 100.0)) self.client.send(livepage.js('''document.getElementById('progress_done').style.width = "%dpx";''' % point)) if percent == 100: # done, turn message box green too self.client.send(livepage.js('''document.getElementById("status").style.backgroundColor="#aaffaa";''')) if self.continuous_update: # add a text node for each update, creating a continuous list self.client.send(livepage.js('''var newNode = document.createElement('div'); newNode.appendChild(document.createTextNode("%d%% -- %s")); document.getElementById('status').appendChild(newNode);''' % (percent, summary))) else: self.client.send(livepage.set('status', "%d%% — %s" % (percent, summary))) # This only properly works with one client (the last one to load the # page). To work with multiples, we'd have to track all clients so # sending async updates to them worked properly. top_level = TorPage() # minimal Tor configuration config = txtorcon.TorConfig() config.OrPort = 1234 config.SocksPort = 9999 # launch a Tor based on the above config; the callback will trigger # when the TorControlProtocol and TorState instances are up and # running (i.e. Tor process is launched, and we connected to it via # control protocol and bootstrapped our notion of its state). d = txtorcon.launch_tor(config, reactor, progress_updates=top_level.tor_update) d.addCallback(top_level.set_tor_state) d.addErrback(setup_failed) print "Launching Tor and providing a Web interface on: \nhttp://localhost:8080\n" # Start up the Web server site = NevowSite(top_level) reactor.listenTCP(8080, site) reactor.run() txtorcon-0.14.2/examples/txtorcon.tac0000644000175000017500000000314412515516430017545 0ustar mikemike00000000000000import functools from os.path import dirname import sys from tempfile import mkdtemp import txtorcon from twisted.application import service, internet from twisted.internet import reactor from twisted.internet.endpoints import TCP4ClientEndpoint from twisted.python import log from twisted.web import static, server from zope.interface import implements class TorService(service.Service): implements(service.IService) directory = dirname(__file__) port = 8080 def __init__(self): self.torfactory = txtorcon.TorProtocolFactory() self.connection = TCP4ClientEndpoint(reactor, 'localhost', 9052) self.resource = server.Site(static.File(self.directory)) def startService(self): service.Service.startService(self) reactor.listenTCP(self.port, self.resource) self._bootstrap().addCallback(self._complete) def _bootstrap(self): self.config = txtorcon.TorConfig() self.config.HiddenServices = [ txtorcon.HiddenService(self.config, mkdtemp(), ['%d 127.0.0.1:%d' % (80, self.port)]) ] self.config.save() return txtorcon.launch_tor(self.config, reactor, progress_updates=self._updates, tor_binary='tor') def _updates(self, prog, tag, summary): log.msg('%d%%: %s' % (prog, summary)) def _complete(self, proto): log.msg(self.config.HiddenServices[0].hostname) application = service.Application("Txtorcon Application") torservice = TorService() torservice.setServiceParent(application) txtorcon-0.14.2/docs/0000755000175000017500000000000012627745451014317 5ustar mikemike00000000000000txtorcon-0.14.2/docs/txtorcon.rst0000644000175000017500000000030412515516430016713 0ustar mikemike00000000000000txtorcon Package ================ .. toctree:: txtorcon-protocol txtorcon-state txtorcon-config txtorcon-endpoints txtorcon-launching txtorcon-interface txtorcon-util txtorcon-0.14.2/docs/examples.rst0000644000175000017500000001466212605552415016670 0ustar mikemike00000000000000Examples ======== In the :file:`examples/` sub-directory are a few different mostly-simple ways of using txtorcon. They all show how to set up a connection and then wait for and use various information from Tor. .. _hello_darkweb.py: :file:`hello_darkweb.py` ------------------------ :download:`Download the example <../examples/hello_darkweb.py>`. This is a minimal (but still working) hidden-service set up using the endpoint parsers (these are Twisted ``IPlugin`` implementations; see `the documentation `_ for more). It even shows Tor's progress messages on the console. .. literalinclude:: ../examples/hello_darkweb.py .. _disallow_streams_by_port.py: :file:`disallow_streams_by_port.py` ----------------------------------- :download:`Download the example <../examples/disallow_streams_by_port.py>`. An example using :class:`~txtorcon.torstate.IStreamAttacher` which is very simple and does just what it sounds like: never attaches Streams exiting to a port in the "disallowed" list (it also explicitly closes them). Note that **Tor already has this feature**; this is just to illustrate how to use IStreamAttacher and that you may close streams. .. literalinclude:: ../examples/disallow_streams_by_port.py .. _launch_tor.py: :file:`launch_tor.py` --------------------- :download:`Download the example <../examples/launch_tor.py>`. Set up a tor configuration and launch a slave Tor. This takes care of the setting Tor's notion ownership so that when the control connection goes away, so does the running Tor. .. literalinclude:: ../examples/launch_tor.py .. _launch_tor_endpoint.py: :file:`launch_tor_endpoint.py` ------------------------------ :download:`Download the example <../examples/launch_tor_endpoint.py>`. Using the :class:`txtorcon.TCP4HiddenServiceEndpoint` class to start up a Tor with a hidden service pointed to an :api:`twisted.internet.interfaces.IStreamServerEndpoint `; fairly similar to :ref:`launch_tor_with_hiddenservice.py` but more things are automated. .. literalinclude:: ../examples/launch_tor_endpoint.py .. _launch_tor_with_hiddenservice.py: :file:`launch_tor_with_hiddenservice.py` ---------------------------------------- :download:`Download the example <../examples/launch_tor_with_hiddenservice.py>`. A more complicated version of the :ref:`launch_tor.py` example where we also set up a Twisted Web server in the process and have the slave Tor set up a hidden service configuration pointing to it. .. literalinclude:: ../examples/launch_tor_with_hiddenservice.py .. _stream_circuit_logger.py: :file:`stream_circuit_logger.py` -------------------------------- :download:`Download the example <../examples/stream_circuit_logger.py>`. For listening to changes in the Circuit and State objects, this example is the easiest to understand as it just prints out (some of) the events that happen. Run this, then visit some Web sites via Tor to see what's going on. .. literalinclude:: ../examples/stream_circuit_logger.py .. _attach_streams_by_country.py: :file:`circuit_for_next_stream.py` ------------------------------------ :download:`Download the example <../examples/circuit_for_next_stream.py>`. This creates a custom stream specified via router names on the command-line and then attaches the next new stream the controller sees to this circuit and exits. A decent custom-circuit example, and a little simpler than the following example (attach_streams_by_country). .. literalinclude:: ../examples/circuit_for_next_stream.py .. _attach_streams_by_country.py: :file:`attach_streams_by_country.py` ------------------------------------ :download:`Download the example <../examples/attach_streams_by_country.py>`. This is one of the more complicated examples. It uses a custom Stream attacher (via :class:`~txtorcon.torstate.IStreamAttacher`) to only attach Streams to a Circuit with an exit node in the same country as the server to which the Stream is going (as determined by GeoIP). Caveat: the DNS lookups go via a Tor-assigned stream, so for sites which use DNS trickery to get you to a "close" server, this won't be as interesting. For bonus points, if there is no Circuit exiting in the correct country, one is created before the Stream is attached. .. literalinclude:: ../examples/attach_streams_by_country.py .. _schedule_bandwidth.py: :file:`schedule_bandwidth.py` ----------------------------- :download:`Download the example <../examples/schedule_bandwidth.py>`. This is pretty similar to a feature Tor already has and is basically useless as-is since what it does is toggle the amount of relay bandwidth you're willing to carry from 0 to 20KiB/s every 20 minutes. A slightly-more-entertaining way to illustate config changes. (This is useless because your relay takes at least an hour to appear in the consensus). .. literalinclude:: ../examples/schedule_bandwidth.py .. _dump_config.py: :file:`dump_config.py` ----------------------------- :download:`Download the example <../examples/dump_config.py>`. Very simple read-only use of :class:`txtorcon.TorConfig` .. literalinclude:: ../examples/dump_config.py .. _monitor.py: :file:`monitor.py` ----------------------------- :download:`Download the example <../examples/monitor.py>`. Use a plain :class:`txtorcon.TorControlProtocol` instance to listen for SETEVNET updates. In this case marginally useful, as it listens for logging things INFO, NOTICE, WARN, ERR. .. literalinclude:: ../examples/monitor.py .. _stem_relay_descriptor.py: :file:`stem_relay_descriptor.py` -------------------------------- :download:`Download the example <../examples/stem_relay_descriptor.py>`. Get information about a relay descriptor with the help of `Stem's Relay Descriptor class `_. We need to specify the nickname or the fingerprint to get back the details. .. literalinclude:: ../examples/stem_relay_descriptor.py .. _circuit_failure_rates.py: :file:`circuit_failure_rates.py` -------------------------------- :download:`Download the example <../examples/circuit_failure_rates.py>`. .. literalinclude:: ../examples/circuit_failure_rates.py .. _txtorcon.tac: :file:`txtorcon.tac` -------------------- :download:`Download the example <../examples/txtorcon.tac>` Create your own twisted `Service` for deploying using ``twistd``. .. literalinclude:: ../examples/txtorcon.tac txtorcon-0.14.2/docs/howtos.rst0000644000175000017500000001250012515516430016357 0ustar mikemike00000000000000HOWTOs ====== Try txtorcon in a Virtualenv ---------------------------- Setting up txtorcon in a virtualenv is a really easy way to play around with it without "messing up" your system site-packages. If you're unfamiliar with ``virtualenv``, you can read more `at readthedocs `_. .. code-block:: shell-session $ virtualenv try_txtorcon $ . ./try_txtorcon/bin/activate $ pip install txtorcon # will install Twisted etc as well $ python try_txtorcon/share/txtorcon/examples/circuit_failure_rates.py # ... You can also use the above virtualenv to play with ``twistd`` and endpoints; see below. Install txtorcon On Debian -------------------------- Thanks to work by `Lunar `_, txtorcon is usually rapidly packaged into Debian. This means that it gets into `jessie `_ fairly quickly, and then arrives in `wheezy-backports `_ a couple weeks after that. You can see the current status on the `Debian QA Page for txtorcon `_ If you're using ``jessie`` (testing), simply: .. code-block:: shell-session $ apt-get install python-txtorcon For wheezy users, you'll need to enabled the ``wheezy-backports`` repository to Apt. There are `instructions on the Debian wiki `_ If you're in a hurry, you could try this: .. code-block:: shell-session # echo "deb http://ftp.debian.org/debian wheezy-backports main contrib non-free" >> /etc/apt/sources.list # apt-get update # apt-get install -t wheezy-backports python-txtorcon .. _howto-endpoint: Endpoints Enable Tor With Any Twisted Service --------------------------------------------- .. raw:: html
(or view `directly on asciienma.org `_). As of v0.10.0, there is full support for :api:`twisted.plugin.IPlugin `-based endpoint parsers. This adds an ``onion:`` prefix to the system. (If you're unfamiliar with Twisted's endpoint system, `read their high-level documentation `_ first). So, with txtorcon installed, **any** Twisted program that uses :api:`twisted.internet.endpoints.serverFromString ` and lets you pass endpoint strings can cause a new or existing hidden-service to become available (usually by launching a new Tor instance). Twisted's own `twistd `_ provides a Web server out of the box that supports this, so if you have a collection of documents in ``~/public_html`` you could make these available via a hidden-service like so (once txtorcon is installed): .. code-block:: shell-session $ twistd web --port "onion:80" --path ~/public_html You can look in the ``twistd.log`` file created to determine what the hidden-serivce keys are. **You must save them** if you want to re-launch this same onion URI later. If you've done that, you can (re-)launch a hidden-service with existing keys by adding an argument to the string: .. code-block:: shell-session $ ls /srv/seekrit/my_service hostname private_key $ twistd web --port "onion:80:hiddenServiceDir=/srv/seekrit/my_service" --path ~/public_html To find out your service's hostname and where the private key is located, look in the ``twistd.log`` file, which will look something like this (trunacted for space): .. code-block:: shell-session ... 2014-06-13 23:48:39-0600 [-] Spawning tor process from: /tmp/tortmpkh4bsM 2014-06-13 23:48:40-0600 [TorControlProtocol,client] 10% Finishing handshake with directory server ... 2014-06-13 23:48:53-0600 [TorControlProtocol,client] 90% Establishing a Tor circuit 2014-06-13 23:48:54-0600 [TorControlProtocol,client] 100% Done 2014-06-13 23:48:54-0600 [TorControlProtocol,client] Site starting on 48275 2014-06-13 23:48:54-0600 [TorControlProtocol,client] Starting factory 2014-06-13 23:48:54-0600 [TorControlProtocol,client] Started hidden service "rv5gkzutsh2k5bzg.onion" on port 80 2014-06-13 23:48:54-0600 [TorControlProtocol,client] Keys are in "/tmp/tortmpoeZJYC". See :class:`txtorcon.TCPHiddenServiceEndpointParser` for all the available options. To test the Web server, you can simply launch with a local-only server string, like so: .. code-block:: shell-session $ twistd web --port "tcp:localhost:8080" --path ~/public_html $ curl http://localhost:8080/index.html If you need more control over the options passed to Tor, you can use the existing Python APIs to accomplish any Tor configuration and launching you like (or connect to already-running Tor instances). Although Twisted Matrix themselves don't recommend doing "Web development" with Twisted, the Twisted Web server is a robust provider of HTTP and HTTPS services. It also supports WSGI so can easily front a Python-based Web application (e.g. Django or Flask). ``twistd`` provides several other services as well; see `twistd(1) `_ for more information. txtorcon-0.14.2/docs/_themes/0000755000175000017500000000000012627745451015743 5ustar mikemike00000000000000txtorcon-0.14.2/docs/_themes/README0000644000175000017500000000021612515516430016607 0ustar mikemike00000000000000This is the "alabaster" theme that I've hacked upon a little. All the HTML mistakes are mine :) See https://github.com/bitprophet/alabaster txtorcon-0.14.2/docs/_themes/alabaster/0000755000175000017500000000000012627745451017701 5ustar mikemike00000000000000txtorcon-0.14.2/docs/_themes/alabaster/_version.py0000644000175000017500000000012012515516430022055 0ustar mikemike00000000000000__version_info__ = (0, 6, 0) __version__ = '.'.join(map(str, __version_info__)) txtorcon-0.14.2/docs/_themes/alabaster/donate.html0000644000175000017500000000047212515516430022031 0ustar mikemike00000000000000{% if theme_gittip_user %}

Donate

Consider supporting the authors on Gittip:

{% endif %} txtorcon-0.14.2/docs/_themes/alabaster/support.py0000644000175000017500000001146412515516430021762 0ustar mikemike00000000000000from pygments.style import Style from pygments.token import Keyword, Name, Comment, String, Error, \ Number, Operator, Generic, Whitespace, Punctuation, Other, Literal # Originally based on FlaskyStyle which was based on 'tango'. class Alabaster(Style): background_color = "#f8f8f8" # doesn't seem to override CSS 'pre' styling? default_style = "" styles = { # No corresponding class for the following: #Text: "", # class: '' Whitespace: "underline #f8f8f8", # class: 'w' Error: "#a40000 border:#ef2929", # class: 'err' Other: "#000000", # class 'x' Comment: "italic #8f5902", # class: 'c' Comment.Preproc: "noitalic", # class: 'cp' Keyword: "bold #004461", # class: 'k' Keyword.Constant: "bold #004461", # class: 'kc' Keyword.Declaration: "bold #004461", # class: 'kd' Keyword.Namespace: "bold #004461", # class: 'kn' Keyword.Pseudo: "bold #004461", # class: 'kp' Keyword.Reserved: "bold #004461", # class: 'kr' Keyword.Type: "bold #004461", # class: 'kt' Operator: "#582800", # class: 'o' Operator.Word: "bold #004461", # class: 'ow' - like keywords Punctuation: "bold #000000", # class: 'p' # because special names such as Name.Class, Name.Function, etc. # are not recognized as such later in the parsing, we choose them # to look the same as ordinary variables. Name: "#000000", # class: 'n' Name.Attribute: "#c4a000", # class: 'na' - to be revised Name.Builtin: "#004461", # class: 'nb' Name.Builtin.Pseudo: "#3465a4", # class: 'bp' Name.Class: "#000000", # class: 'nc' - to be revised Name.Constant: "#000000", # class: 'no' - to be revised Name.Decorator: "#888", # class: 'nd' - to be revised Name.Entity: "#ce5c00", # class: 'ni' Name.Exception: "bold #cc0000", # class: 'ne' Name.Function: "#000000", # class: 'nf' Name.Property: "#000000", # class: 'py' Name.Label: "#f57900", # class: 'nl' Name.Namespace: "#000000", # class: 'nn' - to be revised Name.Other: "#000000", # class: 'nx' Name.Tag: "bold #004461", # class: 'nt' - like a keyword Name.Variable: "#000000", # class: 'nv' - to be revised Name.Variable.Class: "#000000", # class: 'vc' - to be revised Name.Variable.Global: "#000000", # class: 'vg' - to be revised Name.Variable.Instance: "#000000", # class: 'vi' - to be revised Number: "#990000", # class: 'm' Literal: "#000000", # class: 'l' Literal.Date: "#000000", # class: 'ld' String: "#4e9a06", # class: 's' String.Backtick: "#4e9a06", # class: 'sb' String.Char: "#4e9a06", # class: 'sc' String.Doc: "italic #8f5902", # class: 'sd' - like a comment String.Double: "#4e9a06", # class: 's2' String.Escape: "#4e9a06", # class: 'se' String.Heredoc: "#4e9a06", # class: 'sh' String.Interpol: "#4e9a06", # class: 'si' String.Other: "#4e9a06", # class: 'sx' String.Regex: "#4e9a06", # class: 'sr' String.Single: "#4e9a06", # class: 's1' String.Symbol: "#4e9a06", # class: 'ss' Generic: "#000000", # class: 'g' Generic.Deleted: "#a40000", # class: 'gd' Generic.Emph: "italic #000000", # class: 'ge' Generic.Error: "#ef2929", # class: 'gr' Generic.Heading: "bold #000080", # class: 'gh' Generic.Inserted: "#00A000", # class: 'gi' Generic.Output: "#888", # class: 'go' Generic.Prompt: "#745334", # class: 'gp' Generic.Strong: "bold #000000", # class: 'gs' Generic.Subheading: "bold #800080", # class: 'gu' Generic.Traceback: "bold #a40000", # class: 'gt' } txtorcon-0.14.2/docs/_themes/alabaster/static/0000755000175000017500000000000012627745451021170 5ustar mikemike00000000000000txtorcon-0.14.2/docs/_themes/alabaster/static/pygments.css0000644000175000017500000000621712515516430023543 0ustar mikemike00000000000000.highlight{background-color:#073642;color:#93a1a1}.highlight .c{color:#586e75 !important;font-style:italic !important}.highlight .cm{color:#586e75 !important;font-style:italic !important}.highlight .cp{color:#586e75 !important;font-style:italic !important}.highlight .c1{color:#586e75 !important;font-style:italic !important}.highlight .cs{color:#586e75 !important;font-weight:bold !important;font-style:italic !important}.highlight .err{color:#dc322f !important;background:none !important}.highlight .k{color:#cb4b16 !important}.highlight .o{color:#93a1a1 !important;font-weight:bold !important}.highlight .p{color:#93a1a1 !important}.highlight .ow{color:#2aa198 !important;font-weight:bold !important}.highlight .gd{color:#93a1a1 !important;background-color:#372c34 !important;display:inline-block}.highlight .gd .x{color:#93a1a1 !important;background-color:#4d2d33 !important;display:inline-block}.highlight .ge{color:#93a1a1 !important;font-style:italic !important}.highlight .gr{color:#aa0000}.highlight .gh{color:#586e75 !important}.highlight .gi{color:#93a1a1 !important;background-color:#1a412b !important;display:inline-block}.highlight .gi .x{color:#93a1a1 !important;background-color:#355720 !important;display:inline-block}.highlight .go{color:#888888}.highlight .gp{color:#555555}.highlight .gs{color:#93a1a1 !important;font-weight:bold !important}.highlight .gu{color:#6c71c4 !important}.highlight .gt{color:#aa0000}.highlight .kc{color:#859900 !important;font-weight:bold !important}.highlight .kd{color:#268bd2 !important}.highlight .kp{color:#cb4b16 !important;font-weight:bold !important}.highlight .kr{color:#d33682 !important;font-weight:bold !important}.highlight .kt{color:#2aa198 !important}.highlight .n{color:#268bd2 !important}.highlight .na{color:#268bd2 !important}.highlight .nb{color:#859900 !important}.highlight .nc{color:#d33682 !important}.highlight .no{color:#b58900 !important}.highlight .ni{color:#800080}.highlight .nl{color:#859900 !important}.highlight .ne{color:#268bd2 !important;font-weight:bold !important}.highlight .nf{color:#268bd2 !important;font-weight:bold !important}.highlight .nn{color:#b58900 !important}.highlight .nt{color:#268bd2 !important;font-weight:bold !important}.highlight .nx{color:#b58900 !important}.highlight .bp{color:#999999}.highlight .vc{color:#008080}.highlight .vg{color:#268bd2 !important}.highlight .vi{color:#268bd2 !important}.highlight .nv{color:#268bd2 !important}.highlight .w{color:#bbbbbb}.highlight .mf{color:#2aa198 !important}.highlight .m{color:#2aa198 !important}.highlight .mh{color:#2aa198 !important}.highlight .mi{color:#2aa198 !important}.highlight .mo{color:#009999}.highlight .s{color:#2aa198 !important}.highlight .sb{color:#d14}.highlight .sc{color:#d14}.highlight .sd{color:#2aa198 !important}.highlight .s2{color:#2aa198 !important}.highlight .se{color:#dc322f !important}.highlight .sh{color:#d14}.highlight .si{color:#268bd2 !important}.highlight .sx{color:#d14}.highlight .sr{color:#2aa198 !important}.highlight .s1{color:#2aa198 !important}.highlight .ss{color:#990073}.highlight .il{color:#009999}.highlight div .gd,.highlight div .gd .x,.highlight div .gi,.highlight div .gi .x{display:inline-block;width:100%}txtorcon-0.14.2/docs/_themes/alabaster/static/alabaster.css_t0000644000175000017500000002447012515516430024157 0ustar mikemike00000000000000{% set page_width = '80%' %} {% set sidebar_width = '25%' %} {% set theme_sidebar_header = theme_sidebar_header or theme_gray_1 %} {% set theme_sidebar_link = theme_sidebar_link or theme_gray_1 %} {% set theme_anchor_hover_fg = theme_anchor_hover_fg or theme_gray_1 %} {% set theme_note_bg = theme_note_bg or theme_gray_2 %} {% set theme_footnote_border = theme_footnote_border or theme_gray_2 %} {% set theme_pre_bg = theme_pre_bg or theme_gray_2 %} {% set theme_narrow_sidebar_link = theme_narrow_sidebar_link or theme_gray_3 %} {% set theme_sidebar_hr = theme_sidebar_hr or theme_gray_3 %} @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro'; font-size: 1.2em; background-color: white; color: #000; margin: 0; padding: 0; } div.document { width: {{ page_width }}; margin: 30px auto 0 auto; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 {{ sidebar_width }}; } div.sphinxsidebar { width: {{ sidebar_width }}; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: {{ theme_body_text }}; padding: 0 30px 0 30px; } div.footer { width: {{ page_width }}; margin: 20px auto 30px auto; font-size: 14px; color: {{ theme_footer_text }}; text-align: right; } div.footer a { color: {{ theme_footer_text }}; } div.related { display: none; } div.sphinxsidebar a { color: {{ theme_sidebar_link }}; text-decoration: none; border-bottom: 1px dotted {{ theme_sidebar_link_underscore }}; } div.sphinxsidebar a:hover { border-bottom: 1px solid {{ theme_sidebar_link_underscore }}; } div.sphinxsidebar span.pre { font-size: 75%; } div.sphinxsidebar { font-size: 14px; font-size: 1.5em; font-size: 1.3vw; line-height: 1.5; } div.sphinxsidebarwrapper { padding: 18px 10px; } body>p.logo { padding: 0; margin: -10px 0 0 0px; text-align: center; width: 70%; margin-left: 15%; display: none; } div.sphinxsidebarwrapper p.logo { padding: 0; margin: -10px 0 0 0px; text-align: center; width: 100%; } img.logo { width: 100%; } p.badges a { border-bottom: none; text-decoration: none; } h1.logo a { border-bottom: none; text-decoration: none; } div.sphinxsidebarwrapper h1.logo { margin-top: -10px; text-align: center; margin-bottom: 5px; text-align: {{ theme_logo_text_align }}; font-size: 2em; } div.sphinxsidebarwrapper h1.logo-name { margin-top: 0px; } div.sphinxsidebarwrapper p.blurb { margin-top: 0; } div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: 'Garamond', 'Georgia', serif; color: {{ theme_sidebar_header }}; font-size: 24px; font-weight: normal; margin: 0 0 5px 0; padding: 0; } div.sphinxsidebar h4 { font-size: 20px; } div.sphinxsidebar h3 a { color: {{ theme_sidebar_link }}; } div.sphinxsidebar p.logo a, div.sphinxsidebar h3 a, div.sphinxsidebar p.logo a:hover, div.sphinxsidebar h3 a:hover { border: none; } div.sphinxsidebar p { color: {{ theme_sidebar_text }}; margin: 10px 0; } div.sphinxsidebar ul { margin: 10px 0; padding: 0; color: {{ theme_sidebar_list }}; } div.sphinxsidebar ul li.toctree-l1 > a { font-size: 120%; } div.sphinxsidebar ul li.toctree-l2 > a { font-size: 110%; } div.sphinxsidebar input { border: 1px solid {{ theme_sidebar_search_button }}; font-family: 'Georgia', serif; font-size: 1em; } div.sphinxsidebar hr { border: none; height: 1px; color: {{ theme_sidebar_link_underscore }}; background: {{ theme_sidebar_link_underscore }}; text-align: left; margin-left: 0; width: 50%; } /* -- body styles ----------------------------------------------------------- */ a { color: {{ theme_link }}; text-decoration: underline; } a:hover { color: {{ theme_link_hover }}; text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Source Sans Pro', sans-serif; margin: 30px 0px 10px 0px; padding: 0; } div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: {{ theme_anchor }}; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: {{ theme_anchor_hover_fg }}; background: {{ theme_anchor_hover_bg }}; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { margin: 20px 0px; padding: 10px 30px; } div.admonition tt.xref, div.admonition a tt { border-bottom: 1px solid #fafafa; } dd div.admonition { margin-left: -60px; padding-left: 60px; } div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight { background-color: white; } dt:target, .highlight { background: #FAF3E8; } .highlight pre { border-radius: 0.3em; border: 3px solid {{ theme_note_border }}; background: #002B36; /*solarized dark*/ } div.note { background-color: {{ theme_note_bg }}; border: 3px solid {{ theme_note_border }}; border-radius: 0.3em; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Source Code Pro', 'Consolas', monospace; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid {{ theme_footnote_border }}; background: {{ theme_footnote_bg }}; font-size: 0.9em; } table.footnote + table.footnote { margin-top: -15px; border-top: none; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td.label { width: 0px; padding: 0.3em 0 0.3em 0.5em; } table.footnote td { padding: 0.3em 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } blockquote { margin: 0 0 0 30px; padding: 0; } ul, ol { margin: 10px 0 10px 30px; padding: 0; } pre { background: {{ theme_pre_bg }}; padding: 7px 30px; margin: 15px 0px; line-height: 1.3em; } dl pre, blockquote pre, li pre { margin-left: -60px; padding-left: 60px; } dl dl pre { margin-left: -90px; padding-left: 90px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; border-bottom: 1px solid white; } a.reference { text-decoration: none; border-bottom: 1px dotted {{ theme_link }}; } a.reference:hover { border-bottom: 1px solid {{ theme_link_hover }}; } a.footnote-reference { text-decoration: none; font-size: 0.7em; vertical-align: top; border-bottom: 1px dotted {{ theme_link }}; } a.footnote-reference:hover { border-bottom: 1px solid {{ theme_link_hover }}; } a:hover tt { background: #EEE; } @media screen and (max-width: 870px) { div.sphinxsidebar { display: none; } body p.logo { display: block; } div.document { width: 100%; } div.documentwrapper { margin-left: 0; margin-top: 0; margin-right: 0; margin-bottom: 0; } div.bodywrapper { margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; } ul { margin-left: 0; } .document { width: auto; } .footer { width: auto; } .bodywrapper { margin: 0; } .footer { width: auto; } .github { display: none; } } @media screen and (max-width: 875px) { body { margin: 0; padding: 20px 30px; } div.documentwrapper { float: none; background: white; } div.sphinxsidebar { display: block; float: none; width: 102.5%; margin: 50px -30px -20px -30px; padding: 10px 20px; background: {{ theme_narrow_sidebar_bg }}; color: {{ theme_narrow_sidebar_fg }}; font-size: 1.5em; } div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, div.sphinxsidebar h3 a { color: white; } div.sphinxsidebar a { color: {{ theme_narrow_sidebar_link }}; } div.sphinxsidebar p.logo { display: none; } body p.logo { display: block; } div.document { width: 100%; margin: 0; } div.related { display: block; margin: 0; padding: 10px 0 20px 0; } div.related ul, div.related ul li { margin: 0; padding: 0; } div.footer { display: none; } div.bodywrapper { margin: 0; } div.body { min-height: 0; padding: 0; } .rtd_doc_footer { display: none; } .document { width: auto; } .footer { width: auto; } .footer { width: auto; } .github { display: none; } } /* misc. */ .revsys-inline { display: none!important; } /* Make nested-list/multi-paragraph items look better in Releases changelog * pages. Without this, docutils' magical list fuckery causes inconsistent * formatting between different release sub-lists. */ div#changelog > div.section > ul > li > p:only-child { margin-bottom: 0; } txtorcon-0.14.2/docs/_themes/alabaster/__init__.py0000644000175000017500000000072612515516430022004 0ustar mikemike00000000000000import os from alabaster import _version as version def get_path(): """ Shortcut for users whose theme is next to their conf.py. """ # Theme directory is defined as our parent directory return os.path.abspath(os.path.dirname(os.path.dirname(__file__))) def update_context(app, pagename, templatename, context, doctree): context['alabaster_version'] = version.__version__ def setup(app): app.connect('html-page-context', update_context) txtorcon-0.14.2/docs/_themes/alabaster/layout.html0000644000175000017500000000515412515516430022076 0ustar mikemike00000000000000{%- extends "basic/layout.html" %} {%- block extrahead %} {{ super() }} {% if theme_touch_icon %} {% endif %} {% endblock %} {%- block relbar2 %}{% endblock %} {%- block header %} {% if theme_logo %} {% else %}

{{ project }}

{% endif %} {% endblock %} {%- block footer %} {% if theme_github_banner|lower == 'true' %} Fork me on GitHub {% endif %} {% if theme_analytics_id %} {% endif %} {%- endblock %} txtorcon-0.14.2/docs/_themes/alabaster/navigation.html0000644000175000017500000000042712515516430022716 0ustar mikemike00000000000000

Navigation

{{ toctree(includehidden=theme_sidebar_includehidden) }} {% if theme_extra_nav_links %}
    {% for text, uri in theme_extra_nav_links.items() %}
  • {{ text }}
  • {% endfor %}
{% endif %} txtorcon-0.14.2/docs/_themes/alabaster/about.html0000644000175000017500000000346412515516430021675 0ustar mikemike00000000000000{% if theme_logo %}

{{ project }}

{% endif %}

{% else %}

{{ project }}

{% endif %} {% if theme_description %}

{{ theme_description }}

{% endif %} {% if theme_github_button|lower == 'true' %}

{% endif %}

{% if theme_travis_button|lower != 'false' %} {% if theme_travis_button|lower == 'true' %} {% set path = theme_github_user + '/' + theme_github_repo %} {% else %} {% set path = theme_travis_button %} {% endif %} https://secure.travis-ci.org/{{ path }}.png?branch=master {% endif %} {% if theme_coveralls_button|lower != 'false' %} {% if theme_coveralls_button|lower == 'true' %} {% set path = theme_github_user + '/' + theme_github_repo %} {% else %} {% set path = theme_coveralls_button %} {% endif %} code coverage {% endif %} {% if theme_flattr_uri != '' %} {% set path = theme_flattr_uri %}

{% endif %} txtorcon-0.14.2/docs/_themes/alabaster/theme.conf0000644000175000017500000000163712515516430021646 0ustar mikemike00000000000000[theme] inherit = basic stylesheet = alabaster.css pygments_style = alabaster.support.Alabaster [options] logo = logo_name = false logo_text_align = left description = github_user = github_repo = github_button = true github_banner = false github_type = watch github_count = true travis_button = false coveralls_button = false flattr_uri = gittip_user = analytics_id = touch_icon = extra_nav_links = sidebar_includehidden = true show_powered_by = true gray_1 = #444 gray_2 = #EEE gray_3 = #AAA body_text = #3E4349 footer_text = #888 link = #004B6B link_hover = #6D4100 sidebar_header = sidebar_text = #555 sidebar_link = sidebar_link_underscore = #999 sidebar_search_button = #CCC sidebar_list = #000 sidebar_hr = anchor = #DDD anchor_hover_fg = anchor_hover_bg = #EAEAEA note_bg = note_border = #CCC footnote_bg = #FDFDFD footnote_border = pre_bg = narrow_sidebar_bg = #333 narrow_sidebar_fg = #FFF narrow_sidebar_link = txtorcon-0.14.2/docs/_static/0000755000175000017500000000000012627745451015745 5ustar mikemike00000000000000txtorcon-0.14.2/docs/_static/avatar.png0000644000175000017500000004675612312757205017742 0ustar mikemike00000000000000‰PNG  IHDRÈ·W,ŠgAMA± üasRGB®Îé cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿÿÿ ½§“ pHYs × ×B(›xLãIDATxÚí½y|$çyßù}ߪêh\3À`îûàð)rHñ’DJ")‰¶dÉ:l9²ìıl'ÙÄYo²É*k¯7ëDvìÄN|È—,É¢,Š”(K$ER$Åk8äÜ÷…™Á}õÝ]ÝUõ¾ûGu7ƒc €ø}>`ú¨®®zïs?ÐZ³ŒÑp]W !¥$"¥D ‰”Ã0B\ëS\”ÐZ“Íf)•JÔ××cYÖ‚¿òZŸÀBC±XÔƒƒƒ†A8Fàÿ^&ÆUAA]]±XŒL&ƒëº ~w^&H òù¼$°ø|ÕŸe’Ì‚Á Ñh”t:½àI²L22™ŒN  1-Ÿ¢Fz€O”eÌ I– ¤Ò)L%‡ÃHiBTþ¨He‚Ì&I–9Äå*Õ²ý1×ÔÕÕ‘Éd(•J Š$K– …BA  b„ãKÊã˘+X–E]]Ùl–b±¸`H²$ ⺮ìDzL¤”Àˆj5B–íy†eYÔ×דËå°m{AdIdppÆ4ͪZåÿZ¶?®5LÓ$S(I–A …‚ÎòXË@øÒCˆeûc¡À0 êëë)  …kJ’%Gt:…aVÒE|)Q"ËöÇÂaÄãqlÛ¾¦$YRqGç ù²j5²øÇÚbÙþXRVIr­Ô­%EL& h„¬È‹‰ìåü« TÞ¦xô *W¸&Ÿ/¥¤¾¾ž|>M¼[æ5ùÖ×J)ÉfÊi$å…¿l\…72ôß¾J`ëzê>ò‘;oB„‚óz›$N#¥Ôó™¼d’Ëåp=—°*ÛËöÇ• r2ß/•ÁÞw”âÑ3Døg?L`óÚy=Ó4«YÀ ZJ9/7fɨX©t Ó4F±l\…½‡(90 0$ºè{áMúÿ¯ÿAԼžO   ‘Édæí3—AlÛÖÅbÃ0‘¢Ê!–íË¡ry2ßm—F€!q»úúý¿!ýçÑ®7¯ç‰DBÏççÅYI¥“H£ÖÖ‘TÔ>·tIRxó0ÅçÁg‰H‰ÊåI|åÛdžþ1¨ùµc±¶mãyÞœðuOÇut.—Å4kk<&¶?ªÿ[Âö‡.–ÈþèuTÁ†‰J²…@ÛE’ý$ù7ÌëùI) ‡Ãär¹¹ÿ¬yýf×™L…ò³u/·?IJý1„iRÿ±‡ˆ¾ÿNd$ Þ¶†¨T†ä_?‰Û;8¯ç …PJÍyм¸ž›6(¥ôÅÎ ¡1 Ëo¾` a ¥DH‰! „HCbÔ4g²üš%J]r°"óÔ öAÛEãì©Jÿ¹Óø…ÏëùÙ¶]i1g7麖 ù|Ç)!¥1b3ýÁ²ýq9DÀ"|û ¬ø÷ÿ”–óy¬Mk'´7r/îŹÐ=¯ç ñpsE#ÖÚ¶qw| ¾y;¡›¶£í"nï8ÎøÒ¡ì9Q™<öÁÞ>žÂ\µ9Í‚,Û¶ ƒH)g]ź®2œþR¡˜Ç4­* ªö‡X¶?&‚0$檕¸}C¸=åÇÚ*•¡°ïÚõlZ3~ŒCÌDöÜD`Ó¼áî@¯™Œ(‰…·Rp#^‡µ¦uLÐP†ƒ„nØBä®[0šêñ†R¾ús…‡Î(8GᵸýCX­ÍñºQï–Ipëz·݀7œÂ¹Ø3æp*‘"|ÇMþ{Ë( ƒA ÃX&ÈåÈd2_J¤†ýÈyURŒØr<ûC.Ûµ(ì;Báõ£voA¥³6­·2PÆ"„nÜV&Jo8…Je®L”‚Méø9 o¥±Ö·É6â1B·îÂÆ9×9rQšãe‰re¢¨tŽÂ;Ç(ï"°¡£)>ú¸¡Á›)êðÓUj’'½DšÈ7!뢔J%”R„B¡9ÛÙ-A váKƒý–Y¶;–í™À>|Šâñ³~kŸšÝÚÏ¿¢ðÖa´]Âlk×# £á¢Tl”T4“Ú(î…ìÇ1W4Éï’Ñ0æê•öFìre±Ö®"¸}#ù|ž`0ˆišs"=`d`°ÿKv©PvíÊ ìyEû£šµD ܱ se3nw¿o×.êrs8ûÐ) oÄKf0›âõщ‰rãÖêïö ¢2¹ê±Æ¾¡œßõÎ1d]„àæu£ x³µûà‰êcÂ2‰=|/ÖÚUäóùòlûÙÏÁª`QÄqÝÓ×aÊÑä“^R¼,õ]\öØR…°L‚Û7¹ãFD8ˆ7@eó#Á»ŠG*•¥xè…×àö #ê0êÆÍþ•uQB7o÷c(†Û3ˆÎÛµ-+kN@ íö¡“ÈHˆÀ¶ £Ži­_EéÄy?ò®ÁZÓJçEF”J%¿cÍdQFÒtï@Á` Æ~€åo‚†!1 Ãðs³D529ôÈûªí­«ÅÑ¿+_wRGkœK½ä~¼—ü+oSºÐ %×ßÕk‹¨´ÆhŠÞs3±ÞCp禉3o=EñÄ9ÒOüˆüë|ui¼ IkD8Dã犺ŸzpTîUá­# üÎÿÂKf¨{ô>Z~óAR©‘H„¹œ²è¢”ÒgΞ¦äý¨ªªh5eµå¢óÚÅ\»¨G~äH¯Þjg“ÑP.»\£ÈSK˜J¡•aÕ¿3¼áöþãä^Ú‡}ø”Ÿ“å·‰ñ_ 5(ŒÇß¾›ØÃ÷ºq넹XºèPØw˜ô?<ëwMq½±DÑ Òð¹Ç¨ÿøC#¤óÃú8éo?KËoý±‡î N …Ë© ›Ëúií‚‘&oFÅÖgñ_F9IªÞ­ñÞ;ò>*•ÏÇßP5J©Q?•&™¦9Š<‹ºäP:ßIþå}ä_y§«Ï÷RU%¯O ¾u'±ÝKè];ýÔ•qà§¿¿Iæ©çq.ôŒU»´FÄ?óñŸ}¸J8·wá?ù¿ü ß ÇoƒK› ®çjÛ.ËgÉò¸®Cí.^ë¾1Æ‘&RL"a&8þøRÆG…4åÖ˜Õ¥T•8†aŒ"΂–8Zãö ‘{iÙg_ÅéèòÛ^&QD8Hè¦mĹðí»}Ï×x÷´»Ÿôw_$÷ìkxéÑéïZ#,“ºŸ~†Ï}ÔÅhpû1šª’%›õ3(æÒÍ»` âyž¶‹rù…BÇ-•kuÆ[¸/îq‰#§KŒuéjñåÄq]ÏóÐZ"M…8 ‘4îÀ0¹½Næ_Æíì+<…îÞJÝ#÷¾ó¦ñ‰¢ÅãçHýóÞ88º9v¹0+öð½4þÒÇGEÏ+Xrñòá;n72¯íù7þÖ³~W”ËÒß#÷ÞFÓ¯~seÓ¨÷e³Y سL^XQJébÉ&ŸÏ‘¯!Åø†õLmˆñ$ÊD¯[‹¯ ­õ(ÂT$M­”©æš£çQ[7ÐöåßDÖE«¯Éd2X–µx ¢µžãci1#Öâ(µŠñ_!ME-³,k~ã)ìcgHë?—ªX7æÖ˜m-Ô}ä½Ä¾×oEZ{ÿq¿ü×8—z1Wµ°ê÷ ³}EõùEéæÕZkÇ)Q(( 8N ¥•oôʉI± ëÅ‚ŠZV*•FÆ4MÀœFŠä^}‡ô·Ÿ›¸ö¼ì*îÚLüÓ¾c÷¨ªÆÂþã }ù¯ñ)ÚþËoܵ¹úÜ¢ j­µë:Š6¶÷Ó§HŠëͰ^ ¨µc*N˲*™±³~=½¡$™¼Böé—pûGåxÕœ2!úÐ]Ä?ù!ÌU#’ÂÞœ¡?ü;>ÿSDïwõñT*E4Å4Í…G­µv=»–eƒ±Œ›+î/;âZÃó<ŠÅ"®ëV£þå$ÀÙ»ÎZS:ßIúÛÏ‘{é­r“†ñÔ.l^Küç?Bä=·V¥‰}è$ºä¾}wõåsÙ0®‚iDk­=ÏÅ.±‹J¥âhRŒ·ãÏ8j}ýÖ •@&PuµÏêñ—ÂÛGI󇨇Oùu(r¬4‘0ußKüSc´4úãá”B”=tsÝ0®‚)ÄUž.ml{„Œ³'´!*s9¦$-–öŒò ¥P…"Å¡Nÿ0r(…;0Œ°L̶›×b®ZQ]¼S:d:KöÙ×HçG¸ÝýcÕ®òº îÞJã/~ŒÐÍÛG=¯”"•JÑÐЀ˜ÃÅ2† *ok§³çb*›G¶4PŠGp£At8€°,„!G&6UçþÕü0¾$÷gÙ°^PÐ%•Îâ$p{p.õà\êõÿL Ò9(9~Â"€ib4ʼnÞwñO=rŤ—Ãéè"õø3ä~¼wüTx¥0šˆöÃþ´«rÄu]r¹ñx|NN• *“Ó¹—Þ"÷ü”Îu¢r_ÔY&"Æh¨G6Ç‘+1Z‘MõÈx"F„Cˆ`À­i@M핈¸i Maú¯ÒÓló­Q…"^"Û;èá¢O¯o/™Fåm(Gï+÷’ñ6²²ú3^´{J§RrÈ¿~€Ô׿OéôÿÁÚÏQ ÙXOÛ—ÿ-~wŹžn[ à &ôÐ}ükûËyú#C»:™ñƒ>ç:Ë'OÃÓð‰QþÁ0j lʯR–Ÿ·¡2D„CÈh‹bÄcå µzŒ†:d} #BÁY›e·¤¡*WðÛét÷ã\èÁ¹Ð…ÓÙ‡Û?ŒJgÑÅ’¯ãW†™V"áR^9Ï@ƒÐàæmÿÓ´[DÀ"zÿ» îØDú[Ïùá+£x)Ñyo(e‚xž7/&@ö¹×É¿¼ÏzãPoçОçï0vqÔÅšà?ÕÿêšÇ«7Äð¥Œ}âÔÇ0›0Z›±ÚZ0ÛZ0[›ýv–uQ¿]̲Z6ÚõPÙ<Þ`¢L†nœŽnœ®¾r9m]*ç»U  Ê÷wª6„КjñT0€hªGn^|än2¹<‘p¨ÀªM;¨khôkNók Ó rïmX›ÖüÛï’{ñM_«Ñw0Q}Ý|I¡µ¦ðöQ=ôå¿ÆKfü¡ïžJWwzQ£.ü¾(ûÉÑþ¹ ! YÁhiÄZÓŠµa5k°Ö´b47LتfÑ ¼q¨lÞŸÖÔ;è“áR/nw¿¯"%3(ÛW•¯Ë$ö j~+!p AÎ$0„d aAÉp(–2ع%;V~êH(eÓîÛ¸á=²~ÇÍ46·`3Û´t¡Húé“úú÷ñ†’ÔìAšÿåçæÍƒ#])íô à %QÉ îp o(‰7”ÂKøí%½l·Ñv í8h×'Qe±Ž\àJ˘Ë/ù¨¯>ò«¢ë^Í÷,çõTó¼, YÅ\ÙŒµ~Íë°6­Áj_‰ÑG­™ÖA{Ú.¡2¹òزaܞܮ~œÞ¼„¿åmÿúWT$Qc3Ì•~’ðÐØB“6C†Ç€é1dxdMg0‚Ê-à9•Àpå(#÷BiE(cî[Ù}ïÙ~óíÔÕÇgt~hMaïa†ÿø˜kZiýí_ÇQŠ|>?ç,¸RD£µç¢‹º`£ò¶¿“¥³¨T•Îâer¨L•-  T¡è|¥2‰<­FZ)}Ã>`!,ÓW’éCQ—%ÖÕz¸jI#%"ð›'¯^I`Ó[ÖXߎÑÚŒ‹^qÌ×UŠëë¢ì’+×0™ñÉÐ?„Û7Œ7˜ÀK¤QÙÊ.¨GˆYÙHj›«* ¤Yá’.ÒePº$¤KNj!ª¿=ð4h5’§V5ÈMQº^*ƒÛÙGñôJ';(utá %Ñv±F–W¯Ú•%¦Œ…1[š°Ö­ÂÚ´Æ'LS¼\Ì#üïà¸þëù¾C"d(€Ö\ÏßÑ‹Ú.¢ì"*Wð*›G:ço$Ù<:W@lÿõ® žª.:QK€«ÔjU% 8ZQÀ#…ËpCÒ%-=l¡QR`X&€‰i‚%”gãyŽ¿ÁÍ|]øD©‹³sÏ{¹÷ßdÕÚ 3»}¹2 •͇ç4I±z¯uEáåЮ«½á4ÎÅJ§:(žìÀ¹Ð…7˜Dåm´òF{_®êÃF«fX&2€ŠkYk+50Œ÷sÍóÚ+¿®æ¸Õ ]+`Ví¢ËÉࢱµGZx$p>RÒ#/5 Hà  Š„°­lÜR·d£”7»÷µ|=W­ãÞÇ>Ãm÷}€`(4íãTì¹N1©^Û…FË¡=O«TÖwYžë¤tæ"¥Ž.ÜÞÁ²Zæø_¤RžyÕxùQ&Ðï«×mþœbÔoBã °…"ƒK—!á0,Ÿ BQEÙµ®ýBªh]õMC&ž“%ŸÆÎeð\‡Ãpn •BZvÜù^üÄ/L[š”J% …¼Ø°2ZkU(úº{·ïÚìëC‡ŽQIH …¿£ëJÌåzÞfQ{ÊB „À“P’ŠŒðH — 2Ú%‡S&ƒß&RûA„  ÒØÜÌšuëØ´m+ñ¶V^úá?Ð{á4N©F7h”Ò4­ÞÀû?ùn¾ëþ)ÇM²Ù,RJ"‘ȼœðô£9×B !#!ß­{ëN½÷Òa Þ ÒlFh±Â´ˆ0 Jq4VÉC–<„çU%ÄåÛ¼nºF”Õ.]&#E †&#iá’Â%­²Ú%¯]JÊÃE±2!P¾C" oh ­½µ6°vÃzV¶µÅ0 % ÎoÙÎÅS‡¯Qœ?/ÑÕÁ“ÿó?ÓyöïûéÏRo¸â;MÓÄqœy;ÓÅGËpæð!¿ö%ƒn·€( Ã$dˆCÔGÔi“º’ \p=®& ÀÔ!å1 •¤•Tûê§]¾&¢Õe1£JÚÔ@“—š¬ô%B > Ú£¨=ÏCé ´ÈŠ‡¼ÌÃ0E"466ÒºjkÖ­£}íZZZW«««îÌ@)…К»x˜‹gNpáäákÖEH‰WÌóÆ÷¾Aÿ¥ó<ú¹/Ò¾~ã¤ï±,‹B¡€ÖZÏu ®‚ß÷…\YŽªV‘r]rŽC޽Pm*i44†Ò®ÆTS¥ÀÒ`iA…ô„ÀDbé©Z V)ùõ#ý² `Bù·6 ´e MƒB©H2“"ãÉ*‡‚r()LJ”z]eƒÿ˜”‚bõu´´¬ ­½UkÖкªÆæf‘H5º\KˆË¡µ¦>æÁü,÷u‘M%®]6µð7£³û_ã›É!ý'¿ÁÖÝ·Lx>•ò`¥Ôüåb-fعœßÉcœòÛ1@ër@Œ²N/ ¦ZSWþ­êÚ+¿ÚÿoÕUmn]>zEJ(E (ÆOkUÝõG‘€JnÓÈ¢F¤!±‚A¢±MÍͬlk¥­½ÖU«hln&‹a¾sBONˆñ <—ë6qÿ‡>Æ3Oü-žã\ÓŒ)%ýçOòÔŸýúܯ³óÖ;1@¢ê¢'ÈwÝË1Ô×Kz8ÏùuÖwë$U‰c{·—ÿbJÍè¼ÌšË8(—@k´†A ö=KM4·´ÐÜÒLcS 446údå1q•ch­ÑÞÌ­'íyÜvû= öt²÷•gçï¦Mt¤d¸ëÏüÍáy_dç­w Î}Bâ¤ç´è¼X—AÛí bÛ6™tšÄÐà ÷÷“"L’Ïf(ä 8宕…zÔ¢Kˆ‘l™iî¬eIU=v™V0H8£¾¡‘¦•+iikcEk+ÍÍÍÔ×× …˜&²¦!´¾,ž2ÛBÏexêñ¿âØ¡½,„YZ)ZV¯ç¡Ÿû"[n¼HM»RÏóÈd2Äãñ9ÏÃ‚ë€ *ŸÓÞ@oÕ%Z‰FWª¹¥Åb‘B>O!Ÿ'ŸË‘ËæÈçsryìBÛ¶)‹”J%¿—”ã“ÈS ¯fœ®ª2euI×øÃ´¿š–E $‰­¯§®±‘†æf›}©P__O$&`„òsM„É „$™àÉo~…³'O»žc. •båÚ|àçõ;n$"„ P(àº.uuuËq©@òÚè\½©Ô;Œ6k’DkE0á¶÷>ʺMÛpÒ ×¥è8äsRÉ!{»êíÄÎç5›ŸRÛoÝÃ'~ã?‹7Í›YüqŸ ž{­OåºB…$O~ó+tœ9~Õ$ÑJQß¼‚|ê—¸é–=%Û/!¨q©{ŠžKï@/û_{£o¾DÉÎxÖ´æ®G~†‡?÷ELkîSÝẠˆ«½¾.¿¶b³ )%ý½<ñõ?£óâÙ»€µR4¬lã±ÏÿK¶nÚŽÊg'|­„À3-Žy›çÿKý=>AµÆ xøs¿Æ]úؼ´Û\ô6ÈH‘Ñâ&úB‚Ãð;4¯lç=þ4±ú¦9B´ÖDêxôs_dÛ¦m“’ÊãCœ¢ãæ›ïàg¾øïhY½Î/*§Tâ…oý§¼9/×bÑK´Ö*“ÂK%F5-cJP¡•¦P,1”Ls©g€Óº8s¡‹K½ƒÔ“"^º„ž¦­'¤äŸþeîyàaT:5½÷šfÛjNy›Çÿðÿ®æŒ)¥h[¿™Oÿ«ÿDëºMsß8nQC!ëV@{Éat©xõǼŽá \Q^hš|Áf`8ÅùÎ^Nœ»Ä©ŽN.öô“He)–ûg! h™l­ÑÈMù³”òØù®»¸ãþ‡Ñ¹ì”ßWö\´Sbí¶h\ÑF&9„þœùÞ gùÁWÿ„Oüúÿ©£õ sF’ÅO2D8" + ½át!­OgÁ v£Rš\Á¦o0Á¹K=?{‘“ç;éì$•ÎR*Ûq•FâRŠr™‰Æ.¹œN±š¥+~®ÖšX¼‘ûû A4žç2íª5­Ñ¥"2 G=%¥äÔþ7xé;Ç?û+Ú˜£!× A„i £¾A»E{”ºe R …D隺ñ뵄ðŽW´«ê–?r ¿"Q+\WQr ÅÙ|t&O2“%‘Ê0œÊHgIer¤³y²ù»ˆ],áº>i<¥ÐÊï4XéRYäZk2y›·žâäùNþù§?BÛŠF´f”ôQJã¸.ùB‘D:Kïà0—z8ßÙËÅî~z†If²Ø%]V*’|r„‚ëc´·¶°qM›×µ³au+m-ÄcQ³|N#„ðÆÙõ]×eÿ[?!=I¢RŠÕ›¶qÛ{Û¾úø”Rà:X—Ù µÐZsäõsÇCͺÁ¾(òÆ›ïðO|ŸúúÊó3okSÍ¥HÃðKaËÓ[C–AC$ÂPý«¢›É6'È\æ…‹/ðùŸG"V! ´ç¢”âÐÉóœïìe(‘b0™f8™&Í“·‹¾¾.Ë$Q S‹oˆ³º½•H8D(Ä švŽ`uY|ÿ©ÒvëJ~ù‘_`E}Kµ!ºÔŠBÁæO¾ú§Î]âG¯½ÃÀpŠOø½¬ln Í“HeNÑ7˜ wp˜¾¡$‰T†L®@Éq|i#FzaQîH9ÆnX×ÎÆ5m´·6ÓX_G(ðëMÊÆwÅæ¸RIA±X ›I¡§Ž•¨·öÒŸjóÛ›~KZ`Zqïžwñýç_ãà±ÓH))9ÅR©\¯Â({GJ9¾Ýо’•MÛ ž7[ÁSA÷¥l»0þ¬­‰Ö5pïÃ'P´ñ2éêsé\A)“!‹M³ÖDà–Š¤†®ð*æ¤rQdåŠfÒéÌ´ßgCÄ›¸1á Ÿz”KÐÏÞ¿:Á\üsÚWµ‘âìés9ÝÁ`"…aH¶m^ËêÕ«€JUâÔ˜ö<¼LŠUu­ì‰ï¡ãÞÄqA«ÙJxU˜?¹ð'<Ôøïo.»#M‹Æ-õaÚW6ùå¾Wi7Ì„€s§‘JWm­5±XœOüü¯ÒÐØB4V?ª”YÙB@8ŒeYUI …0š›I !¥Äº‚$ñ´æÄÁ·pŠö(Éu9”ò8þÖO¸í}ê@04kž¬EAðwŸÁÁ¡i@„ÂP²Y·fá`€p(Ä–õíÔÇ"Äë¢4Ö×Q,•xâÙŸpôtC V·µúÞ²@ <¼‡ÝÛ¿ÁQû£›Ø݉,)¼R픪îOË2¹e×Vž{å­ª:Wkœ/‹6'޾ƒRªÆ~ÐX -ÔÕ7Œ©,:E×£©¡qÔwÑZcY`Ïó&%ˆ’þÁ~NyûŠö…’®³Çé½p†uÛvÏÚw_D‰ˆáá„Îç : Ok‡†!„ôÊæ&âuQ}`ŸùÈû éG¢•Ò¬hŒó?ÿþiÞ:pŒÕ·]U:ŠrÖц`+ÚÕxÉáq §5lß¼Žx,B2“c8•¦µ¥qÁDJIo÷ŲzUÛ¤Âï“+¥s®žç‘Éç©olšÐ ¯–ÝNaœ=sŒtbèŠÞ/!ùLš“ï¼>«YTõ ÍÍMfšˆ«‹±¶}%»6¯Ã2 ?¯bˆkÍ£ïÝÃg>ü>^zãÉTúªëq W9xÊlZkV­l¦½µ™x,ÂBkC¯µæäÑýrÙQ×C†i!¥1ê«)¥Hf³Äâ ‚Á ŽÉ”:Ê—”Çé£ûk¦Z]á\Sûß ŸIÍÚ‰„…a …i_a˜C!nݵ…µ«VŒIëЀ!%?ûÈìÞ²·›—~µZk"áŸùÈûøÿõغaõ”Û‡Î5„d3)N?4îó†a”¥ÀȰW»T" ‡'‘À¾="'¹¾Ò0èíï¦ëÜ©)»o¥”ô]êàÒéc³v Ab±ù|oºV«”B˜&÷ÜvMñúqU­5Ëäg¹¡<ҙ̼DJÉ-»¶°imÛ¤‹f¾!„ óÂYú»ÇU‡¤4Æ\Ïó®8‡°Òf²xˆ6LŽØK>;=I^*ä9ùÎë³v ALÓáp˜t:=*yq*PZS`N2°SkM4fÏÍ;(ä²ó²› !FT½¥gNž0sWÊÊ ~,FcƤ6”뺭'$ˆ’D:ɉw^ŸvG'!玼C&14+jÖ¢#@$RJ²ÙéU©•ŠE¬)´Ì×Z ‡ ¹Ëtï¹À5=p…sÊç2\<zb«hœñÓz’…_yO.—#°&<®0MŽx“á¾îiGÇ…” ÷uÑuöĬ\‡EI€úúúJ#c=5I¢µcXÖ”Âk¾$‰PÌç)禌·šÕ›ÉøsK@'ÃÚsìïax¨âöeÒS -˜pn‡;ŸÇ+ ƒãÞ!‰lН=?ãÆu¥¢ÍÙÃoÏÊuX8wdšBˆxÜNŸH$°m{Òu_*Q¥Ò´†®H!¨‡É¤’þ ’Ù9oÜr—òÄà8%âÑ赺”)½Ý)Ú&ò¬i=ûð³}‹XàøD/7žÎ¦SÄ˨Dža²ï+ôw]¸Š^\‚ '`ç²W­f-š8ȸ—AQWW‡ã8:›ÍR(t(ªÞ JôÖu]Š™4Ñ€5muƲ,¢Z“¢¾¡‘@ 0­…¨´×T Ïó(‹”Š6Úóñš©P J)ú{»ÐZ!„1ák*—B)…í8Ä›ê«ß»r\×%—Íâ–ŠÄËsǃ”’žÞyåÙ«¶+ÊéïÃ}]´oÚ~U×aQ¤˲Dcc#¥RI—s¶ªJáׇ„‚àØ3:~(@ A:1L & VG]]ÎúEûÏsq¯l˜Z†A̲0Ãá‘J¿ÏsH^!}\)¯L A6—C!p]—R©„R Ïuñ\­ýÍG=¼øô7ýÅ;‡‰šZk\ǹªc\ŸÑZ«¢ÍÅ-h8N‰á)td¯í<!H¥<ûÄß’˜}»c<¨«Ì¡». ¢=‡eõjúBPÈçH&¯¸¹ )‘Ó\äï¼þ"çO™§Ìå«ot]åM{TØ2|!H%†È¤“£Š¡&z­˜b›2¿|7ͱÃûæ1x*®ºÖæ:%Èõ; gî!èëéôÓܯ°¸”R¨)VAúå»gèëž×º!¯.Súú$È2f ¥<:/žR|Ãs]Ü)ŽVSJqêøÁIïÌ6„Slq:®ÏdEÓDÆ´fSxžGÉuðÊc,ÓÄ*÷}Z*B˦é¾tþŠêøqJÅÚ†Š7JpáÜɹtZû¹¦uåö¦“áº$ˆ0-!"1­ÓÉ+¾ViE¾P P´Ú¯1 ƒ’ë ´Â2-ÌXÐ4ÂW¯†û®ÜÔM\Ï¥4…L!Ý~ùîï¤B‚|.K>Ÿ]Rž+ðÙþ¾nΞ<2Í B0<Ô_6ÔÇŸë:\<úª «¦ ­±AÌe‚LéáýêÀßýù—ùÁS_#14€)%'íçñ¿ýcΞ::¥îã× ´æèÁ7'ï<ªQ÷ { !ȦSôv_œWïø*V ¾bŸà+áú¶AjϤôþ—~ÈÛ/ü#½—Ρ=ie/Œ?rmh —SÇÒÓyÛ÷<À÷ò¹,RúÙÂ!QÊãô‰ÃÜòî{ç|F¡”’KgøþK:14+38”R´¯ÙÀ¦m7Œ"¸ŽR õv-˜¬ì{þišZÛ¹û‘OhiS>±%E€ÊÅéæ1rþ‚7ÞF´.>*Ð(,‹\>;ÆcxM!N©È¾ù”RÜõðǵNéägxI qþôQžÿÇoQ,ÚuÑÑ;ž Ò[³æç²ƒTÇ00 dKÙÖä×°Kɦ­»øøgÿŸø¹_e뎛0 cԀѹìôQq-~çužøÚŸÒ×}iÖÈ¡µ&VgÛ®[Æ\J RȦAÊ×£XÈóÜ7þœ§ÿòIô÷LIÿ[r¤×)!9¸—UkÖóž{>ˆLFQéÜ•zØTÕ0°á"BFBˆP`É”R˜¦ÅÎocÖœ8²Ÿ½¯þˆ®‹çPž‡Fωš%¥$JðúËÏðÖ«ÏcÛùÙm†Oµ¶°¢µ}tšŠ+@¢¿÷Údð^B<·ÄÞçžââ©#ÜýðÏè]wÞO´>>!“—,AB᨟òîy¼úâX³~36nÅN¡mÇoðà•;4 áÛ! øÄ˜£ yÊ£ŸÇƒRŠP(­ヌ­;näÈ7yëõéë¾Ä¥ŽÓ³¦fI)qJ%Nœ8ÄO^ü>—:Δ ¿fWQÒ`ÛΛ±¬À˜Ôm ÷u¡”wÕ™´sßéÒÛq†§þü¿òÖóßã¦÷¼_o»uÍm«¹|àç’%HãÊUXÁ N±H&äÇÏ<É'>÷E¢­Í#»b­$©ªXbìsSðÖT"éÑX=wÞû¶ßð.îû ïì}…޳'XÙ¶zÆN)%®ëpáÜIö¾ö<' XnÝ3ÛjŽÖšúxë7m{¾B 4 ÷÷,øÆú•z ‹§ŽÒyæ¯|÷¬Ù²“-7½[ßrïÇê,a‚4¯ZC}c ƒÝ—Rrîô1Þxùî{ÿG1-«lP‹ÑQo]ýgÆÐe»¥¡±™ûzŒ7ÞN:•˜RX-*AÉR©È¥ŽÓìßû §Ž$—Ë ¥˜³øƒÖšU«×ÓдbŒÄBàºéáEÓX¿rÒÃyãÇœ;²ŸuÛw³:¶X©kh¢mýº.úCîµæõ—žábÇâ M45¯¤uÕZZW­%ÞØŒiZ€žµlÝ éV¶­.K©¯BŒ\6ùÓÇ8øö«tœ9N¡Gˆ¹#Fíç¯Û´@`¬z…‹6ÙTbJ­Kükg`˜Æ({pÉD¦X·}·>òÆ‹Õ T*Ùœ=y„JdÜ´,êã¬]¿…[vÒ¶z=­«Ö”É2;˜:1$ I pòè~¿óÝ8N 9Ĩ ²zí¦ ÎQ`ç³þèåäÁš¤ab˜#uìK– k¶ì$ŽR*äË7T”ƒz>*uêÃ}zçuV­^ÏÏýÒÿFý|¦‡H‰Vоž‹ÞÿG¾ÅÐ@o¹`KÎk*‡Öšh]=Í-­ã!ÈgÒ”l{‘òCcZQP–4AÖnÙÅ wÞÏ;/þã„ú%µ])ÖöµÄêãóBŽJ£·ë"û÷¾Ìу{I%‡ÊÏÍ/1j±~Óvbuã_ä3©òtÜEÈ `hT/­%M37Þý>}øµç¯8ìÑ´lßu †aÍx6ø•P› œdß/²ïOH%«¥¿× Bn½ã>z䘖5Á&¡ÉgRsv}æM(ÕðzIüjCà à–JêÍZkâ ͬY·yÖRBFÈà{ÊÇ¡ÏJ ÓÓÕÁÛo¼Dwg‡ÿÚkœ+„àÝw¿ý$¡PxB ª5¸Åâ‚HRœ4ÄâË©E!›ÆsK“•ºì–Æê®Z½BødÈeH&‡ìï¡¿§“þ¾.Cd3)ŠvaVJagZkn|×]<øÈ'&%GùÅÔÅ0LoËZÚ¨Mf\òé½p¶<%iòŨ”š•ñàÛ¯r`ß«$‡ɦS‹…šü,ªReaC±jõ|øã„ÂÑ+KO­ˆ7¶GȧS *k*†dÅêu£[Òñ\W_Ïú7!åØî#B…in[Cs۶ݺ¥<ϤèºÈ…‡9{ømºÏŸ$ŸNùï™`sRJqÃ÷s÷#?ƒ˜`,i‚4·­aýöÝzõyÄ"2*ç J)Á ·Ýù›·ížqZò<Ö­ß̆7qrÿëW,½ÕJŒD¹ëásÏG>E´¾aZ{Œ”†ˆÅ›ˆÅ›Ø¸ëî~ôºÿâ9Žï{•c{_¦¿³ÏsG‡Rk·ìâáŸÿç„cõ~žXlþêÙÆ¡ŸüH?þßå-ÎÔˆÙ@%u~Õêõ¼ç‡Ùuó»1MëªbÒ´8ÓyžoþñïRÈe'”ÐJ)ZV­áŸù§ìÞóÒ0gU§‡õ‰·_cÿÀ¥3Çʙư¢}ŸøõÿÀºí»'ý¼%O\:©ÿæw“‹§Ž.:;a&ðïw¥Ó¼  ÑÚ¾–Ý·ÜÉî[îôk]fkÆI4Æsßÿ¯|÷ãDkͦnå‘_ø5VoÞ1§ši!›Ö'Þ~½Ï>E:1Èc¿ü¯Ùvëž+~æ’'À›Ï<©¿û¿?ÿ³¼ç;Â0MßS§Ë%¥†eG¢46­ }ÍÖoÞAûš Dbu~Ó¼Y\´È[ßüãßåìá}U§r~·Üÿ!>ðé_¦¾iż™mùLJgS V¬^‡¸R Ë|)òw¿÷pþØÚªfêPJohâþ‡ceÛ\·äÇz¤Ä …#D"1Bá(–¨öž«u`Ô7ÐÙßÃ7~ÿ?’ìí£¸ï±ÏpÏG>E ^Ð>e‚”qôÍ—ôãôÛãN¡Z,¨ôËýÐcŸfÓÖ.Ë÷òïs%ê?o÷]JÌmìûÉøî_|™h}üì¯pó=1¹× Ë)Ãuý½¯ü>{Ÿ}êš(Mº<÷}ç ïâ¡’­í jV¢…Ñõ ¼þ쓬޴Í7Þ¾à‰Q=÷e‚Œ`¸·Kí¿þ{ºÎ-ž¨·RŠH4Æž{â®û>D8[p¶”1ZV‚iMoX¨X&Èe8ñökú[ÿýwÈgvÁO¥âpÍúÍÜÿÐclÝqÓ5˜XuH‰ŒÕ!ëæìºoç ˹ Zký“ï~ƒg¾þ§(×]PE?cZJIóŠ6n}÷½ÜzǽÔÇ”J¾ZeÔ7 BaôNs,éHúxBˆ=ú˜îïá>ײ6\kT9n!¥A¬®ž¶ÕëÙ¾ëV¶ïº…ÆæÀšÍ.LYGÆê@.|#üŠßgY‚Œ\:¥_~êk|åYRƒýÀÜw© âI) …"44¯¤}õzÖnØÂêu›hji% -¸±ÒH‰ŒÆuqÄ4Çœ-d,dh¥tgû_~†£o¼ÄPo'ÊóüyƒWYçP”“†išX ±Xœ¶ÕëX·q+ík6Ò´¢•H$æFÏ^”{v®H޸ĵ:5– 2h­ur —SÞäľ×è>’\*‰ç9ÔN·½Â1a˜„"1ê›WÐܶ†–öµ4µ¶Õ1-¢¡0Ñh†iVß· ï‘4á02V†®;bT°LiÂ)õ@÷E.ž:Â…‡è:{’D÷¤} ¤4hXÑÆš-;X¿ýFÚ7m§©­H,^%ž‡ÊeQ™Úu¦p6×B #Ñëžüÿƒ2!4sþŒ%tEXtdate:create2012-03-02T14:03:56-07:00¦àåœ%tEXtdate:modify2012-03-02T14:03:56-07:00×½] IEND®B`‚txtorcon-0.14.2/docs/_static/logo.svg0000644000175000017500000006702112515516430017421 0ustar mikemike00000000000000 image/svg+xml txtorcon-0.14.2/docs/_static/haiku.css0000644000175000017500000001471512312757205017557 0ustar mikemike00000000000000/* custom stuff I put in FIXME where is it "supposed" to go? */ div.admonition-todo { border: 1px solid red; background-color: #Fdd; } div.admonition-todo p.admonition-title { margin: 0; color: red; text-transform: lowercase; } p.admonition-title { font-size: 120%; font-weight: bold; } dl.class>dt, dl.interface>dt, dl.function>dt, dl.staticmethod>dt { font-size: 150%; background-color:#ddd; } dl.method>dt { background-color: #eee; border-bottom: 2px solid #ddd; } dl.method:hover { background-color:#ffd; } /** end custom */ html { margin: 0px; padding: 0px; background: #FFF url(bg-page.png) top left repeat-x; } body { line-height: 1.5; margin: auto; padding: 0px; font-family: "DejaVu Sans", Arial, Helvetica, sans-serif; min-width: 59em; max-width: 70em; color: #333333; } div.footer { padding: 8px; font-size: 11px; text-align: center; letter-spacing: 0.5px; } /* link colors and text decoration */ a:link { font-weight: bold; text-decoration: none; color: #dc3c01; } a:visited { font-weight: bold; text-decoration: none; color: #892601; } a:hover, a:active { text-decoration: underline; color: #ff4500; } /* Some headers act as anchors, don't give them a hover effect */ h1 a:hover, a:active { text-decoration: none; color: #0c3762; } h2 a:hover, a:active { text-decoration: none; color: #0c3762; } h3 a:hover, a:active { text-decoration: none; color: #0c3762; } h4 a:hover, a:active { text-decoration: none; color: #0c3762; } a.headerlink { color: #a7ce38; padding-left: 5px; } a.headerlink:hover { color: #a7ce38; } /* basic text elements */ div.content { margin-top: 20px; margin-left: 40px; margin-right: 40px; margin-bottom: 50px; font-size: 0.9em; } /* heading and navigation */ div.header { position: relative; left: 0px; top: 0px; height: 85px; /* background: #eeeeee; */ padding: 0 40px; } div.header h1 { font-size: 1.6em; font-weight: normal; letter-spacing: 1px; color: #0c3762; border: 0; margin: 0; padding-top: 15px; } div.header h1 a { font-weight: normal; color: #0c3762; } div.header h2 { font-size: 1.3em; font-weight: normal; letter-spacing: 1px; text-transform: uppercase; color: #aaa; border: 0; margin-top: -3px; padding: 0; } div.header img.rightlogo { float: right; } div.title { font-size: 1.3em; font-weight: bold; color: #0c3762; border-bottom: dotted thin #e0e0e0; margin-bottom: 25px; } div.topnav { /* background: #e0e0e0; */ } div.topnav p { margin-top: 0; margin-left: 40px; margin-right: 40px; margin-bottom: 0px; text-align: right; font-size: 0.8em; } div.bottomnav { background: #eeeeee; } div.bottomnav p { margin-right: 40px; text-align: right; font-size: 0.8em; } a.uplink { font-weight: normal; } /* contents box */ table.index { margin: 0px 0px 30px 30px; padding: 1px; border-width: 1px; border-style: dotted; border-color: #e0e0e0; } table.index tr.heading { background-color: #e0e0e0; text-align: center; font-weight: bold; font-size: 1.1em; } table.index tr.index { background-color: #eeeeee; } table.index td { padding: 5px 20px; } table.index a:link, table.index a:visited { font-weight: normal; text-decoration: none; color: #dc3c01; } table.index a:hover, table.index a:active { text-decoration: underline; color: #ff4500; } /* Haiku User Guide styles and layout */ /* Rounded corner boxes */ /* Common declarations */ div.admonition { -webkit-border-radius: 10px; -khtml-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; border-style: dotted; border-width: thin; border-color: #dcdcdc; padding: 10px 15px 10px 15px; margin-bottom: 15px; margin-top: 15px; } div.note { padding: 10px 15px 10px 80px; background: #e4ffde url(alert_info_32.png) 15px 15px no-repeat; min-height: 42px; } div.warning { padding: 10px 15px 10px 80px; background: #fffbc6 url(alert_warning_32.png) 15px 15px no-repeat; min-height: 42px; } div.seealso { background: #e4ffde; } /* More layout and styles */ h1 { font-size: 1.3em; font-weight: bold; color: #0c3762; border-bottom: dotted thin #e0e0e0; margin-top: 30px; } h2 { font-size: 1.2em; font-weight: normal; color: #0c3762; border-bottom: dotted thin #e0e0e0; margin-top: 30px; } h3 { font-size: 1.1em; font-weight: normal; color: #0c3762; margin-top: 30px; } h4 { font-size: 1.0em; font-weight: normal; color: #0c3762; margin-top: 30px; } p { text-align: justify; } p.last { margin-bottom: 0; } ol { padding-left: 20px; } ul { padding-left: 5px; margin-top: 3px; } li { line-height: 1.3; } div.content ul > li { -moz-background-clip:border; -moz-background-inline-policy:continuous; -moz-background-origin:padding; background: transparent url(bullet_orange.png) no-repeat scroll left 0.45em; list-style-image: none; list-style-type: none; padding: 0 0 0 1.666em; margin-bottom: 3px; } td { vertical-align: top; } tt { background-color: #e2e2e2; font-size: 1.0em; font-family: monospace; } pre { border-color: #0c3762; border-style: dotted; border-width: thin; margin: 0 0 12px 0; padding: 0.8em; background-color: #f0f0f0; } hr { border-top: 1px solid #ccc; border-bottom: 0; border-right: 0; border-left: 0; margin-bottom: 10px; margin-top: 20px; } /* printer only pretty stuff */ @media print { .noprint { display: none; } /* for acronyms we want their definitions inlined at print time */ acronym[title]:after { font-size: small; content: " (" attr(title) ")"; font-style: italic; } /* and not have mozilla dotted underline */ acronym { border: none; } div.topnav, div.bottomnav, div.header, table.index { display: none; } div.content { margin: 0px; padding: 0px; } html { background: #FFF; } } .viewcode-back { font-family: "DejaVu Sans", Arial, Helvetica, sans-serif; } div.viewcode-block:target { background-color: #f4debf; border-top: 1px solid #ac9; border-bottom: 1px solid #ac9; margin: -1px -12px; padding: 0 12px; } txtorcon-0.14.2/docs/_static/logo.png0000644000175000017500000004176612515516430017416 0ustar mikemike00000000000000‰PNG  IHDR––<qâbKGDÿÿÿ ½§“ pHYs ÎÛ4tIMEÜ '"Þø< IDATxÚíwxTÕÖÆûLO Bï½7QìQ,(EEì½ë½×^¯½÷^®õꇀ^¥J“" ½‡H€ô™9ûûcï3sr2“‚iûy’™É”s޳ʻ޵¶àðÚ«õÙ\RJ†õ2ùl®ÑÄx)e6ˆåÁ 0¢Ÿ¹úæ²ýLÃf¸†_k¤uë'ø¸Yšréð>æpÀÐ//mo%õßÉéø‡!R9xÆÎ­x˜¤DƒÊu6ˆÉR’-f8,/ÑÏ\k”u½ò¥qüS£Œç8QÌ $òN((§ ïc^èõáFDÎG„1þ? –û0|ª^B–?§ŸÍ1Ú+ÌgsaÞCÊD! çœß7R„˜¦|ý¼>á þ<¥ÅRbîÛŸÍ»ô1n÷aáCJÞÏ-Wœ~ÝçÇ_Z‚ÃýÅ»à: ¬8.phÏ0y#ˆqÖý÷_éCˆ"ÆÎ1N0HR²lÛî\Á2 ) }WÜaÜH— !SPÏ“B‘·]þóêæ‡†AbiI,8â+SߤíÿÇʆQ\p¥#ùlh¯ðIöû?eôp¹Ä$ BHQ´‡žgμ€|y‚1(+›·@ÔB )x C°qµ¼â–!æ8‡åqZ"Óv Û€ŽÜ¶X˜¼P¹ ! Š à“éF¶Ë%>B¦ëkR„‚Üqñqæ<À3ü‘>ð³¸Ò|H0͈e²×þÌ ²+d³XæªÃÀ*oK<.4ùÅçg¤23ʘ#JKļ”tFC&‚ÀH„¢¬Tl¹âÄð?¥‚'†•rZ* L¥`Р:¼cç¸ôÙç)ÚúÌ·X˜Ð+d¤¸=´‘RDO¶D‚ ´XnzèêðRý‘>°‚s{\Êô­´ x@®Ã˲9R>¢“:„! !,K¥½™ôxeIÔýY¸2CìùümùÐòEë`=`/Ém€*±Ìr<¨y`}6×`hÏ0ŸÍuõ‘Ð\+„¢`@< ÓB[L%„S¿7Ÿÿò¹Êæþœ€2mqT™í´*|°€êw…‹Žäb A¢bÛÐ )‚E³åÛ¯< '#õ9ƒõ-–²,U©TáƒTU¼CÃj¹’¤”S…]«HeÍÖ­ßÜq¡ùV8TÎJIGÆg¹½b}+±¹ÀàÁªÃ1–Zí…Mk ªÂ=rÍc7™„CÙ±ŒáþìñT©Ã†VPò®P—qºiÕ•”BHQVFþ³w˜ìÜFÑ¢±áˆ­ìTB‰íVêÔÊuÈÓ N’²zÆBÑÂüø%óÑE³ÈCÕýœêO'?+¦rfò`;®‡º+4œX.d¯ÄZ`ѯòÓï>•kmÇNØ2:+Húçb¥`wò°Å:xÓÂŽ@ª,†*, ‘·].|æŸæb—hÂDiÀƒÀ¿€D`QÔ©X8hסm±„8§zÁºe¥b÷¿.0/.Ât\–ûۣ㵇Р>Р̨ƒX»ÅJŠygJäÇSª¢]¬¸êÃÌv猤  çdREºNÎÑgÚÞO8,ßa`ËÜ | å|°`wÄØô©ÊZI +Ë/¾+×íh¶€UÔî.  ‚›\´ŽX¥€w€LmÝìõCFpÌé@,? ÜiûÎrÔt£‹Ç'H¤¢ÐcÁ DI1›/?Ѽ®¬4RP¶âª2 7p3 À0$݉VCÀD$?GîÙ æ¡Ê?ÒÄËÃkÿ_WxÐ(‰C¥= à§ÿÍÄ4ã+ zÂfÈ–a\†i [Hú‹TU]¼ÁÀȽýßAúw;¸ö{€*ÀZ°xñòÊ •aÓ%¡bÂf(0üÕ§Pj7{<ŠÚÍŒä_  Û©Dæî÷œ×¡¬|€üü=Tâ EY°Œ°© )ûËøë•S–ý)Óð*Ó©õ/à[Tå | ¸ÆƒX:<ñɦèÚST@iY)>w—¡s›BmMöÕ2€Î‡%Éù¥”ðÚÀe'TÅa`íÛÕ‡Œ`(àF­P2œËQŠ ÷ߨ¬µÂ¦ÀûÀú4ˆ/>{•VmZ!5Oe,'g;?%¥¥4lÒŒ1c¾fõºutkx$!3„ËpQ7)›…9³0eX½Úb¹M@òkädÍÒ@^¡#²@} ›¥H¶#hø©(5•3°ÏÚ"؆Ôà¤ïƒ’á¸6OÖÞ­SPdb/Ë}>öe1`àñÈP!BJKƒ¸Ý.rr¶ã÷û(,,¢~ƒl6mØÄøÿ~K÷FGáq)º!ÅŸF½ä†,Ι­ÞÁ*çFNñÛ:º*ÕT@oà{ ®2  [‘Ìš!"-Xö¸+èŒÀ…d-èŠRu,Öÿçù`r…‰ÀKÚ9Õ·®Îî¿Qœ{ΩPisŦM[¢çG@q‰©põUç&È’-s#]a3L»zÝÔáÂò™š:µ¯“5)ð6Šï¼‰j^ýJs_@‚×üR˶Ã*þœ€à< ‰âï¿FÕ×ï3Æþ`±XgŸ¢ä¾‘ýiî¿÷î¿÷ Ã([¹ ,XJ³f”+ øÙ¸q ­Û4')%“Õ«VñãÔ©tkÜC¨¿5Í0Ù©ÍHò¥²bû"û{÷Ö®w€@âÂ$¡*»µý¥—oÂHvhë•PMjBT,ËzÕZ"ÈG²C›¦ÀoÚ5‡-VÕË£“ü±:#²Nÿùk<ôà͸\® R™üù„C¦ÍbäåíV.ä™§ï$/”Ãï[æ`êï…H$=Íå}ï"àI´ç†½n$O#xA}$Jq ðŠ»ÿð X€àeTy¼¶( K˜ CpBäU‡? x|ë¸ý¥–ë@µXÐŇ÷·ó6Í›5âËq¯sêiÇ!M3SE޽”,_±—Ë aÃúääl'!1ÀÂEËèÓ§Ò ‘ð †ygÜ»ôl|4>w 8Ó´«×%[犲°ª-Ò x Aíx† بéh†’6Ï&Ë€^”‘È\$é@=D¥«ŽÅrjë[éxn ’ ©ÀeÀ”ÇŒñÊâP¶Xu€—ujÝ­\wí¦þ<šcïa3&µ „ 'g¦ (†Q‹†Áõ×]HVý ÆÎSFêqyY”3‹Qó^¤ tW”EzZÊ ÊC:²ºÊÚ¡º¡‡¢:´¯@•Ÿ#ø\;M£–9¯.(a›r­gïëÌQþU”Äf±úéXåx[T!êÕË`ÒĹâ²a¤¦&#MEÇ, ÁþóÇÓ‡´´e±ü¬]»‘íšãó)$§¥‘–’È'ŸÃïI QZK’ï–}Ê”_Q,P\÷«úSÅ+õ´Nf#ÙM"p´¶`_¢úË€>lE²AkTOPM( ‡Ž° Ù £¶ˆkh‹ª>η9ãZu‡ °R4ñø®Ž[$ ’ÄÕWŸÇ”)ŸÒ¨q„Ž…Ô-ö …‚!ž|ú-n¼a¤¶^Ûñù|”–•QZZFÆõÕ¹2ÃôìÝï¿ûٿϦYfÆ/z%[çªÑî½Q…”zUdxp ‚õÀF  ½¦C¦ÿ6=)%À,”¤/ËáOj ,'%¦P›«”á×c©fæjU_ ë8`ª¯/’ñrr>xÿi®¸r„C‘x*î±—áö2fô×”””rÚ§€ k`yHJJ`ñâåtíÞ!"ò8ðX^zí=æ­›J^q®ºúoFµÔû‰Ý‰/Í8Õ“¸ÑÕ”ª.NÓ§½ K‘äjëå¥êí0«K]„u ÑÁn`;' ´^óP]IîÚ°^û{Œõ€Îf,-¥0„ï½óÿýæ]z÷ê á`9&=n¶.àç¾_à¢‹ÎÆ®Ü ‡MZ¶lÂÊ•ëP[œ(ˇáæö;ž¤¤¤Œ° +¶ýM¢*ôš¨J­®S< ‰j¨}Õ<± ¸Aó^‚ÅHžF²™Ú-d›ÚþŸ мêI¨ZãiÄVJˆƒXõõXÕ6­›3eòÇ\zùy•Ê_bÛfƒ©?ÿHAA½ûaÓñ°‹â’uüÝn~[°”öíO`Ô§_[ä£Ò4³Y‰½]ÍwôBêŽÂ‘úÕ³´Ë¿ ØA ‚W‘.ÃÚ^½ŒÐVLýû•~ï¢Ê2gÇ ¬þúêi÷óŽ8‹Ÿ~ü„cï •Ui¡*x‹P˜'ž|‹Ç¾ (­Àm†Àëõ‚æÉ'^ç„“.dÙ²ÕÊÝݧ!•Ïh¨î i÷ ‚k•ü ¥¯úUŠù5td: ø É$ŵ¼„Qƒž.ºE û(ªJJg ÍEZZŠxþÙ{¸ûÞa蘨& ’R" @páEÿàœÁ8æèÞå^#gËvü±|5>þº²RnTËÂÄ—#˽žã,Ml=° ‰’wF‘¿E)Á:SFs‘¤ÙÕš¼n?,CÎÀÔ_ÆâOðWÉMÅæª\„Ê‚|óí >÷:’’™þËêÖ¯‡ ‡Õë Á¿ûG{¾Š§P“d":´º1’ý„ZC)Ûê«&q™ýÔ¤k~~‚ÐU¾Z€jÿÊ:QF3tŒÔ€š+$D%îÜD}Z#X¤ C'9šPEÁþíYaTùûõsÍÕçóÉÇÏ«! áêƒJêñÆÂ`ÕŠµœ|ê% v#ÿ~ðf¾ÿö]êd¤#C!„Ë——뮿Ÿ³Ï¹†;ó•~üàz‡qßÛµ¸ 5„;q/¹¦0ª\t'pUäøœ¢9.+Á¹B»(eUFëÏ^›gT¢öÖFîY©Ý¡ŠM³å舿Ãb]‚ Ù‹!âÊ+†óæ[ÏaˆPEBe€²b/<òèË >çZ22ÓY0ÿúÑ C?&„`ó¦­tï1ˆ‰?LÅ4MU–yG³fá½8èΫ] 4ñ;t.Jõ ªac¹G%;îˆ`&"¥9+F°Çiv&øI­ñŠe…jb± £Q@¡ã½óQŠW Gkÿb_«®Ž´S ×\}o¾õ2TTí ÝŠ¥„;Àì_ç1tøMŒó wßy-ø,‰I Ê•¸Ü|üñ— :ë*rr¶)2ãÍOí­•Š¬oÑ“"PÒº\*Þ'^–i¢äÇ fhm‰p}í´küØt'D"ó‘$"hÐÕ–õÿ\`\¹ß®Gêuň+D“û Xýïˆv$GÝ“OêÏG<Ë5Êü„àòpÏÝOp͵÷‘^'•ÿ~ýç]p&†U–q¹().aȰëyêé·)..QÜÔKú+ x÷X „Ñ; nRCŠÊö¨ÖŠY(zeqWeôEöCàG°P“0X®íÿ€ö˜4d9’MÚ:bƪ€…vp?é¬P=c†Ž…ƒ1­Iù­\ä¾–5”çs¢{F¾N½z,œÿ .wT¥Y+—·‹¡C¯ã½ÿ|Îe—åë¯ß&«^F¹€劵uÌpf̘¯ giÂ3¥÷óg-ÖÀ6h™Ñú)È-ÜŠÌ7•ãêH´­´&À²€ohÒ¡#‚HB$ê€:¤0^“ËÙ‰Áb$­ô¦Â¢®Ñ¥!:-òèt\ Ç+Ø7¦Š5ÎRþ•Àj¬Ãâ{A[S©¼II LŸú™Y™ÕÔ••róßï~äÔÓ.cë¶\F}ü·ýó&0Kµ•2‡Â¼þæ(Ÿ{Û·ïTNøâá8Vª¶€õ“ŠFê&eÓ4­IõÙU’GiQ±²fI¶bÉÞ®–ô¸?‚¶p$* Yˆª÷m:QBó´klÇbI[Âð507rÏͨùbX(k[âígÓÎkýUÀ:N×»”ëޤ5‚©Ñ'<öè?8sЉզTÖçáÙ§ßä’Koçè£{1éÿ> k×H-.ƒ¢‚bŸ{/½ò¡PX%êÏê"I¸¾§¶€õ£Šxv—äÓ0­9MÓ[ѬN;ŠÊö_¼CE+€#¨¸‡Xu€eÙ‹Dý‰Nªêw J¸÷=JÙ“F,G² Aû8àrŸ Y¹ç#mý\”ߊزNA°bnîYÛÀr¡tÕã€t’µ•è‡ày”¤ hß®%o¾þ^¯§ÚîOÁ 7>À£½Æ=w]˻dß+ÀöøÂ…еû -þC9áÁÚ'9b›¿ XRÇ4³!\dCÞ*¥µ¤qzKÚÔíBQpÛ ¶Àz©ˆ‚£lö`oJD–d°•NÊ"YcŽ.Ã|­CÿVäb°I[~Êz{ÉÆÈ=¯¡Ä‡nP쀲6ô´vµv“µïÅX«1Vc¼«Íº"¹J_%ß鯩×[o>B×î]f¨jqžqÁ…·ñÁ‡ãøøÃg¸å¶+‘!-qq¹0ÃayôU†Ÿ³êl¬3¾«ˆø—ûX-5K· ÌÜ0Ë·/Àe¸i™ÑŽõ{“äKaã®Õ„rƒª±‰>ræ^Ö¥†NW` ’­(}U ·Ñ(I_gJu­1€ ªl’- ’—5ç‰GÅÚŠ¸”ò»ÆFXøÚ*饭”⦆Ç"ðèûj Яo7¦ÏœVwK•îÏÍÉ'dÒäiÌšñ9}úvÓŒ¸Æ¹¹œtòÅÌÿm‰ú£#PЦôegù"Öã5-ÁĪ Ô¶˜ïjúh’ÞŠáÝ®Ãçö³µ`3£ç½ÂžÒ|ui_¢™ÿ¢jÔc}Fëù%À×H>œ×šXù¥${^Ó’örÊíŽq—f® Û«Z.ξiz1Ñ-‰KV˲nÚb¹QäQ@"IHn@p$"rà¡&´èß?õ?5V{”nêjMÉ .Ó[ØÇ'Ú%$ÓQ[¨é<üЭ$%%TA€z9ñ¤ø|>>úðüy;ó¹á¦‡¸ãÎ' ‡Ãªxü(jܲ8,òþ,ËMõ×ùÚ|()*â÷-sHð$Ñ.«ƒ@ú‡ IDATmêu&¯x;; ·*§µ¥«JÊX–Ål†Úaq’ר@[®"°,kT∭bYªXèk€¶=0\ œgRŠu=šbÛ¡²¦ÙÜqçÍ +˜îòðÒ‹ïòλcùàý§éÒ­ +W¬æˆ#‡ðó/z6Õ(5x[Èøgè},;sÞTÓ!¸«Œ?¶ýÆŽ¢­tË>’öY=p»Ü¬Ùù‡Š»f¢H^ÿ^ËbÕSt¦^‚d%B»ÆÓt'×[•Úne¶[(ËЬÊõÝ¥]_€ºH®CÐ#² Zü*¹ÕÀ¥ßþÜsNeР“‘f0fŒ%¥dgîN®¼ênú÷ïÅÞÁ[oü‡SN½„Ý» T@{ªYÔ¬EÞi_Ëz¾_G<Uè¼uÏ6íZM“ôÖ´ÏêI’/•õy+凳ÔA»4YC`9ï®§J¼—‚R÷ÿ¡³Â2G|åU,Õ(5Vk” ìb@r j¸DC*NvËÐÞû»èÝ·Ü| ]º¶CH3®ÿåDFšÀgc^檫nç™gß!R {A§ïŵLhþÀ²¯~(-í¯°³h;Ë·/$9FïÆÇÑ0­›v­¡p×r»4À䟖5­´½ÎwàAµrdir´ÈQ¶)‹Sl®œ*Šsÿ` *¼KôI•”×FÇÓN»Pr‘g¢wÏ›ý%Ý{up8.Ú±ã)øý>–/_CAf oÔ„†Y¡Yq_¤8"ê¾ÞFT¹~'¸„‹ã[ŸM¿f'S,bÜ·Y½C«'ë(7Þg—Õønزöü9ƒ«P-_[bÔMG\UeµÉ¾R4ÆÙHԥ䚈áÖ|ÎÈj×®%˜fPù˜þËl–,Yɼy¿+P5Ôøfj®î4uÒ°›¿¾sÒ$t…îÇŠß½Ú±ç–a&-ÿ‚ÿ.…Žl6ÃÒdþ +µ±Á¯OsðÆK¶DµP ¤rýEµŠÆÖê†ÒM© µOÐY_Fœ¬J\¡ ¥Œ^½û‘Çî³,¨¼,_¶œáÜÂŽZzy&JZÖÉ‘õU×uyõ þ7ª•©½æuBµì ](uÁ"]%}Ÿ¨:]îÅÅ ³F?°rv¯cÕŽßÉL¬ÏÊÜÅÑý·ë ¾©æÈÂ5p…±2Õö@;«‘쎌—l€RI˜{0 ¢³ú1€tE”Ó.9Í~e®Ð«!ú½Î¬F†öDw‹³š4iç½^ènTÛÓ.¥ºæÝ^Þ‰’”ú^ªO^m¸B©Ýý‹À ¿vÔùöƵJ”¼æ åŒ2ú„ò\×!8I_€{{¬¤-c}Éo‘×_¯/“MŽ+fË—Ó^©K3~€»u9àÏÎ(ˆvv–¢:æíÙ p{yâÉ78yÀÅ T^TÊHöN*ëýï$Z§ÿPÇ'/è(\C7ieº»t‚~›Žu–Øžs/UoäTK(P¡@–¾F\ç'ù];\ÁkúóÔ†NÅnG0ðG&/ÏBµÆTÜåµÒ—šªi~(C2•Z3Ÿœ’Tñh›!Ž?®/:µ‰Þ7USqµµZ¡š¥¬BÍV¾Uóÿ^ÁS 5 ^ÓLÿ½š·¶¯þ(eEðO»€Ê¹]ô>w€3;]„Û(÷!›j箜VOjgX úøîB…D{O_öª9âÈБбÀXB~Ò®ˆòJçš.ÛVáPŒLÐ4éÓ»+“'~H‡ö­”)S[`-Û¥A¤?˹çžK§NÔÁ›¯Ýî©: Ž—²j–ÿ,”†c½¢F®½öZ}ôQ\.—Þ°ZøÌní;U‰‡ßàô’•ÔØùÌ$ÀÇíâÿŠîÐXƒÙЀš®ßɺ+Ý„Óþ‘†£„'yä÷ YhûÓš‚Ëv•–VDŠ¥ú”@Ø49þ¸¾$''*Û9P[„?kÞM}mߨ~ùD•˜ŽB‰ûþŒ *ÖnúcuWfb}Žl>€²P1¹…›U3ˆ}5!:è©¶²[C_Ô¼ˆÄDh â|}4R‰Ý¬*ª¢^G Ž]K‚ב|ª_¦¦Ì²Íb•••Uò\IIq _t.£G½HfÝt*^®oÒŸ8XBŸ°ó®ŸŸÏ Aƒ8úè£5j¿üò Ï=÷uëÖUÌÍDÅvVf têÔ‰ï¾ûŽ~ø‡~˜ŒŒ üq–,Y¢2¸[öÒYRæ-:üAÝݼN{Úgõ l†) *­¼S¿ÖèO›XÖ=Wgã_F,Ðu4¹Õeå×7oŒxKT,¡“ç–Àw„Lž@FxuÎ@ôÇ]» â"Óå2ðz½ìØ‘GÏyçGÉή§ñ$J^,káj¼UŠÉ“'3vìX„´nÝš[o½•wÞy'öwÓôÄqÇÇ©§žJff¦â27näPÞ§¿«ÜKà/Dµ‚®CtÎîK“ôVz3N‰@ +¾|kjof—V#y©%6A˜¼ªiÔ¨|úr°â-£:Ëúº.;? ”± ÁSH–WÓz™º®eŃ%¥6BÊ,$%’••ÉÂEËðû}deeòÜÓwsÂñýT9Q;饔oDªéjœ¨~ü÷¿ÿÍ®]»¢á ×M·c…¿ßqßC†h>¤ª@\ÓÈ­!Œßk¶½¼ItÉîG@=¤4ÂÀãö’àMÂ0bhg:Ô°,×÷=ð‚„®<¥­•Dv0Ù7Û4bÅYî*®§g€_€OØL]^Ñ1ÇéULYX«V¬¤eëffúiÚ4›3æ“Z§ÙÙ*¹æêóiß¾%¯¾ö±Õ¥Úê\ªIϽ1õ7ÿ¿ÿþ;Ï>û,ƒ Âív³råÊèa'¤5qâD Ô›æää0{öluíž¿ñ§µÜsDæ;d&5 ufg<.RJ áR ò$“¨CÑ®‚ŠJî¶q¯ÕêSŨ®ðß#÷LÓQ^®UØv1~6lßJ8óXok¿ÍÕEO(¡+ãä!¢›%âUÛë—¿kæÌù´lÝ2æeÖ·OW>5Ò¢BêÕËÀçóà÷ûü´lÞ„'ž~“mÛv(û¹ZÇ45å‹L—Ü¢‚ä‡~˜‡~¸üsœKsq .dáÂ…Ž­-VI AU€jöX îÈNiJËÌŽêÌhÒØçö“èM!Õ_‡TË·/°¤ÊZ¹ÿ°„¦<ÁR÷šÚB}d½.hÆù9í[!ïZ·$ÝX‹`©ÎLêÄ`ã­¬jJå ]ÞàÁg MÇüOnCðÖ;c8òÈ´ïØËC Áßç#à£{·Ž,]º’¼¼]*\¢Ë ¨™|7ŒÎYIÓyŽu«¯/!¿>¼[P]Ô­·¦ îJÊ\±ÈպηՖÙfí"nÖårã÷$ìO'=IŠ/¿ÛÏ‚Í3É-̉¾ÞiÚb…ãļñJ:–ãúxI5Ûýu”ˆÜk»Äìú,»ŒÆ.ø Rqº_L‹e/X1 \…ºú6x‰5xÉH}ˆîk€í/[ë+˜:u„‹*j±L“ž}z“ššÄçŸÏÀÓOÁë “b$áq»ñù½¤¦¦š–LÃì,¶çî¤lZPME¿G³é5‰qB¨uKßš-•d‹¯.¬¤4SèÖqÌ«ÊýøÜ~Ýc˜Š4ÃBà2¼< $ùSIõeèKÆçöS*!¯h[ù×;j/㺰NˆGÎóRí óˆÎñK”`*‰ª˜å#εm¿Æ}eßj¾w3EzCíÏcdË:X|ÛÎ|æÎ[ƒmPßñÂg3}Æ<öìÚ‰·ÛEbb€¬z™4oÖˆ¢ÂbúÑ{﹞:uR•2õ6¬ùM5£C(á`šã–BÅb»°]éö›¬¦Ë‘ºœôŒU‚'™Ž&É—  áÂíö‘èK&-!“Œ„,Rü©øÝ ¸ 7ÅÁ"¶F7í$KΚÆuë›¶Y|ßj/ßvi9Ï·ªbMÄ8;tâj´ŒJ‡\aT£Ò)(Í–2­/#ÙItÇvk®“–ÔæïÚÃÔ©s+~g¡šp¯¿öB–ý±šez¶Ã0ðû½¤¥¥`¸ 2ë¤sÂñGpÿ½7г§ÞžðqÔxÆ-ì?3 -²1_çÕª»¤4¥gã£ñ¸ü «BÅSɾ4ÒuIÔ%É›Œ×À% òW6mæ©[ ."ËOŽ4§æÝÌOÅ–¯  PEú«K'D%ÓÅ\Õ¸ÞbõB€Ÿô)=‰\Ó‘4Õ –]ƒš}”ƒ 2·ÛUÞ%JIݬLÆ›ˆ)%'žÐ¯ÜÌÃà?ÿƒa.¾è<^7mÛ´Äív±d©îÉ›¨ƒéúqÜľTzPóïnA¼Vu;Ó4½5B¨kÙm¸ð»Hò§‘È$Í_‡o—7ú—‡ ¿ݾM5®ä³Ø#å ª5î{$A„þ4ë³bP~¾•³åËÞùl·TN™r̆ £ŠC'c¼¹½3¶H#°•b/ ùN'1^Áè5eÊ 6nÚŠ0Œ ;C`šÜxÃHÞxëSB¡P9y š7kÄo¿-%+;‹ÍÓ®] .9˜›n¸ˆ„„€ïp¢3}÷,µ±¬©R©Z›ÛpÓ5»R[è‡ÃCÀDJ ƒ: uI Ô!àIÄmXCR$†0ØQ¸•œÝ뢯®‡ªÞ_è‹ù$óIXÛ­U2â‰,Ktœ×bSÛ­•õ¸³K'¦TÙµרt€Î¥3‹ÿÓ }+–è{ÚêvïiÑ fõêõ\páùàÈ¥”ÔIOå•W?¢~V]zôîb¸˜;g“&Oã¦F’ššŒßçÃíqÑ A=Ú·kÅÆM[ÉÍÝ™OÅÚЛûÈbY¢’÷Q£5$$ûÒèÜ /Éþ4$a\ÂÀëòð&‘È -A²/ ŸÛËp#H©Ž‰×ãç«Åï—Ï[ëè6žæÝЧ/Dº w¡Ô ã(ßíT±Tí€2+ÉG÷ X"°°º=Ú!åÇ‘ƒàWT`®°bÅZNx ›d—›%„ =³Ë–®ä…ßçî;¯ŠzÃ`Ýšõ|5áúÑŽÛàõ¸ üx}^’“èÒ¹-EEŬZ½^ ð¾Au׉qMÕ6°¬då6"J‰ºIÙtjÐÛ Rb.¼n?I¾Ò¤2T<åöa—žG!B…; ¶0iÅåã«‘ÚbÅÓtºt!æ‹•°¥•ø#†ë³÷:Ý^¬xÊÙ ZkÀ¢’kÅnÁæ£NmJHd:’>À²(N-þƒ+.R‘z‚Ö-›òÒ+’””À‘ý{«â«‰€¶f™œzÚ±Bàñx|ü~ÜÚ·"==…%KVÜRn±9ªaSüEÀr¡šÔïPÉ»@Ð,½-mêu‰Ä‰.Ãßí'ÉŸJZ “T½I¸]^ ¡*"zücÍ›6 [n`–ß‘¢mÛ|òÉWLùq&]r>2\Fvƒº¼ûîXfÏYÄ ÇаIÃH¯¢"T•kTl°?þXÍ‚…ËhѼ1y[v+p•¢†ÝŠË­ì‡Píezunп'Aé„ Ÿ;@’?•t¿Š§¼Éx] a” ÐDqQ°€Qó^¦8XX¾|3Ôfe¨‰î/"YùïBdÓAù¹¡±FųTÎY¢{MØÔ°b 6´0ëûRƒ«™u¨fÌüŽ¢^½Œr/˜˜œLFTž{á=š5É¢{ÏN[·í`ÆŒù̘9Ÿë®»!£þ,`ºÝ.|>‰ 6mÚÊ“¦qã ‘—·[@Z€’>°1y²ßpµ–Þ¬· m½®HiânüžDRüŠIO ¤ð&â Ãeå'¨s)%É+¾be®­ì•†j³ƒÿ}à$…]ã»SWBdœÒŒðŒ5æÑYž‘Ô‚„ð¯*‚Äcì­>™ºV%±°°ˆùó—0äÜSõŽ_Ú%šaºõèͬ³=æÎv©)Édf¤3fÌ7ääl£eólºtiWnchÜ¥¶âMLLàíwÆÒ¬Y#®½æòòö°~Ãf̦Òkt!ºci<@™¨A´·™ül­úɨ›˜ËåVñT éL’ýiøÜ ¸…CÂv×g)þdË&-ÿ¢|bðŒÖ“¸ÙÊH~@ ¨ŽåÇ4¸±g°W6æ±´º,úþ,Q‰[´Ôð¿Ú-ׯ[ÄŒY¿1rÄY.—í…BœvÚ±¼òÚǬ\±–s‡žCƒ†u™5ë7–,]ÉïKVrÉEƒ+Œõ–:xBРQ#ÆŒ™@NÎ6Î>ˆZR?+“%KWRšW¦"@7ÑÖ­Xª€ç·Ô¯M›4¤°°Xmò´¯ßƒO ^%ÊK d’äMÁç²ø){MÔY€¯ÛÇâœÙŒ[ôn´aBè,°½-xXñ ïi×Wìp}¦My Pö¬¯ŒØùj¥^ñW–m%{‚í–ë'=$¬[·‰_]Ȱ¡q¹¢µD¿ßG‹¹ïhP?^½zrÖ cyâé·Ø¾}'áp˜“¶)6vë…áÂ%$òç@ã&Ùdf¤Ó³G'.\Æî¼˜­µR';¤?!T;šÞø cÇÖtéÚ–¥KW!¥$àI¢uÝΤèz_ª_‘ž—/ìÊöçB°.o9_,|‹iCõù¨Q©Žç"lÔ”ænø™/¿WTÃPmo‹€G,޼À§¨1¶ÒŒ¬¢ÖWREÖ'ÿ*PÁ¾ÝÚî\:zð蘫6úÖ¾\4oÖˆ&~@‹æ#Wþ®=4oq,={vbÒäQÈP#ÆÌ™¿Ñ©S~þqéé©1w›õëŽ8ro½ñW^}%……äçðóϳÈÝ‘O0äÖã$åÜ?1`iñ_N$?7§Ÿq†€Üí;ÉÈLgâÓ˜>c#FœÅÉ'ÅÜy‹hÕ² +V¬£´¬Lu éÕ¶^7Nhu&†0l ªh%›îBƒIËÇñýÒÑl¶ë«Q|iÀÝHD,ú8ùY®/#žª*@ExÂ>R©ý›Kbï€ÔõÅu½KBJÉW&Ézõû2vôd(̹C1áË7yì‰7xÿýÏIIIf̘—yáÙLŸ1Ÿ3κ’¼»ÊÌá0×ßxÚ·ä?Œcæô9šïr‘ž–Ê7ßL&))³Ï<‰;w±gO‰‰‰lÝ–KaaqÄ< l~°–~ª¼01úUWïXÆSSnaÚšï( Ú"Û£$˛܆ds¤êqS•ÆQ$ØE–ÅTÔ£;Çfÿ¥AúþcÅ‹¹¬ÁV¼ЀïÚ¾²Ÿ¾èÛ§+·Þr)ƒÀŒó~þÍ\qÙ|àfIõùèý÷¹ñ–ãózyù¥û9÷œ¸¼¾È.ö[6o£K÷ÓIIIæS>fOA+W®åŒ³®æ¢‘g3lÈi :ë*Z4o̦Í[)--ß¼wV§KèÜ ¯­Þ§‚tËb¹ Ee{Xµc ó7NeÕŽ%å¿u¢¦NF#9Ë´*a å÷ tžÎxÊœ‡þN@ýËi¹bÅ –®ë&” i•]Îúu#/þ'ÝzœÁÌY¿1îvæí¢K÷AüüãDF^r KÏ‘Gvgøù7Ó»ï9|5þ{„; „‹ú êò寳iÓúõÆ”)3yö¹÷¸âòáÜr›M$ QT=C×ì~6×Õáu©þÿik¾çYóÅÂw*‚ªŸÌû¢ô[QP×Nq­ T¦#-±Y*{<¯½4‘ØqŸÚºÚ9wF‰€ZjXX§ú}àS¤–¹l@Mƒ_LtVŽJpjÑ1U¼’äþ¨ýXvpa—Û.¯ \níoÑLm¦}«®D.F5?åÔҧ뤳ºþú]ý¶b‹½µþv`UÄõÍB•”-êÞni‚ŽŒ8V€^æp}ûUgÇLѦgÖ«c†\.ÜÏz9ݤÔ'p<ª Ô–M¦"醠n ì‚¡íßuH–DÚ-¦h˜–9Â~g,Uä}"> ¬š­2Y XK&"ñ#è\Ecí6x ÉÎȰßWQÓ†ÝKrd}U òœ,º<ÐOÆÁ,»âAVr[šãu"e¤0CsNGÇ—5ú.`ldîÔ&”ðeU\¬¬Y4Hy »; œ#̓TjV¸·ßÏEù¶}Ó¡þ¨¾=5‡èB}¢?ŽüýFÔ´á|*Ž2mÈ^š)ŽCxî÷äÃÀªùwµSvÍ—@M zY“önÓQ“JmÞ–½€lŸêâ¤ì ’;¨vW/¸wºK«‚Xˆšü,ˆn¿ô(ªc¦ŒŠs§ªKx:ë}ò`Õ¡f±ìßÙi½ì‚BJãÓdÀæ2…-Otî5ã䥜„§y(êP–\"¸|š½²ïlØ‚öx*§Ìå±Röå>DåTR9]£Çv¿9k«µXƒaʬï0°*W,BÕž±Ùã©X+舣‚‡ªë; ¬Ê­—“PuË>7ÝNˆÆ²R‡,¨«"¸bí´&ª˜ˆ7ß<Ô]ßáà½æY£}ƒm§uZ(óPÔa`U\öÌÑ.‘ŽWƒä0°«ªãâܼ×y¬âI/àÿ˜^þzZ& ÂIEND®B`‚txtorcon-0.14.2/docs/index.rst0000644000175000017500000001461512627744056016167 0ustar mikemike00000000000000.. txtorcon documentation master file, created by sphinx-quickstart on Thu Jan 26 13:04:28 2012. txtorcon ======== txtorcon is a `Twisted `_-based `Python `_ asynchronous controller library for `Tor `_, following `control-spec `_. This would be of interest to anyone wishing to write event-based software in Python that talks to (and/or launches) a Tor program. You get real-time access to all state in Tor (circuits, streams, logging, hidden-services) and utilities to launch or connect to running Tor instances (including Tor Browser Bundle). There is a `Walkthrough `_ and `HOWTOs `_. The main code is around 2300 lines according to ohcount, or about 5600 lines including tests. With txtorcon installed, you can use ``"onion:"`` port/endpoint strings with **any endpoint-aware Twisted program**. For example, to use Twisted Web to serve your ``~/public_html`` as a hidden service (``-n`` *means don't daemonize and log to stdout*): .. code-block:: shell-session $ twistd -n web --port "onion:80" --path ~/public_html 2014-05-30 21:40:23-0600 [-] Log opened. #...truncated 2014-05-30 21:41:16-0600 [TorControlProtocol,client] Tor launching: 90% Establishing a Tor circuit 2014-05-30 21:41:17-0600 [TorControlProtocol,client] Tor launching: 100% Done 2014-05-30 21:41:17-0600 [TorControlProtocol,client] Site starting on 46197 2014-05-30 21:41:17-0600 [TorControlProtocol,client] Starting factory 2014-05-30 21:41:17-0600 [TorControlProtocol,client] Set up hidden service "2vrrgqtpiaildmsm.onion" on port 80 There's a `complete demonstration `_ at asciinema.org. Some (other) features and motivating examples: - :class:`txtorcon.TorControlProtocol` implements the control-spec protocol (only) - see :ref:`monitor.py` which listens for events (SETEVENT ones) - :class:`txtorcon.TorState` tracks state for you: all Routers, Streams and Circuits, with listeners - see :ref:`stream_circuit_logger.py` which logs all stream and circuit activity - :class:`txtorcon.TorConfig` tracks and allows updating of config with attribute-style acccess (including hidden services): - :samp:`print config.ORPort` - :samp:`config.HiddenServices.append(HiddenService(config, '/hidden/service/dir', ['80 127.0.0.1:1234']))` - :samp:`config.SocksPort = 9052` - see :ref:`dump_config.py` - see also :ref:`launch_tor_with_hiddenservice.py` - helpers to launch new slave Tor instances - use :class:`txtorcon.TCPHiddenServiceEndpoint` and :api:`twisted.internet.endpoints.serverFromString ` if you can - uses TAKEOWNERSHIP and __OwningControllerProcess (killing connection causes Tor to exit) - see :ref:`launch_tor.py` - see :ref:`launch_tor_with_hiddenservice.py` - :class:`txtorcon.TCPHiddenServiceEndpoint` to simplify hidden service listening into Twisteds endpoint paradigm. - see :ref:`launch_tor_endpoint.py` A slight change to the Echo Server example on the front page of `Twisted's Web site `_ can make it appear as a hidden service: .. code-block:: python from __future__ import print_function from twisted.internet import protocol, reactor, endpoints class Echo(protocol.Protocol): def dataReceived(self, data): self.transport.write(data) class EchoFactory(protocol.Factory): def buildProtocol(self, addr): return Echo() endpoints.serverFromString(reactor, "onion:1234").listen(EchoFactory()).addCallback(lambda x: print(x.getHost())) reactor.run() This is just a one-line change. Note there isn't even an "import txtorcon" (although it does need to be installed so that Twisted finds the ``IPlugin`` that does the parsing). This documentation was generated |today|. .. image:: https://travis-ci.org/meejah/txtorcon.png?branch=master :target: https://www.travis-ci.org/meejah/txtorcon .. image:: https://coveralls.io/repos/meejah/txtorcon/badge.png :target: https://coveralls.io/r/meejah/txtorcon .. image:: https://pypip.in/d/txtorcon/badge.png :target: https://pypi.python.org/pypi/txtorcon Getting txtorcon: ----------------- The canonical URI is http://timaq4ygg2iegci7.onion Code available at https://github.com/meejah/txtorcon - meejah@meejah.ca (public key: :download:`meejah.asc <../meejah.asc>`) - ``git clone git://github.com/meejah/txtorcon.git`` - ``pip install txtorcon`` - Watch an `asciinema demo `_ for an overview. If you're using Debian, txtorcon is now in testing (jessie) and `wheezy-backports `_ thanks to Lunar:: echo "deb http://ftp.ca.debian.org/debian/ wheezy-backports main" >> /etc/apt/sources.list apt-get update apt-get install python-txtorcon It also `appears txtorcon is in Gentoo `_ but I don't use Gentoo (if anyone has a shell-snippet that installs it, send a pull-request). **Installing the wheel files** requires a recent pip and setuptools. At least on Debian, it is important to upgrade setuptools *before* pip. This procedure appears to work fine:: virtualenv foo . foo/bin/activate pip install --upgrade setuptools pip install --upgrade pip pip install path/to/txtorcon-0.9.0-py27-none-any.whl Known Users: ------------ - txtorcon received a brief mention `at 29C3 `_ starting at 12:20 (or via `youtube `_). - `carml `_ command-line utilities for Tor - `APAF `_ anonymous Python application framework - `OONI `_ the Open Observatory of Network Interference - `exitaddr `_ scan Tor exit addresses Official Releases: ------------------ .. toctree:: :maxdepth: 2 releases Documentation ------------- .. toctree:: :maxdepth: 2 introduction howtos walkthrough README examples API Docs: --------- .. toctree:: :maxdepth: 3 txtorcon Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` txtorcon-0.14.2/docs/txtorcon-util.rst0000644000175000017500000000052112312757205017671 0ustar mikemike00000000000000:mod:`txtorcon.util` Module =========================== util.NetLocation ---------------- .. autoclass:: txtorcon.util.NetLocation util.process_from_address ------------------------- .. automethod:: txtorcon.util.process_from_address util.delete_file_or_tree ------------------------ .. automethod:: txtorcon.util.delete_file_or_tree txtorcon-0.14.2/docs/Makefile0000644000175000017500000001075512515516430015754 0ustar mikemike00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/txtor.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/txtor.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/txtor" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/txtor" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." txtorcon-0.14.2/docs/releases.rst0000644000175000017500000004175612627745341016667 0ustar mikemike00000000000000Releases ======== There isn't a "release schedule" in any sense. If there is something in master your project depends upon, let me know and I'll do a release. Starting with v0.8.0 versions are following `semantic versioning `_. unreleased ---------- `git master `_ *will likely become v0.15.0* v0.14.1 ------- *October 25, 2015* * `txtorcon-0.14.1.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * subtle bug with ``.is_built`` on Circuit; changing the API (but with backwards-compatibility until 0.15.0 at least) v0.14.2 ------- *December 2, 2015* * `txtorcon-0.14.2.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * compatibility for Twisted 15.5.0 (released on 0.14.x for `OONI `_) v0.14.0 ------- *September 26, 2015* * `txtorcon-0.14.0.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * :class:`txtorcon.interface.IStreamAttacher` handling was missing ``None`` and ``DO_NOT_ATTACH`` cases if a Deferred was returned. * add ``.is_built`` Deferred to :class:`txtorcon.Circuit` that gets `callback()`d when the circuit becomes BUILT * `david415 `_ ported his ``tor:`` endpoint parser so now both client and server endpoints are supported. This means **any** Twisted program using endpoints can use Tor as a client. For example, to connect to txtorcon's Web site: ``ep = clientFromString("tor:timaq4ygg2iegci7.onion:80")``. (In the future, I'd like to automatically launch Tor if required, too). * Python3 fixes from `isis `_ (note: needs Twisted 15.4.0+) v0.13.0 ------- *May 10, 2015* * `txtorcon-0.13.0.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * support ``basic`` and ``stealth`` hidden service authorization, and parse ``client_keys`` files. * 2x speedup for TorState parsing (mostly by lazy-parsing timestamps) * can now parse ~75000 microdescriptors/second per core of 3.4GHz Xeon E3 * ``launch_tor`` now doesn't use a temporary ``torrc`` (command-line options instead) * tons of pep8 cleanups * several improvements to hidden-service configuration from `sambuddhabasu1`_. * populated valid signals from ``GETINFO signals/names`` from `sambuddhabasu1`_. .. _sambuddhabasu1: https://github.com/sammyshj v0.12.0 ------- *February 3, 2015* * `txtorcon-0.12.0.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * doc, code and import cleanups from `Kali Kaneko `_ * HiddenServiceDirGroupReadable support * Issue #80: honour ``ControlPort 0`` in incoming TorConfig instance. The caller owns both pieces: you have to figure out when it's bootstraped, and are responsible for killing it off. * Issue #88: clarify documentation and fix appending to some config lists * If GeoIP data isn't loaded in Tor, it sends protocol errors; if txtorcon also hasn't got GeoIP data, the queries for country-code fail; this error is now ignored. * **100% unit-test coverage!** (line coverage) * PyPy support (well, at least all tests pass) * TCP4HiddenServiceEndpoint now waits for descriptor upload before the ``listen()`` call does its callback (this means when using ``onion:`` endpoint strings, or any of the :doc:`endpoints APIs ` your hidden service is 100% ready for action when you receive the callback) * ``TimeIntervalCommaList`` from Tor config supported * :class:`TorControlProtocol ` now has a ``.all_routers`` member (a ``set()`` of all Routers) * documentation fix from `sammyshj `_ v0.11.0 ------- *August 16, 2014* * September 6, 2015. bugfix release: `txtorcon-0.11.1.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * fixed Debian bug `797261 `_ causing 3 tests to fail * `txtorcon-0.11.0.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * More control for ``launch_tor``: access stdout, stderr in real-time and control whether we kill Tor on and stderr output. See issue #79. * Warning about ``build_circuit`` being called without a guard first is now optional (default is still warn) (from arlolra_) * ``available_tcp_port()`` now in util (from arlolra_) * ``TorState`` now has a ``.routers_by_hash`` member (from arlolra_) .. _arlolra: https://github.com/arlolra v0.10.1 ------- *July 20, 2014* * `txtorcon-0.10.1.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * fix bug incorrectly issuing RuntimeError in brief window of time on event-listeners * issue #78: Add tox tests and fix for Twisted 12.0.0 (and prior), as this is what Debian squeeze ships * issue #77: properly expand relative and tilde paths for ``hiddenServiceDir`` via endpoints v0.10.0 ------- *June 15, 2014* * `txtorcon-0.10.0.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * In collaboration with `David Stainton `_ after a pull-request, we have endpoint parser plugins for Twisted! This means code like ``serverFromString("onion:80").listen(...)`` is enough to start a service. See the **4-line example** :ref:`hello_darkweb.py` * The above **also** means that **any** endpoint-using Twisted program can immediately offer its TCP services via Hidden Service with **no code changes**. For example, using Twisted Web to serve a WSGI web application would be simply: ``twistd web --port onion:80 --wsgi web.app`` * switch to a slightly-modified `Alabaster Sphinx theme `_ * added :doc:`howtos` to documentation (see :ref:`howto-endpoint`, with demo "video") v0.9.2 ------ *April 23, 2014* * `txtorcon-0.9.2.tar.gz `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * add ``on_disconnect`` callback for TorControlProtocol (no more monkey-patching Protocol API) * add ``age()`` method to Circuit * add ``time_created`` property to Circuit * don't incorrectly listen for NEWDESC events in TorState * add ``.flags`` dict to track flags in Circuit, Stream * ``build_circuit()`` can now take hex IDs (as well as Router instances) * add ``unique_name`` property to Router (returns the hex id, unless ``Named`` then return name) * add ``location`` property to Router * ``TorState.close_circuit`` now takes either a Circuit ID or Circuit instance * ``TorState.close_stream`` now takes either a Stream ID or Stream instance * support both GeoIP API versions * more test-coverage * small patch from `enriquefynn `_ improving ``tor`` binary locating * strip OK lines in TorControlProtocol (see `issue #8 `_) * use TERM not KILL when Tor launch times out (see `issue #68 `_) from ``hellais`` v0.9.1 ------ *January 20, 2014* * `txtorcon-0.9.1.tar.gz `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * put test/ directory at the top level * using "`coverage `_" tool instead of custom script * using `coveralls.io `_ and `travis-ci `_ for test coverage and continuous integration * `issue #56 `_: added Circuit.close() and Stream.close() starting from aagbsn's patch * parsing issues with multi-line keyword discovered and resolved * preserve router nicks from long-names if consensus lacks an entry (e.g. bridges) * using `Twine `_ for releases * `Wheel `_ release now also available * `issue #57 `_: "python setup.py develop" now supported * `issue #59 `_: if tor_launch() times out, Tor is properly killed (starting with pull-request from Ryman) * experimental docker.io-based tests (for HS listening, and tor_launch() timeouts) * `issue #55 `_: pubkey link on readthedocs * `issue #63 `_ * clean up GeoIP handling, and support pygeoip both pre and post 0.3 * slightly improve unit-test coverage (now at 97%, 61 lines of 2031 missing) * added a `Walkthrough `_ to the documentation v0.8.2 ------ *November 22, 2013* * `txtorcon-0.8.2.tar.gz `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * ensure hidden service server-side endpoints listen only on 127.0.0.1 v0.8.1 ------ *May 13, 2013* * `txtorcon-0.8.1.tar.gz `_ (:download:`local-sign ` or `github-sig `_) (`source `_) * fixed improper import in setup.py preventing 0.8.0 from installing * signatures with proper subkey this time * Proper file-flushing in tests and PyPy fixes from Lukas Lueg * docs build issue from isis v0.8.0 ------ *April 11, 2013* (actually uploaded May 11) * **Please use 0.8.1; this won't install due to import problem in setup.py (unless you have pypissh).** * following `semantic versioning `_; * slight **API change** :meth:`.ICircuitListener.circuit_failed`, :meth:`~.ICircuitListener.circuit_closed` and :meth:`.IStreamListener.stream_failed`, :meth:`~.IStreamListener.stream_closed` and :meth:`~.IStreamListener.stream_detach` all now include any keywords in the notification method (some of these lacked flags, or only included some) (`issue #18 `_); * launch_tor() can take a timeout (starting with a patch from hellais); * cleanup from aagbsn; * more test coverage; * run tests cleanly without graphviz (from lukaslueg); * `issue #26 `_ fix from lukaslueg; * pep8 and whitespace targets plus massive cleanup (now pep8 clean, from lukaslueg); * `issue #30 `_ fix reported by webmeister making ipaddr actually-optional; * example using synchronous web server (built-in SimpleHTTPServer) with txtorcon (from lukaslueg); * TorState can now create circuits without an explicit path; * passwords for non-cookie authenticated sessions use a password callback (that may return a Deferred) instead of a string (`issue #44 `_); * fixes for AddrMap in case `#8596 `_ is implemented; v0.7 ---- *November 21, 2012* * `txtorcon-0.7.tar.gz `_ (:download:`local-sig <../signatues/txtorcon-0.7.tar.gz.sig>` or `github-sig `_) (`source `_) * `issue #20 `_ config object now hooked up correctly after launch_tor(); * `patch `_ from hellais for properly handling data_dir given to TCPHiddenServiceEndpoint; * `.tac example `_ from mmaker; * allow TorConfig().hiddenservices.append(hs) to work properly with no attached protocol v0.6 ---- *October 10, 2012* * `txtorcon-0.6.tar.gz `_ (:download:`local-sig <../signatues/txtorcon-0.6.tar.gz.sig>` or `github-sig `_) (`source `_) * debian packaging (mmaker); * psutil fully gone; * *changed API* for launch_tor() to use TorConfig instead of args; * TorConfig.save() works properly with no connected Tor; * fix incorrect handling of 650 immediately after connect; * `pep8 compliance `_; * use assertEqual in tests; * messages with embdedded keywords work properly; * fix bug with setup.py + pip; * `issue #15 `_ reported along with patch by `Isis Lovecruft `_; * consolidate requirements (from `aagbsn `_); * increased test coverage and various minor fixes; * https URIs for ReadTheDocs; v0.5 ---- June 20, 2012 * `txtorcon-0.5.tar.gz `_ (`txtorcon-0.5.tar.gz.sig `_) (`source `_) * remove psutil as a dependency, including from `util.process_from_address` v0.4 ---- June 6, 2012 * `txtorcon-0.4.tar.gz `_ (`txtorcon-0.4.tar.gz.sig `_) * remove built documentation from distribution; * fix PyPI problems ("pip install txtorcon" now works) v0.3 ---- * 0.3 was broken when released (docs couldn't build). v0.2 ---- June 1, 2012 * `txtorcon-0.2.tar.gz `_ (`txtorcon-0.2.tar.gz.sig `_) * incremental parsing; * faster TorState startup; * SAFECOOKIE support; * several bug fixes; * options to :ref:`circuit_failure_rates.py` example to make it actually-useful; * include built documentation + sources in tarball; * include tests in tarball; * improved logging; * patches from `mmaker `_ and `kneufeld `_; v0.1 ---- march, 2012 * `txtorcon-0.1.tar.gz `_ (`txtorcon-0.1.tar.gz.sig `_) txtorcon-0.14.2/docs/walkthrough.rst0000644000175000017500000002513212572755166017416 0ustar mikemike00000000000000Walkthrough =========== .. _Twisted: https://twistedmatrix.com/documents/current/ .. _virtualenv: http://www.virtualenv.org/en/latest/ If this is your first time using a Tor controller library, you're in the right spot. I presume at least some `familiarity `_ with Twisted_ and asynchronous programming. What We'll Learn ---------------- .. _NEWNYM: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n379 .. _walkthrough directory: https://github.com/meejah/txtorcon/tree/master/walkthrough In this tutorial, I will go through several examples building up a small program. We will: * connect to a running Tor; * launch our own Tor; * change the configuration; * get some information from Tor; * listen for events; * and send a NEWNYM_ signal. All the code examples are also in the `walkthrough directory`_. Install txtorcon in a virtualenv -------------------------------- First we need to be able to ``import txtorcon`` in a Python shell. We will accomplish that in a virtualenv_. .. note:: If you're using Debian or Ubuntu, ``pip install txtorcon`` may just work. For the virtualenv, first get the code:: git clone https://github.com/meejah/txtorcon cd txtorcon Now, we can use the Makefile there to create ourselves a virtualenv, activate it and install all the pre-requisites:: make venv . venv/bin/activate pip install -r requirements.txt pip install -r dev-requirements.txt # optional You should now be able to run "import txtorcon" in a python shell, for example:: python -c "import txtorcon" The above should produce no output. If you got an exception, or something else went wrong, read up on virtualenv or try a global install with ``python setup.py install`` Connect to a Running Tor ------------------------ If you've got a system-wide Tor running, it defaults to port 9051 if you have the control interface turned on. ``/etc/tor/torrc`` should contain lines similar to this:: ControlPort 9051 CookieAuthentication 1 Alternatively, if you're currently running the Tor Browser Bundle, it defaults to a port of 9151 and doesn't turn on cookie authentication. Change the options to turn on cookie authentication and change "9051" to "9151" in the following examples. We will use the :meth:`txtorcon.build_tor_connection` API call, which returns a Deferred that callbacks with a :class:`TorControlProtocol ` or :class:`TorState ` instance (depending on whether the ``build_state`` kwarg was True -- the default -- or False). The TorState instance takes a second or two to get built as it queries Tor for all the current relays and creates a :class:`Router ` instance of which there are currently about 5000. TorControlProtocol alone is much faster (dozens of milliseconds). The code to do this would look something like: .. sourcecode:: python from twisted.internet import reactor from twisted.internet.endpoints import TCP4ClientEndpoint import txtorcon def example(state): """ This callback gets called after we've connected and loaded all the current Tor state. state is a TorState instance. """ print "Fully bootstrapped state:", state print " with bootstrapped protocol:", state.protocol reactor.stop() ## change the port to 9151 for Tor Browser Bundle connection = TCP4ClientEndpoint(reactor, "localhost", 9051) d = txtorcon.build_tor_connection(connection) d.addCallback(example) ## this will only return after reactor.stop() is called reactor.run() If all is well, you should see two lines get printed out and then the script will exit:: python 0_connection.py Fully bootstrapped state: with bootstrapped protocol: Launch Our Own Tor ------------------ .. _GETINFO: https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt#l444 .. _mkdtemp: https://docs.python.org/2/library/tempfile.html?highlight=mkdtem#tempfile.mkdtemp For some use-cases you will want to launch a private Tor instance. txtorcon provides :meth:`txtorcon.launch_tor` to do just that. This also uses some Tor commands to link the controller to the Tor instance, so that if the connection is lost Tor will shut itself down. The main difference between connecting and launching is that you have to provide a configuration to launch a Tor with. This is provided via a :class:`TorConfig` instance. This class is a little "magic" in order to provide a nice API, and so you simply set configuration options as members. A minimal configuration to launch a Tor might be:: config = txtorcon.TorConfig() config.ORPort = 0 config.SocksPort = 9999 The ``launch_tor`` method itself also adds several necessary configuration options but *only if* they aren't supplied already. For example, if you want to maintain state (or hidden service keys) between launches, provide your own ``DataDirectory``. The configuration keys ``launch_tor`` adds are: * ``DataDirectory`` a mkdtemp_ directory in ``/tmp/`` (which is deleted at exit, unless it was user-specified) * ``ControlPort`` is set to 9052 unless already specified * ``CookieAuthentication`` is set to 1 * ``__OwningControllerProcess`` is set to our PID Check out the :meth:`txtorcon.launch_tor` documentation. You'll likely want to provide a ``progress_updates`` listener to provide interesting information to your user. Here's a full example:: import os from twisted.internet import reactor, defer from twisted.internet.endpoints import TCP4ClientEndpoint import txtorcon @defer.inlineCallbacks def launched(process_proto): """ This callback gets called after Tor considers itself fully bootstrapped -- it has created a circuit. We get the TorProcessProtocol object, which has the TorControlProtocol instance as .tor_protocol """ protocol = process_proto.tor_protocol print "Tor has launched.\nProtocol:", protocol info = yield protocol.get_info('traffic/read', 'traffic/written') print info reactor.stop() def error(failure): print "There was an error", failure.getErrorMessage() reactor.stop() def progress(percent, tag, summary): ticks = int((percent/100.0) * 10.0) prog = (ticks * '#') + ((10 - ticks) * '.') print '%s %s' % (prog, summary) config = txtorcon.TorConfig() config.ORPort = 0 config.SocksPort = 9999 try: os.mkdir('tor-data') except OSError: pass config.DataDirectory = './tor-data' d = txtorcon.launch_tor(config, reactor, progress_updates=progress) d.addCallback(launched).addErrback(error) ## this will only return after reactor.stop() is called reactor.run() If you've never seen the ``defer.inlineCallbacks`` decorator, then you should `read up on it `_. Once we get the Tor instance launched, we just make two GETINFO_ calls and then exit (which will cause the underlying Tor to also exit). Putting It All Together ----------------------- So, now we've gotten a basic connection to Tor (either by launching one or connecting to a running one) and basically done nothing but exit. Let's do something slightly more interesting. We will connect to a running Tor (like the first example), issue the NEWNYM_ signal (which tells Tor to no longer use any existing circuits for new connections) and then continuously monitor two events: circuit events via ``TorState`` interfaces and ``INFO`` messages via a raw ``add_event_listener``. First, we add a simple implementation of :class:`txtorcon.ICircuitListener`:: class MyCircuitListener(object): implements(txtorcon.ICircuitListener) def circuit_new(self, circuit): print "new", circuit def circuit_launched(self, circuit): print "launched", circuit def circuit_extend(self, circuit, router): print "extend", circuit def circuit_built(self, circuit): print "built", circuit def circuit_closed(self, circuit, **kw): print "closed", circuit, kw def circuit_failed(self, circuit, **kw): print "failed", circuit, kw Next, to illustrate setting up TorState from a TorControlProtocol directly, we add a ``main()`` method that uses ``inlineCallbacks`` to do a few things sequentially after startup. First we use ``TorControlProtocol.signal`` to send a NEWNYM_ request. After that we create a ``TorState`` instance, print out all existing circuits and set up listeners for circuit events (an instance of ``MyCircuitListener``) and INFO messages (via our own method). Here is the full listing:: from twisted.internet import reactor, defer from twisted.internet.endpoints import TCP4ClientEndpoint from zope.interface import implements import txtorcon ## change the port to 9151 for Tor Browser Bundle connection = TCP4ClientEndpoint(reactor, "localhost", 9051) def error(failure): print "Error:", failure.getErrorMessage() reactor.stop() class MyCircuitListener(object): implements(txtorcon.ICircuitListener) def circuit_new(self, circuit): print "new", circuit def circuit_launched(self, circuit): print "launched", circuit def circuit_extend(self, circuit, router): print "extend", circuit def circuit_built(self, circuit): print "built", circuit def circuit_closed(self, circuit, **kw): print "closed", circuit, kw def circuit_failed(self, circuit, **kw): print "failed", circuit, kw @defer.inlineCallbacks def main(connection): version = yield connection.get_info('version', 'events/names') print "Connected to Tor.", version['version'] print version['events/names'] print "Issuing NEWNYM." yield connection.signal('NEWNYM') print "OK." print "Building state." state = txtorcon.TorState(connection) yield state.post_bootstrap print "State initialized." print "Existing circuits:" for c in state.circuits.values(): print ' ', c print "listening for circuit events" state.add_circuit_listener(MyCircuitListener()) print "listening for INFO events" def print_info(i): print "INFO:", i connection.add_event_listener('INFO', print_info) ## since we don't call reactor.stop(), we keep running d = txtorcon.build_tor_connection(connection, build_state=False) d.addCallback(main).addErrback(error) ## this will only return after reactor.stop() is called reactor.run() txtorcon-0.14.2/docs/txtorcon-launching.rst0000644000175000017500000000075012515516430020666 0ustar mikemike00000000000000Launching Tor ============= .. note:: Please see :class:`txtorcon.TCPHiddenServiceEndpoint` In general, endpoints are an easier way to interact with launching Tor. However, if you do need configuration control, you can do that too and you're in the right spot. get_global_tor -------------- .. autofunction:: txtorcon.get_global_tor TorProcessProtocol ------------------ .. autoclass:: txtorcon.TorProcessProtocol launch_tor ---------- .. autofunction:: txtorcon.launch_tor txtorcon-0.14.2/docs/introduction.rst0000644000175000017500000000235312515516430017562 0ustar mikemike00000000000000Introduction ============ txtorcon is an implementation of the `control-spec `_ for `Tor `_ using the `Twisted `_ networking library for `Python `_. This would be of interest to anyone wishing to write event-based software in Python that talks to a Tor program. Currently, txtorcon is capable of: * maintaining up-to-date state information about Tor (Circuits, Streams and Routers) * maintaining current configuration information * maintaining representation of Tor's address mappings (with expiry) * interrogating initial state of all three of the above * listing for and altering stream to circuit mappings * building custom circuits * Circuit and Stream state listeners * uses `GeoIP `_ to provide location and ASN information for Routers * uses `psutil `_ (optional) to locate processes creating Streams * listening for any Tor EVENT Comments (positive or negative) appreciated. Even better if they come with patches. .. role:: raw-html(raw) :format: html :raw-html:`` txtorcon-0.14.2/docs/txtorcon-protocol.rst0000644000175000017500000000062212312757205020557 0ustar mikemike00000000000000Protocol and Helper Classes =========================== TorControlProtocol ------------------ .. autoclass:: txtorcon.TorControlProtocol TorProtocolFactory ------------------ .. autoclass:: txtorcon.TorProtocolFactory TorProcessProtocol ------------------ .. autoclass:: txtorcon.TorProcessProtocol TCPHiddenServiceEndpoint ------------------------ .. autoclass:: txtorcon.TCPHiddenServiceEndpoint txtorcon-0.14.2/docs/conf.py0000644000175000017500000002075112627744056015623 0ustar mikemike00000000000000# -*- coding: utf-8 -*- # # txtorcon documentation build configuration file, created by # sphinx-quickstart on Thu Jan 26 13:04:28 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('.')) ## we have a custom-changed alabaster theme... sys.path.insert(0, os.path.join(os.path.split(__file__)[0], '_themes')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' ## trying to set t his somewhere... autodoc_member_order = 'bysource' autodoc_default_flags = ['members', 'show-inheritance', 'undoc-members'] autoclass_content = 'both' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.autosummary', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'repoze.sphinx.autointerface', 'apilinks_sphinxext' ] todo_include_todos = True # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'txtorcon' copyright = u'2012, meejah@meejah.ca' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. from txtorcon import __version__ version = __version__ # The full version, including alpha/beta/rc tags. release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' html_theme = 'scrolls' html_theme = 'traditional' html_theme = 'nature' html_theme = 'pyramid' html_theme = 'agogo' html_theme = 'haiku' html_theme_options = { # 'stickysidebar': 'true', # 'rightsidebar':'true', 'nosidebar': 'false', # 'full_logo': 'false' 'sidebarwidth': '300' } # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} ## playing with alabaster, from https://github.com/bitprophet/alabaster import alabaster html_theme_path = [alabaster.get_path()] extensions.append('alabaster') html_theme = 'alabaster' html_sidebars = { '**': [ 'about.html', 'navigation.html', 'searchbox.html', 'donate.html', ] } html_theme_options = { 'logo': 'logo.svg', 'github_button': 'false', 'github_user': 'meejah', 'github_repo': 'txtorcon', 'travis_button': 'true', 'coveralls_button': 'true', 'logo_name': 'true', 'description': 'Control Tor from Twisted', 'logo_text_align': 'center', 'flattr_uri': 'http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub', 'note_bg': '#ccddcc', 'note_border': '#839496', } # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = 'timaq4ygg2iegci7.onion: txtorcon documentation' # A shorter title for the navigation bar. Default is the same as html_title. html_short_title = 'txtorcon docs' # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = 'avatar.png' #html_logo = 'logo.png' #html_logo = 'logo.svg' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. html_show_copyright = False # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'txtorcondoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'txtorcon.tex', u'txtorcon Documentation', u'meejah', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'txtorcon', u'txtorcon Documentation', [u'meejah'], 1) ] txtorcon-0.14.2/docs/apilinks_sphinxext.py0000644000175000017500000000313612312757205020607 0ustar mikemike00000000000000''' Sphinx/docutils extension to create links to pyDoctor documentation using a RestructuredText interpreted text role that looks like this: :api:`python_object_to_link_to