txtorcon-0.19.3/0000755000175000017500000000000013111226470013355 5ustar mikemike00000000000000txtorcon-0.19.3/scripts/0000755000175000017500000000000013111226470015044 5ustar mikemike00000000000000txtorcon-0.19.3/scripts/asciinema-demo0.py0000755000175000017500000000700712444716153020372 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.19.3/scripts/asciinema-demo1.py0000755000175000017500000000450712701107713020365 0ustar mikemike00000000000000#!/usr/bin/env python # this is a hack-tacular script to pass to asciinema like: # asciinema -c ./asciinema-demo1.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$ ' def interkey_interval(): "in milliseconds" # return 0 # faster debugging 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: if callable(line): line() continue really_run = True if line.startswith('!'): really_run = False line = line[1:] sys.stdout.write(colors.blue(prompt)) type_it_out(line) time.sleep(0.5) print if really_run: # XXX nice to have time-limit? os.system(colors.strip_color(line)) def change_prompt(p): global prompt prompt = p commands = [] commands.append(colors.bold('export TMPDIR=/dev/shm')) commands.append(colors.red('# see http://txtorcon.readthedocs.org')) commands.append('git clone https://github.com/meejah/txtorcon') commands.append(colors.bold('virtualenv venv')) commands.append('!' + colors.bold('source ./venv/bin/activate')) commands.append(lambda: change_prompt('(venv)user@machine:~/src/txtorcon$ ')) commands.append(lambda: sys.path.insert(0, './venv/bin')) commands.append(colors.bold(colors.white('pip install --editable ./txtorcon'))) commands.append('!' + colors.white('cd txtorcon')) commands.append(lambda: os.chdir('./txtorcon')) commands.append(lambda: change_prompt('user@machine:~/src/txtorcon$ ')) commands.append('make coverage') #commands.append('python examples/add_hiddenservice_to_system_tor.py') commands.append(colors.red('# okay, lets try one of the examles')) commands.append('ls examples/') commands.append('python examples/dump_config.py | head') commands.append('python examples/hello_darkweb.py') commands.append(colors.red('# thanks for watching')) commands.append(colors.red('# https://github.com/meejah/txtorcon')) commands.append(colors.red('# https://txtorcon.readthedocs.org')) if __name__ == '__main__': do_commands(commands) txtorcon-0.19.3/twisted/0000755000175000017500000000000013111226470015040 5ustar mikemike00000000000000txtorcon-0.19.3/twisted/plugins/0000755000175000017500000000000013111226470016521 5ustar mikemike00000000000000txtorcon-0.19.3/twisted/plugins/txtorcon_endpoint_parser.py0000644000175000017500000000024112752747562024250 0ustar mikemike00000000000000import txtorcon tcpHiddenServiceEndpointParser = txtorcon.TCPHiddenServiceEndpointParser() tcpTorClientEndpointParser = txtorcon.TorClientEndpointStringParser() txtorcon-0.19.3/dev-requirements.txt0000644000175000017500000000022713106646374017433 0ustar mikemike00000000000000tox coverage cuvner setuptools>=0.8.0 Sphinx repoze.sphinx.autointerface>=0.4 coveralls codecov wheel twine pyflakes pep8 mock ipaddress>=1.0.16 geoip txtorcon-0.19.3/PKG-INFO0000644000175000017500000001560113111226470014455 0ustar mikemike00000000000000Metadata-Version: 1.1 Name: txtorcon Version: 0.19.3 Summary: Twisted-based Tor controller client, with state-tracking and configuration abstractions. https://txtorcon.readthedocs.org https://github.com/meejah/txtorcon Home-page: https://github.com/meejah/txtorcon Author: meejah Author-email: meejah@meejah.ca License: MIT Description: .. _NOTE: see docs/index.rst for the starting-point .. _ALSO: https://txtorcon.readthedocs.org for rendered docs .. image:: https://travis-ci.org/meejah/txtorcon.png?branch=master :target: https://www.travis-ci.org/meejah/txtorcon :alt: travis .. image:: https://coveralls.io/repos/meejah/txtorcon/badge.png :target: https://coveralls.io/r/meejah/txtorcon :alt: coveralls .. image:: http://codecov.io/github/meejah/txtorcon/coverage.svg?branch=master :target: http://codecov.io/github/meejah/txtorcon?branch=master :alt: codecov .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=stable :target: https://txtorcon.readthedocs.io/en/stable :alt: ReadTheDocs .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=latest :target: https://txtorcon.readthedocs.io/en/latest :alt: ReadTheDocs .. image:: http://api.flattr.com/button/flattr-badge-large.png :target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub :alt: flattr .. image:: https://landscape.io/github/meejah/txtorcon/master/landscape.svg?style=flat :target: https://landscape.io/github/meejah/txtorcon/master :alt: Code Health txtorcon ======== - **docs**: https://txtorcon.readthedocs.org or http://timaq4ygg2iegci7.onion - **code**: https://github.com/meejah/txtorcon - ``torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git`` - MIT-licensed; - Python 2.7, PyPy 5.0.0+, Python 3.4+; - depends on `Twisted `_, `Automat `_, (and the `ipaddress `_ backport for non Python 3) .. caution:: Several large, new features have landed on master. If you're working directly from master, note that some of these APIs may change before the next release. Ten Thousand Feet ----------------- txtorcon is an implementation of the `control-spec `_ for `Tor `_ using the `Twisted `_ networking library for `Python `_. This is useful for writing utilities to control or make use of Tor in event-based Python programs. If your Twisted program supports endpoints (like ``twistd`` does) your server or client can make use of Tor immediately, with no code changes. Start your own Tor or connect to one and get live stream, circuit, relay updates; read and change config; monitor events; build circuits; create onion services; etcetera (`ReadTheDocs `_). Some Possibly Motivational Example Code --------------------------------------- `download `_ (also `python3 style `_) .. code:: python from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks from twisted.internet.endpoints import UNIXClientEndpoint import treq import txtorcon @react @inlineCallbacks def main(reactor): tor = yield txtorcon.connect( reactor, UNIXClientEndpoint(reactor, "/var/run/tor/control") ) print("Connected to Tor version {}".format(tor.version)) url = 'https://www.torproject.org:443' print("Downloading {}".format(url)) resp = yield treq.get(url, agent=tor.web_agent()) print(" {} bytes".format(resp.length)) data = yield resp.text() print("Got {} bytes:\n{}\n[...]{}".format( len(data), data[:120], data[-120:], )) print("Creating a circuit") state = yield tor.create_state() circ = yield state.build_circuit() yield circ.when_built() print(" path: {}".format(" -> ".join([r.ip for r in circ.path]))) print("Downloading meejah's public key via above circuit...") resp = yield treq.get( 'https://meejah.ca/meejah.asc', agent=circ.web_agent(reactor, tor.config.socks_endpoint(reactor)), ) data = yield resp.text() print(data) Try It Now On Debian/Ubuntu --------------------------- For example, serve some files via an onion service (*aka* hidden service): .. code-block:: shell-session $ sudo apt-get install python-txtorcon $ twistd -n web --port "onion:80" --path ~/public_html Read More --------- All the documentation starts `in docs/index.rst `_. Also hosted at `txtorcon.rtfd.org `_. You'll want to start with `the introductions `_ (`hosted at RTD `_). 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.19.3/setup.cfg0000644000175000017500000000004613111226470015176 0ustar mikemike00000000000000[egg_info] tag_build = tag_date = 0 txtorcon-0.19.3/test/0000755000175000017500000000000013111226470014334 5ustar mikemike00000000000000txtorcon-0.19.3/test/test_circuit.py0000644000175000017500000005143413106646076017432 0ustar mikemike00000000000000import datetime import ipaddress from mock import patch from twisted.trial import unittest from twisted.internet import defer from twisted.python.failure import Failure from zope.interface import implementer from txtorcon import Circuit from txtorcon import Stream from txtorcon import TorControlProtocol from txtorcon import TorState from txtorcon import Router from txtorcon.router import hexIdFromHash from txtorcon.circuit import TorCircuitEndpoint, _get_circuit_attacher 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 from mock import Mock @implementer(IRouterContainer) @implementer(ICircuitListener) @implementer(ICircuitContainer) @implementer(ITorControlProtocol) class FakeTorController(object): post_bootstrap = defer.Deferred() queue_command = Mock() 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.id_hash = hsh self.id_hex = hexIdFromHash(self.id_hash) 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 TestCircuitEndpoint(unittest.TestCase): @defer.inlineCallbacks def test_attach(self): @implementer(ICircuitContainer) class FakeContainer(object): pass container = FakeContainer() stream = Stream(container) circuit = Mock() target_endpoint = Mock() reactor = Mock() state = Mock() TorCircuitEndpoint( reactor, state, circuit, target_endpoint, ) attacher = yield _get_circuit_attacher(reactor, state) attacher.add_endpoint(target_endpoint, circuit) yield attacher.attach_stream(stream, []) # hmmm, no assert?? @defer.inlineCallbacks def test_attach_stream_failure(self): @implementer(ICircuitContainer) class FakeContainer(object): pass container = FakeContainer() stream = Stream(container) stream.source_addr = ipaddress.IPv4Address(u'0.0.0.0') stream.source_port = 12345 circuit = Mock() circuit.when_built = Mock(return_value=Failure(Exception('testing1234'))) target_endpoint = Mock() src_addr = Mock() src_addr.host = u'0.0.0.0' src_addr.port = 12345 target_endpoint._get_address = Mock(return_value=defer.succeed(src_addr)) reactor = Mock() state = Mock() TorCircuitEndpoint( reactor, state, circuit, target_endpoint, ) attacher = yield _get_circuit_attacher(reactor, state) d = attacher.add_endpoint(target_endpoint, circuit) self.assertEqual(len(attacher._circuit_targets), 1) # this will fail, but should be ignored yield attacher.attach_stream(stream, []) with self.assertRaises(Exception) as ctx: yield d self.assertTrue("testing1234" in str(ctx.exception)) @defer.inlineCallbacks def test_attach_failure_unfound(self): @implementer(ICircuitContainer) class FakeContainer(object): pass reactor = Mock() container = FakeContainer() stream = Stream(container) state = Mock() attacher = yield _get_circuit_attacher(reactor, state) attacher.attach_stream_failure(stream, None) # no assert; just making sure this doesn't explode 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' % now.strftime('%Y-%m-%dT%H:%M:%S') circuit.update(update.split()) diff = circuit.age(now=now) self.assertEqual(diff, 0) self.assertTrue(circuit.time_created is not None) @patch('txtorcon.circuit.datetime') def test_age_default(self, fake_datetime): """ age() w/ defaults works properly """ from datetime import datetime now = datetime.fromtimestamp(60.0) fake_datetime.return_value = now fake_datetime.utcnow = Mock(return_value=now) tor = FakeTorController() circuit = Circuit(tor) circuit._time_created = datetime.fromtimestamp(0.0) self.assertEqual(circuit.age(), 60) 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.assertEqual(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), 1) self.assertTrue('Circuit is FAILED but still has 1 streams' in str(errs[0])) 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.id_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)) d0 = 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 d0.result.called) # not unit-test-y? shouldn't probably delve into internals I # suppose... self.assertTrue(circuit._closing_deferred is not None) # if we try to close it again (*before* the actual close has # succeeded!) we should also still be waiting. d1 = circuit.close() self.assertTrue(not d1.called) # ...and this Deferred should *not* be the same as the first self.assertTrue(d0 is not d1) # 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()) # if we close *after* the close has succeeded, then we should # immediately "succeed" d2 = circuit.close() self.assertTrue(d1.called) # confirm that our circuit callback has been triggered already self.assertRaises( defer.AlreadyCalledError, d0.callback, "should have been called already" ) return defer.DeferredList([d0, d1, d2]) 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_when_closed(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()) d0 = circuit.when_closed() self.assertFalse(d0.called) circuit.update('123 BUILT $E11D2B2269CC25E67CA6C9FB5843497539A74FD0=eris,$50DD343021E509EB3A5A7FD0D8A4F8364AFBDCB5=venus,$253DFF1838A2B7782BE7735F74E50090D46CA1BC=chomsky PURPOSE=GENERAL'.split()) circuit.update('123 CLOSED'.split()) d1 = circuit.when_closed() self.assertTrue(d0 is not d1) self.assertTrue(d0.called) self.assertTrue(d1.called) 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() called = [] def err(f): called.append(f) return None d.addErrback(err) state.circuit_closed(circuit, REASON='testing') self.assertEqual(1, len(called)) self.assertTrue(isinstance(called[0], Failure)) self.assertTrue('testing' in str(called[0].value)) return d def test_stream_success(self): tor = FakeTorController() a = FakeRouter('$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', 'a') tor.routers['$E11D2B2269CC25E67CA6C9FB5843497539A74FD0'] = a circuit = Circuit(tor) reactor = Mock() circuit.stream_via( reactor, 'torproject.org', 443, Mock(), use_tls=True, ) def test_circuit_web_agent(self): tor = FakeTorController() a = FakeRouter('$E11D2B2269CC25E67CA6C9FB5843497539A74FD0', 'a') tor.routers['$E11D2B2269CC25E67CA6C9FB5843497539A74FD0'] = a circuit = Circuit(tor) reactor = Mock() # just testing this doesn't cause an exception circuit.web_agent(reactor, Mock()) txtorcon-0.19.3/test/test_util.py0000644000175000017500000003434413106645477016752 0ustar mikemike00000000000000import os import sys import tempfile from mock import patch from unittest import skipIf import ipaddress 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 implementer 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 from txtorcon.util import unescape_quoted_string from txtorcon.util import available_tcp_port from txtorcon.util import version_at_least from txtorcon.util import default_control_port from txtorcon.util import _Listener, _ListenerCollection from txtorcon.util import create_tbb_web_headers class FakeState: tor_pid = 0 @implementer(IProtocolFactory) class FakeProtocolFactory: 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.assertEqual(ret_val, None) @skipIf('pypy' in sys.version.lower(), "No GeoIP in PyPy") 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.assertEqual(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.assertEqual(nl.city[0], 'City') self.assertEqual(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.assertEqual(nl.city[0], 'City') self.assertEqual(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 port = yield available_tcp_port(reactor) ep = TCP4ServerEndpoint(reactor, port) listener = yield ep.listen(FakeProtocolFactory()) try: pid = process_from_address('0.0.0.0', port, 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, b'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'), 'wb') f.write(b'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): def test_create_ipaddr(self): ip = maybe_ip_addr('1.2.3.4') self.assertTrue(isinstance(ip, ipaddress.IPv4Address)) @patch('txtorcon.util.ipaddress') def test_create_ipaddr_fail(self, ipaddr): def foo(blam): raise ValueError('testing') ipaddr.ip_address.side_effect = foo ip = maybe_ip_addr('1.2.3.4') self.assertTrue(isinstance(ip, type('1.2.3.4'))) class TestUnescapeQuotedString(unittest.TestCase): ''' Test cases for the function unescape_quoted_string. ''' def test_valid_string_unescaping(self): unescapeable = { '\\\\': '\\', # \\ -> \ r'\"': r'"', # \" -> " r'\\\"': r'\"', # \\\" -> \" r'\\\\\"': r'\\"', # \\\\\" -> \\" '\\"\\\\': '"\\', # \"\\ -> "\ "\\'": "'", # \' -> ' "\\\\\\'": "\\'", # \\\' -> \ r'some\"text': 'some"text', 'some\\word': 'someword', '\\delete\\ al\\l un\\used \\backslashes': 'delete all unused backslashes', '\\n\\r\\t': '\n\r\t', '\\x00 \\x0123': 'x00 x0123', '\\\\x00 \\\\x00': '\\x00 \\x00', '\\\\\\x00 \\\\\\x00': '\\x00 \\x00' } for escaped, correct_unescaped in unescapeable.items(): escaped = '"{}"'.format(escaped) unescaped = unescape_quoted_string(escaped) msg = "Wrong unescape: {escaped} -> {unescaped} instead of {correct}" msg = msg.format(unescaped=unescaped, escaped=escaped, correct=correct_unescaped) self.assertEqual(unescaped, correct_unescaped, msg=msg) def test_string_unescape_octals(self): ''' Octal numbers can be escaped by a backslash: \0 is interpreted as a byte with the value 0 ''' for number in range(0x7f): escaped = '\\%o' % number result = unescape_quoted_string('"{}"'.format(escaped)) expected = chr(number) msg = "Number not decoded correctly: {escaped} -> {result} instead of {expected}" msg = msg.format(escaped=escaped, result=repr(result), expected=repr(expected)) self.assertEqual(result, expected, msg=msg) def test_invalid_string_unescaping(self): invalid_escaped = [ '"""', # " - unescaped quote '"\\"', # \ - unescaped backslash '"\\\\\\"', # \\\ - uneven backslashes '"\\\\""', # \\" - quotes not escaped ] for invalid_string in invalid_escaped: self.assertRaises(ValueError, unescape_quoted_string, invalid_string) class TestVersions(unittest.TestCase): def test_version_1(self): self.assertTrue( version_at_least("1.2.3.4", 1, 2, 3, 4) ) def test_version_2(self): self.assertFalse( version_at_least("1.2.3.4", 1, 2, 3, 5) ) def test_version_3(self): self.assertTrue( version_at_least("1.2.3.4", 1, 2, 3, 2) ) def test_version_4(self): self.assertTrue( version_at_least("2.1.1.1", 2, 0, 0, 0) ) class TestHeaders(unittest.TestCase): def test_simple(self): create_tbb_web_headers() class TestDefaultPort(unittest.TestCase): def test_no_env_var(self): p = default_control_port() self.assertEqual(p, 9151) @patch('txtorcon.util.os') def test_env_var(self, fake_os): fake_os.environ = dict(TX_CONTROL_PORT=1234) p = default_control_port() self.assertEqual(p, 1234) class TestListeners(unittest.TestCase): def test_add_remove(self): listener = _Listener() calls = [] def cb(*args, **kw): calls.append((args, kw)) listener.add(cb) listener.notify('foo', 'bar', quux='zing') listener.remove(cb) listener.notify('foo', 'bar', quux='zing') self.assertEqual(1, len(calls)) self.assertEqual(('foo', 'bar'), calls[0][0]) self.assertEqual(dict(quux='zing'), calls[0][1]) def test_notify_with_exception(self): listener = _Listener() calls = [] def cb(*args, **kw): calls.append((args, kw)) def bad_cb(*args, **kw): raise Exception("sadness") listener.add(bad_cb) listener.add(cb) listener.notify('foo', 'bar', quux='zing') self.assertEqual(1, len(calls)) self.assertEqual(('foo', 'bar'), calls[0][0]) self.assertEqual(dict(quux='zing'), calls[0][1]) def test_collection_invalid_event(self): collection = _ListenerCollection(['event0', 'event1']) with self.assertRaises(Exception) as ctx: collection('bad', lambda: None) self.assertTrue('Invalid event' in str(ctx.exception)) def test_collection_invalid_event_notify(self): collection = _ListenerCollection(['event0', 'event1']) with self.assertRaises(Exception) as ctx: collection.notify('bad', lambda: None) self.assertTrue('Invalid event' in str(ctx.exception)) def test_collection_invalid_event_remove(self): collection = _ListenerCollection(['event0', 'event1']) with self.assertRaises(Exception) as ctx: collection.remove('bad', lambda: None) self.assertTrue('Invalid event' in str(ctx.exception)) def test_collection(self): collection = _ListenerCollection(['event0', 'event1']) calls = [] def cb(*args, **kw): calls.append((args, kw)) collection('event0', cb) collection.notify('event0', 'foo', 'bar', quux='zing') collection.remove('event0', cb) collection.notify('event0', 'foo', 'bar', quux='zing') self.assertEqual(1, len(calls)) self.assertEqual(calls[0][0], ('foo', 'bar')) self.assertEqual(calls[0][1], dict(quux='zing')) txtorcon-0.19.3/test/test_torstate.py0000644000175000017500000014435513106645477017646 0ustar mikemike00000000000000from __future__ import print_function from zope.interface import implementer, directlyProvides 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 tempfile import six from ipaddress import IPv4Address from mock import patch, Mock 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 import build_timeout_circuit from txtorcon import CircuitBuildTimedOutError 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 from txtorcon.torstate import _extract_reason from txtorcon.circuit import _get_circuit_attacher if six.PY3: from .py3_torstate import TorStatePy3Tests # noqa @implementer(ICircuitListener) class CircuitListener(object): 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) @implementer(IStreamListener) class StreamListener(object): 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) @implementer(IReactorCore) class FakeReactor: 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' @implementer(IStreamClientEndpoint) class FakeEndpoint: 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) @implementer(IStreamClientEndpoint) class FakeEndpointAnswers: 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): data = self.answers.pop().split('\n') if len(data) == 1: linecb('{}={}'.format(key, data[0])) else: linecb('{}='.format(key)) for line in data: linecb(line) 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) return x def confirm_state(self, x): self.assertIsInstance(x, TorState) self.assertTrue(x.post_bootstrap.called) return x def confirm_consensus(self, x): self.assertEqual(1, len(x.all_routers)) self.assertEqual('fake', list(x.routers.values())[0].name) 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) return state 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_ns(self): fake_consensus = '\n'.join([ 'r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 11.11.11.11 443 80', 's Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof', 'r ekaf foooooooooooooooooooooooooo barbarbarbarbarbarbarbarbar 2011-11-11 16:30:00 22.22.22.22 443 80', 's Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof', '', ]) p = FakeEndpointAnswers([fake_consensus, # 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) d.addCallback(self.confirm_consensus).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 UtilTests(unittest.TestCase): def test_extract_reason_no_reason(self): reason = _extract_reason(dict()) self.assertEqual("unknown", reason) 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): @implementer(IStreamAttacher) class MyAttacher(object): 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'): 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() + b"\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(b"250+ns/all=") self.send(b".") self.send(b"250 OK") self.send(b"250+circuit-status=") self.send(b".") self.send(b"250 OK") self.send(b"250-stream-status=") self.send(b"250 OK") self.send(b"250+address-mappings/all=") self.send(b'www.example.com 127.0.0.1 "2012-01-01 00:00:00"') self.send(b'subdomain.example.com 10.0.0.0 "2012-01-01 00:01:02"') self.send(b".") self.send(b"250 OK") for ignored in self.state.event_map.items(): self.send(b"250 OK") self.send(b"250-entry-guards=") self.send(b"250 OK") self.send(b"250 OK") self.assertEqual(len(self.state.addrmap.addr), 4) self.assertTrue('www.example.com' in self.state.addrmap.addr) self.assertTrue('subdomain.example.com' in self.state.addrmap.addr) self.assertTrue('10.0.0.0' in self.state.addrmap.addr) self.assertTrue('127.0.0.1' in self.state.addrmap.addr) self.assertEqual(IPv4Address(u'127.0.0.1'), self.state.addrmap.find('www.example.com').ip) self.assertEqual('www.example.com', self.state.addrmap.find('127.0.0.1').name) self.assertEqual(IPv4Address(u'10.0.0.0'), self.state.addrmap.find('subdomain.example.com').ip) self.assertEqual('subdomain.example.com', self.state.addrmap.find('10.0.0.0').name) 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(b"250+ns/all=") self.send(b".") self.send(b"250 OK") self.send(b"250-circuit-status=123 BUILT PURPOSE=GENERAL") self.send(b"250 OK") self.send(b"250-stream-status=") self.send(b"250 OK") self.send(b"250+address-mappings/all=") self.send(b".") self.send(b"250 OK") for ignored in self.state.event_map.items(): self.send(b"250 OK") self.send(b"250-entry-guards=") self.send(b"250 OK") self.send(b"250 OK") self.assertTrue(self.state.find_circuit(123)) self.assertEqual(len(self.state.circuits), 1) return d def test_unset_attacher(self): @implementer(IStreamAttacher) class MyAttacher(object): def attach_stream(self, stream, circuits): return None fr = FakeReactor(self) attacher = MyAttacher() self.state.set_attacher(attacher, fr) self.send(b"250 OK") self.state.set_attacher(None, fr) self.send(b"250 OK") self.assertEqual( self.transport.value(), b'SETCONF __LeaveStreamsUnattached=1\r\nSETCONF' b' __LeaveStreamsUnattached=0\r\n' ) def test_attacher_twice(self): """ It should be an error to set an attacher twice """ @implementer(IStreamAttacher) class MyAttacher(object): pass attacher = MyAttacher() self.state.set_attacher(attacher, FakeReactor(self)) # attach the *same* instance twice; not an error self.state.set_attacher(attacher, FakeReactor(self)) with self.assertRaises(RuntimeError) as ctx: self.state.set_attacher(MyAttacher(), FakeReactor(self)) self.assertTrue( "already have an attacher" in str(ctx.exception) ) @defer.inlineCallbacks def _test_attacher_both_apis(self): """ similar to above, but first set_attacher is implicit via Circuit.stream_via """ reactor = Mock() directlyProvides(reactor, IReactorCore) @implementer(IStreamAttacher) class MyAttacher(object): pass circ = Circuit(self.state) circ.state = 'BUILT' # use the "preferred" API, which will set an attacher factory = Mock() proto = Mock() proto.when_done = Mock(return_value=defer.succeed(None)) factory.connect = Mock(return_value=defer.succeed(proto)) ep = circ.stream_via(reactor, 'meejah.ca', 443, factory) addr = Mock() addr.host = '10.0.0.1' addr.port = 1011 ep._target_endpoint._get_address = Mock(return_value=defer.succeed(addr)) print("EP", ep) attacher = yield _get_circuit_attacher(reactor, self.state) print("attacher", attacher) d = ep.connect('foo') print("doin' it") stream = Mock() import ipaddress stream.source_addr = ipaddress.IPv4Address(u'10.0.0.1') stream.source_port = 1011 attacher.attach_stream(stream, []) yield d # ...now use the low-level API (should be an error) with self.assertRaises(RuntimeError) as ctx: self.state.set_attacher(MyAttacher(), FakeReactor(self)) self.assertTrue( "already have an attacher" in str(ctx.exception) ) def test_attacher(self): @implementer(IStreamAttacher) class MyAttacher(object): 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(b"250 OK") self.send(b"650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER") self.send(b"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], b'ATTACHSTREAM 1 0') # we should totally ignore .exit URIs attacher.streams = [] self.send(b"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(b"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], b'ATTACHSTREAM 3 0') # normal attach circ = FakeCircuit(1) circ.state = 'BUILT' self.state.circuits[1] = circ attacher.answer = circ self.send(b"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], b'ATTACHSTREAM 4 1') def test_attacher_defer(self): @implementer(IStreamAttacher) class MyAttacher(object): 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(b"250 OK") self.send(b"650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER") self.send(b"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], b'ATTACHSTREAM 1 1') @defer.inlineCallbacks def test_attacher_errors(self): @implementer(IStreamAttacher) class MyAttacher(object): 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 as 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 as 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 as e: msg = str(e) self.assertTrue('Circuit instance' in msg) def test_attacher_no_attach(self): @implementer(IStreamAttacher) class MyAttacher(object): 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(b"250 OK") self.transport.clear() self.send(b"650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER") self.send(b"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(self.transport.value(), b'') 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(), b'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(), b'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(), b'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(), b'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(), b'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(b"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(b"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) first_circuit = list(self.state.circuits.values())[0] self.assertTrue(listen in first_circuit.listeners) # now add a Circuit after we started listening self.protocol.dataReceived(b"650 CIRC 456 LAUNCHED PURPOSE=GENERAL\r\n") self.assertEqual(len(self.state.circuits), 2) self.assertTrue(listen in list(self.state.circuits.values())[0].listeners) self.assertTrue(listen in list(self.state.circuits.values())[1].listeners) # now update the first Circuit to ensure we're really, really # listening self.protocol.dataReceived(b"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 as e: # self.assertTrue('Illegal state' in str(e)) # flip back when we go back to Automat self.assertTrue('Expected "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(b"250+ns/all=") self.send(b".") self.send(b"250 OK") self.send(b"250+circuit-status=") self.send(b".") self.send(b"250 OK") self.send(b"250-stream-status=") self.send(b"250 OK") self.send(b"250-address-mappings/all=") self.send(b'250 OK') for ignored in self.state.event_map.items(): self.send(b"250 OK") self.send(b"250-entry-guards=") self.send(b"250 OK") self.send(b"250 OK") # state is now bootstrapped, we can send our NEWCONSENSUS update self.protocol.dataReceived(b'\r\n'.join(b'''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(b'\n'))) self.protocol.dataReceived(b'\r\n'.join(b'''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(b'\n'))) self.assertEqual(1, len(self.state.all_routers)) 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(b"250+ns/all=") self.send(b".") self.send(b"250 OK") self.send(b"250+circuit-status=") self.send(b".") self.send(b"250 OK") self.send(b"250-stream-status=") self.send(b"250 OK") self.send(b"250-address-mappings/all=") self.send(b"250 OK") for ignored in self.state.event_map.items(): self.send(b"250 OK") self.send(b"250-entry-guards=") self.send(b"250 OK") self.send(b"250 OK") # state is now bootstrapped, we can send our NEWCONSENSUS update self.protocol.dataReceived(b'\r\n'.join(b'''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(b'\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(b"250+ns/all=") self.send(b".") self.send(b"250 OK") self.send(b"250+circuit-status=") self.send(b".") self.send(b"250 OK") self.send(b"250-stream-status=") self.send(b"250 OK") self.send(b"250-address-mappings/all=") self.send(b"250 OK") for ignored in self.state.event_map.items(): self.send(b"250 OK") self.send(b"250-entry-guards=") self.send(b"250 OK") self.send(b"250 OK") # state is now bootstrapped, we can send our NEWCONSENSUS update self.protocol.dataReceived(b'\r\n'.join(b'''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(b'\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(b"250 OK") expected = [('new', {}), ] listen = StreamListener(expected) self.send(b"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.assertEqual(1, len(self.state.streams.values())) self.assertTrue(listen in list(self.state.streams.values())[0].listeners) self.assertEqual(len(self.state.streams), 1) self.assertEqual(len(listen.expected), 1) self.send(b"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(), b'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(), b'EXTENDCIRCUIT 0\r\n') def test_build_circuit_unfound_router(self): self.state.build_circuit(routers=[b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'], using_guards=False) self.assertEqual(self.transport.value(), b'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(), b'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002\r\n') self.send(b"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 as e: self.assertTrue('Expected EXTENDED' in str(e)) def test_listener_mixins(self): self.assertTrue(verifyClass(IStreamListener, StreamListenerMixin)) self.assertTrue(verifyClass(ICircuitListener, CircuitListenerMixin)) def test_build_circuit_timedout(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 timeout = 10 clock = task.Clock() d = build_timeout_circuit(self.state, clock, path, timeout, using_guards=False) clock.advance(10) def check_for_timeout_error(f): self.assertTrue(isinstance(f.type(), CircuitBuildTimedOutError)) d.addErrback(check_for_timeout_error) return d def test_build_circuit_timeout_after_progress(self): """ Similar to above but we timeout after Tor has ack'd our circuit-creation attempt, but before reaching BUILT. """ class FakeRouter: def __init__(self, i): self.id_hex = i self.flags = [] class FakeCircuit(Circuit): def close(self): return defer.succeed(None) path = [] for x in range(3): path.append(FakeRouter("$%040d" % x)) def fake_queue(cmd): self.assertTrue(cmd.startswith('EXTENDCIRCUIT 0')) return defer.succeed("EXTENDED 1234") queue_command = patch.object(self.protocol, 'queue_command', fake_queue) circuit_factory = patch.object(self.state, 'circuit_factory', FakeCircuit) with queue_command, circuit_factory: timeout = 10 clock = task.Clock() d = build_timeout_circuit(self.state, clock, path, timeout, using_guards=False) clock.advance(timeout + 1) def check_for_timeout_error(f): self.assertTrue(isinstance(f.type(), CircuitBuildTimedOutError)) d.addErrback(check_for_timeout_error) return d def test_build_circuit_not_timedout(self): class FakeRouter: def __init__(self, i): self.id_hex = i self.flags = [] path = [] for x in range(3): path.append(FakeRouter("$%040d" % x)) path[0].flags = ['guard'] timeout = 10 clock = task.Clock() d = build_timeout_circuit(self.state, clock, path, timeout, using_guards=True) d.addCallback(self.circuit_callback) self.assertEqual(self.transport.value(), b'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002\r\n') self.send(b"250 EXTENDED 1234") # we can't just .send(b'650 CIRC 1234 BUILT') this because we # didn't fully hook up the protocol to the state, e.g. via # post_bootstrap etc. self.state.circuits[1234].update(['1234', 'BUILT']) # should have gotten a warning about this not being an entry # guard self.assertEqual(len(self.flushWarnings()), 1) return d txtorcon-0.19.3/test/util.py0000644000175000017500000000066313106645477015710 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.19.3/test/test_torinfo.py0000644000175000017500000002244013100171552017425 0ustar mikemike00000000000000from zope.interface import implementer from twisted.trial import unittest from twisted.test import proto_helpers from twisted.internet import defer from txtorcon import ITorControlProtocol, TorInfo, TorControlProtocol @implementer(ITorControlProtocol) class FakeControlProtocol: 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() + b"\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(b'250-AUTH METHODS=HASHEDPASSWORD') self.send(b'250 OK') # response to AUTHENTICATE self.send(b'250 OK') # now we're in _bootstrap() in TorControlProtocol() self.send(b"250-signal/names=") self.send(b"250 OK") self.send(b"250-version=foo") self.send(b"250 OK") self.send(b"250-events/names=") self.send(b"250 OK") self.send(b"250 OK") # for USEFEATURE # do the TorInfo magic self.send(b'250-info/names=') self.send(b'250-multi/path/arg/* a documentation string') self.send(b'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(b"250-multi/path/arg/quux=foo") self.send(b"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.19.3/test/py3_torstate.py0000644000175000017500000000601313106645477017366 0ustar mikemike00000000000000from twisted.trial import unittest from twisted.test import proto_helpers from twisted.internet import defer from twisted.internet.interfaces import IReactorCore from zope.interface import implementer from txtorcon import TorControlProtocol from txtorcon import TorState from txtorcon import Circuit from txtorcon.interface import IStreamAttacher @implementer(IReactorCore) class FakeReactor: 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 TorStatePy3Tests(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 send(self, line): self.protocol.dataReceived(line.strip() + b"\r\n") def test_attacher_coroutine(self): @implementer(IStreamAttacher) class MyAttacher(object): def __init__(self, answer): self.streams = [] self.answer = answer async def attach_stream(self, stream, circuits): self.streams.append(stream) x = await defer.succeed(self.answer) return x 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(b"250 OK") self.send(b"650 STREAM 1 NEW 0 ca.yahoo.com:80 SOURCE_ADDR=127.0.0.1:54327 PURPOSE=USER") self.send(b"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], b'ATTACHSTREAM 1 1') txtorcon-0.19.3/test/profile_startup.py0000644000175000017500000000076613073067516020154 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 = 5494 # number of routers in above file iters = 5 start = time() if False: cProfile.run('state._update_network_status(data)') else: for x in range(iters): state._update_network_status(data) diff = time() - start print("%fs: %f microdescriptors/second" % (diff, (routers * iters) / diff)) txtorcon-0.19.3/test/test_microdesc.py0000644000175000017500000000700213106645477017734 0ustar mikemike00000000000000 from twisted.trial import unittest from txtorcon._microdesc_parser import MicrodescriptorParser class ParserTests(unittest.TestCase): def test_two_no_w(self): relays = [] def create_relay(**kw): relays.append(kw) m = MicrodescriptorParser(create_relay) for line in [ 'r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 11.11.11.11 443 80', 's Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof', 'r ekaf foooooooooooooooooooooooooo barbarbarbarbarbarbarbarbar 2011-11-11 16:30:00 22.22.22.22 443 80', 's Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof', ]: m.feed_line(line) m.done() self.assertEqual(2, len(relays)) self.assertEqual('fake', relays[0]['nickname']) self.assertEqual('ekaf', relays[1]['nickname']) self.assertEqual('11.11.11.11', relays[0]['ip']) self.assertEqual('22.22.22.22', relays[1]['ip']) self.assertTrue('bandwidth' not in relays[0]) self.assertTrue('bandwidth' not in relays[1]) self.assertTrue('flags' in relays[0]) self.assertTrue('flags' in relays[1]) self.assertTrue('FutureProof' in relays[1]['flags']) def test_two(self): relays = [] def create_relay(**kw): relays.append(kw) m = MicrodescriptorParser(create_relay) for line in [ 'r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 11.11.11.11 443 80', 's Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof', 'r ekaf foooooooooooooooooooooooooo barbarbarbarbarbarbarbarbar 2011-11-11 16:30:00 22.22.22.22 443 80', 's Exit Fast Guard HSDir Named Running Stable V2Dir Valid FutureProof', 'w Bandwidth=518000', 'p accept 43,53,79-81', ]: m.feed_line(line) m.done() self.assertEqual(2, len(relays)) self.assertEqual('fake', relays[0]['nickname']) self.assertEqual('ekaf', relays[1]['nickname']) self.assertEqual('11.11.11.11', relays[0]['ip']) self.assertEqual('22.22.22.22', relays[1]['ip']) self.assertTrue('bandwidth' not in relays[0]) self.assertTrue('bandwidth' in relays[1]) self.assertTrue('flags' in relays[0]) self.assertTrue('flags' in relays[1]) self.assertTrue('FutureProof' in relays[1]['flags']) # re-enable when we switch back to Automat def test_bad_line(self): relays = [] def create_relay(**kw): relays.append(kw) m = MicrodescriptorParser(create_relay) with self.assertRaises(Exception) as ctx: m.feed_line('x blam') # self.assertTrue('Unknown microdescriptor' in str(ctx.exception)) self.assertTrue('Expected "r " ' in str(ctx.exception)) self.assertEqual(0, len(relays)) def test_single_ipv6(self): relays = [] def create_relay(**kw): relays.append(kw) m = MicrodescriptorParser(create_relay) for line in [ 'r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 11.11.11.11 443 80', 'a [2001:0:0:0::0]:4321' ]: m.feed_line(line) m.done() self.assertEqual(1, len(relays)) self.assertEqual(['[2001:0:0:0::0]:4321'], list(relays[0]['ip_v6'])) txtorcon-0.19.3/test/test_log.py0000644000175000017500000000023412752747562016551 0ustar mikemike00000000000000from twisted.trial import unittest from txtorcon import log class LoggingTests(unittest.TestCase): def test_debug(self): log.debug_logging() txtorcon-0.19.3/test/test_endpoints.py0000644000175000017500000010715213111226032017750 0ustar mikemike00000000000000from __future__ import print_function import os from mock import patch from mock import Mock, MagicMock from zope.interface import implementer, directlyProvides from twisted.trial import unittest from twisted.test import proto_helpers from twisted.internet import defer, error, tcp, unix from twisted.internet.endpoints import TCP4ClientEndpoint from twisted.internet.endpoints import UNIXClientEndpoint 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 IStreamClientEndpoint from twisted.internet.interfaces import IReactorCore 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 TorConfig from txtorcon import TCPHiddenServiceEndpoint from txtorcon import TorClientEndpoint # from txtorcon import TorClientEndpointStringParser from txtorcon import IProgressProvider from txtorcon import TorOnionAddress from txtorcon.util import NoOpProtocolFactory from txtorcon.util import SingleObserver from txtorcon.endpoints import get_global_tor # FIXME from txtorcon.endpoints import _create_socks_endpoint from txtorcon.circuit import TorCircuitEndpoint, _get_circuit_attacher from txtorcon.controller import Tor from txtorcon.socks import _TorSocksFactory from . import util from .test_torconfig import FakeControlProtocol # FIXME @implementer(IReactorCore) class MockReactor(Mock): """ Just so that our 'provides IReactorCore' assertions pass, but it's still "just a Mock". """ pass @patch('txtorcon.controller.find_tor_binary', return_value='/bin/echo') 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\nControlPort LineList' ) self.protocol.answers.append('HiddenServiceOptions') # why do i have to pass a dict for this V but not this ^ self.protocol.answers.append({'ControlPort': '37337'}) self.patcher = patch( 'txtorcon.controller.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, ftb): 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, ftb): yield get_global_tor( reactor=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: yield get_global_tor( reactor=Mock(), control_port=111, _tor_launcher=lambda x, y, z: True ) self.fail() except RuntimeError: # should be an error pass @defer.inlineCallbacks def test_endpoint_properties(self, ftb): ep = yield TCPHiddenServiceEndpoint.private_tor(self.reactor, 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, ftb): m = Mock() from txtorcon import endpoints endpoints.launch_tor = m yield TCPHiddenServiceEndpoint.private_tor( Mock(), 80, control_port=1234, ) self.assertTrue(m.called) @defer.inlineCallbacks def test_private_tor_no_control_port(self, ftb): m = Mock() from txtorcon import endpoints endpoints.launch_tor = m yield TCPHiddenServiceEndpoint.private_tor(Mock(), 80) self.assertTrue(m.called) @defer.inlineCallbacks def test_system_tor(self, ftb): def boom(): # 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): self.config.bootstrap() return defer.succeed(Tor(Mock(), self.protocol, _tor_config=self.config)) return bam with patch('txtorcon.endpoints.launch_tor') as launch_mock: with patch('txtorcon.controller.connect', new_callable=boom): 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, ftb): 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: 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, ftb): 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)) def test_progress_updates_private_tor(self, ftb): with patch('txtorcon.endpoints.launch_tor') as tor: ep = TCPHiddenServiceEndpoint.private_tor(self.reactor, 1234) self.assertEqual(len(tor.mock_calls), 1) tor.call_args[1]['progress_updates'](40, 'FOO', 'foo to the bar') return ep def test_progress_updates_system_tor(self, ftb): control_ep = Mock() control_ep.connect = Mock(return_value=defer.succeed(None)) directlyProvides(control_ep, IStreamClientEndpoint) ep = TCPHiddenServiceEndpoint.system_tor(self.reactor, control_ep, 1234) ep._tor_progress_update(40, "FOO", "foo to bar") return ep def test_progress_updates_global_tor(self, ftb): with patch('txtorcon.endpoints.get_global_tor') as 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, ftb): 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, ftb): 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, ftb): self.config.bootstrap() ep = TCPHiddenServiceEndpoint(self.reactor, self.config, 123) d = ep.listen(NoOpProtocolFactory()) return d @defer.inlineCallbacks def test_explicit_data_dir(self, ftb): with util.TempDir() as tmp: d = str(tmp) with open(os.path.join(d, 'hostname'), 'w') as f: f.write('public') config = TorConfig(self.protocol) ep = TCPHiddenServiceEndpoint(self.reactor, config, 123, d) # make sure listen() correctly configures our hidden-serivce # with the explicit directory we passed in above yield ep.listen(NoOpProtocolFactory()) self.assertEqual(1, len(config.HiddenServices)) self.assertEqual(config.HiddenServices[0].dir, d) self.assertEqual(config.HiddenServices[0].hostname, 'public') def test_failure(self, ftb): 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, ftb): # 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, ftb): # 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, ftb): # 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) @defer.inlineCallbacks def test_stealth_auth(self, ftb): ''' make sure we produce a HiddenService instance with stealth-auth lines if we had authentication specified in the first place. ''' config = TorConfig(self.protocol) ep = TCPHiddenServiceEndpoint(self.reactor, config, 123, '/dev/null', stealth_auth=['alice', 'bob']) # 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) yield d # returns 'port' self.assertEqual(1, len(config.HiddenServices)) self.assertEqual(config.HiddenServices[0].dir, '/dev/null') self.assertEqual( config.HiddenServices[0].authorize_client[0], 'stealth alice,bob' ) self.assertEqual(None, ep.onion_uri) # XXX cheating; private API config.HiddenServices[0].hostname = 'oh my' self.assertEqual('oh my', ep.onion_uri) @defer.inlineCallbacks def test_factory(self, ftb): reactor = Mock() cp = Mock() cp.get_conf = Mock(return_value=defer.succeed(dict())) with patch(u'txtorcon.endpoints.available_tcp_port', return_value=9999): ep = yield TorClientEndpoint.from_connection(reactor, cp, 'localhost', 1234) self.assertTrue(isinstance(ep, TorClientEndpoint)) self.assertEqual(ep.host, 'localhost') self.assertEqual(ep.port, 1234) 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 = proto_helpers.MemoryReactor() 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') 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' ) 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. @implementer(IProtocol) class FakeProtocol(object): 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!") @implementer(IAddress) class FakeAddress(object): 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)) @implementer(IListeningPort) class FakeListeningPort(object): 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(): # XXX six has xrange/range stuff? for x in range(65535, 0, -1): yield x @implementer(IReactorTCP, IReactorCore) class FakeReactorTcp(object): 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.protocol = self.protocol def blam(): self.protocol.outReceived(b"Bootstrap") self.transport.closeStdin = blam self.protocol.makeConnection(self.transport) self.test = test 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 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 listenTCP(self, port, factory, **kwargs): '''returns IListeningPort''' if self.failures > 0: self.failures -= 1 raise error.CannotListenError(None, None, None) if port == 0: port = next(self._port_generator) 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 def connectUNIX(self, address, factory, timeout=30, checkPID=0): '''should return IConnector''' r = unix.Connector( address, factory, timeout, self, checkPID, ) def blam(*args): print("BLAAAAAM", args) r.connect = blam return r class FakeTorSocksEndpoint(object): """ This ctor signature matches TorSocksEndpoint even though we don't use it in the tests. """ def __init__(self, socks_endpoint, host, port, tls=False, **kw): self.host = host self.port = port self.transport = None self.failure = kw.get('failure', None) self.accept_port = kw.get('accept_port', None) def connect(self, fac): self.factory = fac if self.accept_port: if self.port != self.accept_port: return defer.fail(self.failure) else: if self.failure: return defer.fail(self.failure) self.proto = fac.buildProtocol(None) transport = proto_helpers.StringTransportWithDisconnection() self.proto.makeConnection(transport) self.transport = transport return defer.succeed(self.proto) class FakeSocksProto(object): def __init__(self, host, port, method, factory): self.host = host self.port = port self.method = method self.factory = factory self._done = SingleObserver() def when_done(self): return self._done.when_fired() def makeConnection(self, transport): proto = self.factory.buildProtocol('socks5 addr') self._done.fire(proto) class TestTorCircuitEndpoint(unittest.TestCase): @defer.inlineCallbacks def test_circuit_failure(self): """ If the circuit fails the error propagates """ reactor = Mock() torstate = Mock() target = Mock() target.connect = Mock(return_value=defer.succeed(None)) circ = Mock() circ.state = 'FAILED' src_addr = Mock() src_addr.host = 'host' src_addr.port = 1234 target._get_address = Mock(return_value=defer.succeed(src_addr)) stream = Mock() stream.source_port = 1234 stream.source_addr = 'host' # okay, so we fire up our circuit-endpoint with mostly mocked # things, and a circuit that's already in 'FAILED' state. ep = TorCircuitEndpoint(reactor, torstate, circ, target) # should get a Failure from the connect() d = ep.connect(Mock()) attacher = yield _get_circuit_attacher(reactor, Mock()) attacher.attach_stream(stream, [circ]) try: yield d self.fail("Should get exception") except RuntimeError as e: assert "unusable" in str(e) @defer.inlineCallbacks def test_circuit_stream_failure(self): """ If the stream-attach fails the error propagates """ reactor = Mock() torstate = Mock() target = Mock() target.connect = Mock(return_value=defer.succeed(None)) circ = Mock() circ.state = 'FAILED' src_addr = Mock() src_addr.host = 'host' src_addr.port = 1234 target._get_address = Mock(return_value=defer.succeed(src_addr)) stream = Mock() stream.source_port = 1234 stream.source_addr = 'host' # okay, so we fire up our circuit-endpoint with mostly mocked # things, and a circuit that's already in 'FAILED' state. ep = TorCircuitEndpoint(reactor, torstate, circ, target) # should get a Failure from the connect() d = ep.connect(Mock()) attacher = yield _get_circuit_attacher(reactor, Mock()) attacher.attach_stream_failure(stream, RuntimeError("a bad thing")) try: yield d self.fail("Should get exception") except RuntimeError as e: self.assertEqual("a bad thing", str(e)) @defer.inlineCallbacks def test_success(self): """ Connect a stream via a circuit """ reactor = Mock() torstate = Mock() target = Mock() target.connect = Mock(return_value=defer.succeed('fake proto')) circ = Mock() circ.state = 'NEW' src_addr = Mock() src_addr.host = 'host' src_addr.port = 1234 target._get_address = Mock(return_value=defer.succeed(src_addr)) stream = Mock() stream.source_port = 1234 stream.source_addr = 'host' # okay, so we fire up our circuit-endpoint with mostly mocked # things, and a circuit that's already in 'FAILED' state. ep = TorCircuitEndpoint(reactor, torstate, circ, target) # should get a Failure from the connect() d = ep.connect(Mock()) attacher = yield _get_circuit_attacher(reactor, torstate) yield attacher.attach_stream(stream, [circ]) proto = yield d self.assertEqual(proto, 'fake proto') class TestTorClientEndpoint(unittest.TestCase): @patch('txtorcon.endpoints.get_global_tor') def test_client_connection_failed(self, ggt): """ This test is equivalent to txsocksx's TestSOCKS4ClientEndpoint.test_clientConnectionFailed """ tor_endpoint = FakeTorSocksEndpoint( None, "host123", 9050, failure=Failure(ConnectionRefusedError()), ) endpoint = TorClientEndpoint( '', 0, socks_endpoint=tor_endpoint, ) 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. """ tor_endpoint = FakeTorSocksEndpoint( None, "fakehose", 9050, failure=Failure(ConnectionRefusedError()), ) endpoint = TorClientEndpoint( 'invalid host', 0, socks_username='billy', socks_password='s333cure', socks_endpoint=tor_endpoint) d = endpoint.connect(None) # XXX we haven't fixed socks.py to support user/pw yet ... return self.assertFailure(d, RuntimeError) return self.assertFailure(d, ConnectionRefusedError) def test_no_host(self): self.assertRaises( ValueError, TorClientEndpoint, None, None, Mock(), ) 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) # XXX what's "the Twisted way" to get the port out here? self.assertEqual(ep._socks_endpoint._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 """ tor_endpoint = FakeTorSocksEndpoint(None, "fakehost", 9050) endpoint = TorClientEndpoint( '', 0, socks_endpoint=tor_endpoint, ) endpoint.connect(Mock) self.assertEqual(tor_endpoint.transport.value(), b'\x05\x01\x00') @defer.inlineCallbacks def test_success(self): with patch.object(_TorSocksFactory, "protocol", FakeSocksProto): tor_endpoint = FakeTorSocksEndpoint(Mock(), "fakehost", 9050) endpoint = TorClientEndpoint( u'meejah.ca', 443, socks_endpoint=tor_endpoint, ) proto = yield endpoint.connect(MagicMock()) self.assertTrue(isinstance(proto, FakeSocksProto)) self.assertEqual(u"meejah.ca", proto.host) self.assertEqual(443, proto.port) self.assertEqual('CONNECT', proto.method) 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 ConnectionRefusedError 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: tor_endpoint = FakeTorSocksEndpoint( u"fakehost", "127.0.0.1", port, accept_port=port, failure=Failure(ConnectionRefusedError()), ) endpoint = TorClientEndpoint( '', 0, socks_endpoint=tor_endpoint, ) endpoint.connect(Mock()) self.assertEqual(tor_endpoint.transport.value(), b'\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: ep = FakeTorSocksEndpoint( '', '', 0, accept_port=port, failure=Failure(ConnectionRefusedError()), ) endpoint = TorClientEndpoint('', 0, socks_endpoint=ep) d = endpoint.connect(None) return self.assertFailure(d, ConnectionRefusedError) @patch('txtorcon.endpoints.TorSocksEndpoint') def test_default_socks_ports_fails(self, ep_mock): """ Ensure we iterate over the default socks ports """ class FakeSocks5(object): def __init__(self, *args, **kw): pass def connect(self, *args, **kw): raise ConnectionRefusedError() def _get_address(self): return defer.succeed(None) ep_mock.side_effect = FakeSocks5 endpoint = TorClientEndpoint('', 0) d = endpoint.connect(Mock()) self.assertFailure(d, ConnectionRefusedError) @patch('txtorcon.endpoints.TorSocksEndpoint') @defer.inlineCallbacks def test_default_socks_ports_happy(self, ep_mock): """ Ensure we iterate over the default socks ports """ proto = object() class FakeSocks5(object): def __init__(self, *args, **kw): pass def connect(self, *args, **kw): return proto def _get_address(self): return defer.succeed(None) ep_mock.side_effect = FakeSocks5 endpoint = TorClientEndpoint('', 0) p2 = yield endpoint.connect(None) self.assertTrue(proto is p2) @patch('txtorcon.endpoints.TorSocksEndpoint') @defer.inlineCallbacks def test_tls_socks_no_endpoint(self, ep_mock): the_proto = object() proto = defer.succeed(the_proto) class FakeSocks5(object): def __init__(self, *args, **kw): pass def connect(self, *args, **kw): return proto def _get_address(self): return defer.succeed(None) ep_mock.side_effect = FakeSocks5 endpoint = TorClientEndpoint('torproject.org', 0, tls=True) p2 = yield endpoint.connect(None) self.assertTrue(the_proto is p2) @patch('txtorcon.endpoints.TorSocksEndpoint') @defer.inlineCallbacks def test_tls_socks_with_endpoint(self, ep_mock): """ Same as above, except we provide an explicit endpoint """ the_proto = object() proto_d = defer.succeed(the_proto) class FakeSocks5(object): def __init__(self, *args, **kw): pass def connect(self, *args, **kw): return proto_d def _get_address(self): return defer.succeed(None) ep_mock.side_effect = FakeSocks5 endpoint = TorClientEndpoint( u'torproject.org', 0, socks_endpoint=clientFromString(Mock(), "tcp:localhost:9050"), tls=True, ) p2 = yield endpoint.connect(None) self.assertTrue(p2 is the_proto) def test_client_endpoint_old_api(self): """ Test the old API of passing socks_host, socks_port """ reactor = Mock() endpoint = TorClientEndpoint( 'torproject.org', 0, socks_hostname='localhost', socks_port=9050, reactor=reactor, ) self.assertTrue( isinstance(endpoint._socks_endpoint, TCP4ClientEndpoint) ) endpoint.connect(Mock()) calls = reactor.mock_calls self.assertEqual(1, len(calls)) name, args, kw = calls[0] self.assertEqual("connectTCP", name) self.assertEqual("localhost", args[0]) self.assertEqual(9050, args[1]) def test_client_endpoint_get_address(self): """ Test the old API of passing socks_host, socks_port """ reactor = Mock() endpoint = TorClientEndpoint( 'torproject.org', 0, socks_endpoint=clientFromString(Mock(), "tcp:localhost:9050"), reactor=reactor, ) d = endpoint._get_address() self.assertTrue(not d.called) class TestSocksFactory(unittest.TestCase): @defer.inlineCallbacks def test_explicit_socks(self): reactor = Mock() cp = Mock() cp.get_conf = Mock( return_value=defer.succeed({ 'SocksPort': ['9050', '9150', 'unix:/tmp/boom'] }) ) ep = yield _create_socks_endpoint(reactor, cp, socks_config='unix:/tmp/boom') self.assertTrue(isinstance(ep, UNIXClientEndpoint)) @defer.inlineCallbacks def test_unix_socket_with_options(self): reactor = Mock() cp = Mock() cp.get_conf = Mock( return_value=defer.succeed({ 'SocksPort': ['unix:/tmp/boom SomeOption'] }) ) ep = yield _create_socks_endpoint(reactor, cp) self.assertTrue(isinstance(ep, UNIXClientEndpoint)) self.assertEqual("/tmp/boom", ep._path) @defer.inlineCallbacks def test_nothing_exists(self): reactor = Mock() cp = Mock() cp.get_conf = Mock(return_value=defer.succeed(dict())) with patch(u'txtorcon.endpoints.available_tcp_port', return_value=9999): ep = yield _create_socks_endpoint(reactor, cp) self.assertTrue(isinstance(ep, TCP4ClientEndpoint)) # internal details, but ... self.assertEqual(ep._port, 9999) txtorcon-0.19.3/test/test_socks.py0000644000175000017500000006635013106645477017121 0ustar mikemike00000000000000from six import BytesIO, text_type from mock import Mock, patch from twisted.trial import unittest from twisted.internet import defer from twisted.internet.address import IPv4Address from twisted.internet.protocol import Protocol from twisted.test import proto_helpers from twisted.test.iosim import connect, FakeTransport from txtorcon import socks class SocksStateMachine(unittest.TestCase): def test_illegal_request(self): with self.assertRaises(ValueError) as ctx: socks._SocksMachine('FOO_RESOLVE', u'meejah.ca', 443) self.assertTrue( 'Unknown request type' in str(ctx.exception) ) def test_illegal_host(self): with self.assertRaises(ValueError) as ctx: socks._SocksMachine('RESOLVE', 1234, 443) self.assertTrue( "'host' must be" in str(ctx.exception) ) def test_illegal_ip_addr(self): with self.assertRaises(ValueError) as ctx: socks._create_ip_address(1234, 443) self.assertTrue( "'host' must be" in str(ctx.exception) ) def test_connect_but_no_creator(self): with self.assertRaises(ValueError) as ctx: socks._SocksMachine( 'CONNECT', u'foo.bar', ) self.assertTrue( "create_connection function required" in str(ctx.exception) ) @defer.inlineCallbacks def test_connect_socks_illegal_packet(self): class BadSocksServer(Protocol): def __init__(self): self._buffer = b'' def dataReceived(self, data): self._buffer += data if len(self._buffer) == 3: assert self._buffer == b'\x05\x01\x00' self._buffer = b'' self.transport.write(b'\x05\x01\x01') factory = socks._TorSocksFactory(u'meejah.ca', 1234, 'CONNECT', Mock()) server_proto = BadSocksServer() server_transport = FakeTransport(server_proto, isServer=True) client_proto = factory.buildProtocol('ignored') client_transport = FakeTransport(client_proto, isServer=False) pump = yield connect( server_proto, server_transport, client_proto, client_transport, ) self.assertTrue(server_proto.transport.disconnected) self.assertTrue(client_proto.transport.disconnected) pump.flush() @defer.inlineCallbacks def test_connect_socks_unknown_version(self): class BadSocksServer(Protocol): def __init__(self): self._buffer = b'' self._recv_stack = [ (b'\x05\x01\x00', b'\x05\xff'), ] def dataReceived(self, data): self._buffer += data if len(self._recv_stack) == 0: assert "not expecting any more data, got {}".format(repr(self._buffer)) return expecting, to_send = self._recv_stack.pop(0) got = self._buffer[:len(expecting)] self._buffer = self._buffer[len(expecting):] assert got == expecting, "wanted {} but got {}".format(repr(expecting), repr(got)) self.transport.write(to_send) factory = socks._TorSocksFactory(u'1.2.3.4', 1234, 'CONNECT', Mock()) server_proto = BadSocksServer() server_transport = FakeTransport(server_proto, isServer=True) client_proto = factory.buildProtocol('ignored') client_transport = FakeTransport(client_proto, isServer=False) # returns IOPump yield connect( server_proto, server_transport, client_proto, client_transport, ) self.assertTrue(server_proto.transport.disconnected) self.assertTrue(client_proto.transport.disconnected) @defer.inlineCallbacks def test_connect_socks_unknown_reply_code(self): class BadSocksServer(Protocol): def __init__(self): self._buffer = b'' self._recv_stack = [ (b'\x05\x01\x00', b'\x05\x00'), # the \xff is an invalid reply-code (b'\x05\x01\x00\x01\x01\x02\x03\x04\x04\xd2', b'\x05\xff\x00\x04\x01\x01\x01\x01'), ] def dataReceived(self, data): self._buffer += data if len(self._recv_stack) == 0: assert "not expecting any more data, got {}".format(repr(self._buffer)) return expecting, to_send = self._recv_stack.pop(0) got = self._buffer[:len(expecting)] self._buffer = self._buffer[len(expecting):] assert got == expecting, "wanted {} but got {}".format(repr(expecting), repr(got)) self.transport.write(to_send) factory = socks._TorSocksFactory(u'1.2.3.4', 1234, 'CONNECT', Mock()) server_proto = BadSocksServer() server_transport = FakeTransport(server_proto, isServer=True) client_proto = factory.buildProtocol('ignored') client_transport = FakeTransport(client_proto, isServer=False) d = client_proto._machine.when_done() # returns IOPump yield connect( server_proto, server_transport, client_proto, client_transport, ) with self.assertRaises(Exception) as ctx: yield d self.assertIn('Unknown SOCKS error-code', str(ctx.exception)) @defer.inlineCallbacks def test_socks_relay_data(self): class BadSocksServer(Protocol): def __init__(self): self._buffer = b'' self._recv_stack = [ (b'\x05\x01\x00', b'\x05\x00'), (b'\x05\x01\x00\x01\x01\x02\x03\x04\x04\xd2', b'\x05\x00\x00\x01\x01\x02\x03\x04\x12\x34'), ] def dataReceived(self, data): self._buffer += data if len(self._recv_stack) == 0: assert "not expecting any more data, got {}".format(repr(self._buffer)) return expecting, to_send = self._recv_stack.pop(0) got = self._buffer[:len(expecting)] self._buffer = self._buffer[len(expecting):] assert got == expecting, "wanted {} but got {}".format(repr(expecting), repr(got)) self.transport.write(to_send) factory = socks._TorSocksFactory(u'1.2.3.4', 1234, 'CONNECT', Mock()) server_proto = BadSocksServer() server_transport = FakeTransport(server_proto, isServer=True) client_proto = factory.buildProtocol('ignored') client_transport = FakeTransport(client_proto, isServer=False) pump = yield connect( server_proto, server_transport, client_proto, client_transport, ) # should be relaying now, try sending some datas client_proto.transport.write(b'abcdef') pump.flush() self.assertEqual(b'abcdef', server_proto._buffer) @defer.inlineCallbacks def test_socks_ipv6(self): class BadSocksServer(Protocol): def __init__(self): self._buffer = b'' self._recv_stack = [ (b'\x05\x01\x00', b'\x05\x00'), (b'\x05\x01\x00\x04\x20\x02\x44\x93\x04\xd2', b'\x05\x00\x00\x04' + (b'\x00' * 16) + b'\xbe\xef'), ] def dataReceived(self, data): self._buffer += data if len(self._recv_stack) == 0: assert "not expecting any more data, got {}".format(repr(self._buffer)) return expecting, to_send = self._recv_stack.pop(0) got = self._buffer[:len(expecting)] self._buffer = self._buffer[len(expecting):] assert got == expecting, "wanted {} but got {}".format(repr(expecting), repr(got)) self.transport.write(to_send) factory = socks._TorSocksFactory(u'2002:4493:5105::a299:9bff:fe0e:4471', 1234, 'CONNECT', Mock()) server_proto = BadSocksServer() expected_address = object() server_transport = FakeTransport(server_proto, isServer=True) client_proto = factory.buildProtocol(u'ignored') client_transport = FakeTransport(client_proto, isServer=False, hostAddress=expected_address) pump = yield connect( server_proto, server_transport, client_proto, client_transport, ) # should be relaying now, try sending some datas client_proto.transport.write(b'abcdef') addr = yield factory._get_address() # FIXME how shall we test for IPv6-ness? assert addr is expected_address pump.flush() self.assertEqual(b'abcdef', server_proto._buffer) def test_end_to_end_wrong_method(self): dis = [] def on_disconnect(error_message): dis.append(error_message) sm = socks._SocksMachine('RESOLVE', u'meejah.ca', 443, on_disconnect=on_disconnect) sm.connection() sm.feed_data(b'\x05') sm.feed_data(b'\x01') # we should have sent the request to the server, and nothing # else (because we disconnected) data = BytesIO() sm.send_data(data.write) self.assertEqual( b'\x05\x01\x00', data.getvalue(), ) self.assertEqual(1, len(dis)) self.assertEqual("Wanted method 0 or 2, got 1", dis[0]) def test_end_to_end_wrong_version(self): dis = [] def on_disconnect(error_message): dis.append(error_message) sm = socks._SocksMachine('RESOLVE', u'meejah.ca', 443, on_disconnect=on_disconnect) sm.connection() sm.feed_data(b'\x06') sm.feed_data(b'\x00') # we should have sent the request to the server, and nothing # else (because we disconnected) data = BytesIO() sm.send_data(data.write) self.assertEqual( b'\x05\x01\x00', data.getvalue(), ) self.assertEqual(1, len(dis)) self.assertEqual("Expected version 5, got 6", dis[0]) def test_end_to_end_connection_refused(self): dis = [] def on_disconnect(error_message): dis.append(error_message) sm = socks._SocksMachine( 'CONNECT', u'1.2.3.4', 443, on_disconnect=on_disconnect, create_connection=lambda a, p: None, ) sm.connection() sm.feed_data(b'\x05') sm.feed_data(b'\x00') # reply with 'connection refused' sm.feed_data(b'\x05\x05\x00\x01\x00\x00\x00\x00\xff\xff') self.assertEqual(1, len(dis)) self.assertEqual(socks.ConnectionRefusedError.message, dis[0]) def test_end_to_end_successful_relay(self): class Proto(object): data = b'' lost = [] def dataReceived(self, d): self.data = self.data + d def connectionLost(self, reason): self.lost.append(reason) the_proto = Proto() dis = [] def on_disconnect(error_message): dis.append(error_message) sm = socks._SocksMachine( 'CONNECT', u'1.2.3.4', 443, on_disconnect=on_disconnect, create_connection=lambda a, p: the_proto, ) sm.connection() sm.feed_data(b'\x05') sm.feed_data(b'\x00') # reply with success, port 0x1234 sm.feed_data(b'\x05\x00\x00\x01\x00\x00\x00\x00\x12\x34') # now some data that should get relayed sm.feed_data(b'this is some relayed data') # should *not* have disconnected self.assertEqual(0, len(dis)) self.assertTrue(the_proto.data, b"this is some relayed data") sm.disconnected(socks.SocksError("it's fine")) self.assertEqual(1, len(Proto.lost)) self.assertTrue("it's fine" in str(Proto.lost[0])) def test_end_to_end_success(self): sm = socks._SocksMachine('RESOLVE', u'meejah.ca', 443) sm.connection() sm.feed_data(b'\x05') sm.feed_data(b'\x00') # now we check we got the right bytes out the other side data = BytesIO() sm.send_data(data.write) self.assertEqual( b'\x05\x01\x00' b'\x05\xf0\x00\x03\tmeejah.ca\x00\x00', data.getvalue(), ) def test_end_to_end_connect_and_relay(self): sm = socks._SocksMachine( 'CONNECT', u'1.2.3.4', 443, create_connection=lambda a, p: None, ) sm.connection() sm.feed_data(b'\x05') sm.feed_data(b'\x00') sm.feed_data(b'some relayed data') # now we check we got the right bytes out the other side data = BytesIO() sm.send_data(data.write) self.assertEqual( b'\x05\x01\x00' b'\x05\x01\x00\x01\x01\x02\x03\x04\x01\xbb', data.getvalue(), ) def test_resolve(self): # kurt: most things use (hsot, port) tuples, this probably # should too sm = socks._SocksMachine('RESOLVE', u'meejah.ca', 443) sm.connection() sm.version_reply(0x00) data = BytesIO() sm.send_data(data.write) self.assertEqual( b'\x05\x01\x00' b'\x05\xf0\x00\x03\tmeejah.ca\x00\x00', data.getvalue(), ) @defer.inlineCallbacks def test_resolve_with_reply(self): # kurt: most things use (hsot, port) tuples, this probably # should too sm = socks._SocksMachine('RESOLVE', u'meejah.ca', 443) sm.connection() sm.version_reply(0x00) # make sure the state-machine wanted to send out the correct # request. data = BytesIO() sm.send_data(data.write) self.assertEqual( b'\x05\x01\x00' b'\x05\xf0\x00\x03\tmeejah.ca\x00\x00', data.getvalue(), ) # now feed it a reply (but not enough to parse it yet!) d = sm.when_done() # ...we have to send at least 8 bytes, but NOT the entire hostname sm.feed_data(b'\x05\x00\x00\x03') sm.feed_data(b'\x06meeja') self.assertTrue(not d.called) # now send the rest, checking the buffering in _parse_domain_name_reply sm.feed_data(b'h\x00\x00') self.assertTrue(d.called) answer = yield d # XXX answer *should* be not-bytes, though I think self.assertEqual(b'meejah', answer) @defer.inlineCallbacks def test_unknown_response_type(self): # kurt: most things use (hsot, port) tuples, this probably # should too sm = socks._SocksMachine('RESOLVE', u'meejah.ca', 443) sm.connection() # don't actually support username/password (which is version 0x02) yet # sm.version_reply(0x02) sm.version_reply(0) # make sure the state-machine wanted to send out the correct # request. data = BytesIO() sm.send_data(data.write) self.assertEqual( b'\x05\x01\x00' b'\x05\xf0\x00\x03\tmeejah.ca\x00\x00', data.getvalue(), ) sm.feed_data(b'\x05\x00\x00\xaf\x00\x00\x00\x00') with self.assertRaises(socks.SocksError) as ctx: yield sm.when_done() self.assertTrue('Unexpected response type 175' in str(ctx.exception)) @defer.inlineCallbacks def test_resolve_ptr(self): sm = socks._SocksMachine('RESOLVE_PTR', u'1.2.3.4', 443) sm.connection() sm.version_reply(0x00) data = BytesIO() sm.send_data(data.write) self.assertEqual( b'\x05\x01\x00' b'\x05\xf1\x00\x01\x01\x02\x03\x04\x00\x00', data.getvalue(), ) sm.feed_data( b'\x05\x00\x00\x01\x00\x01\x02\xff\x12\x34' ) addr = yield sm.when_done() self.assertEqual('0.1.2.255', addr) def test_connect(self): sm = socks._SocksMachine( 'CONNECT', u'1.2.3.4', 443, create_connection=lambda a, p: None, ) sm.connection() sm.version_reply(0x00) data = BytesIO() sm.send_data(data.write) self.assertEqual( b'\x05\x01\x00' b'\x05\x01\x00\x01\x01\x02\x03\x04\x01\xbb', data.getvalue(), ) # XXX should re-write (at LEAST) these to use Twisted's IOPump class SocksConnectTests(unittest.TestCase): @defer.inlineCallbacks def test_connect_no_tls(self): socks_ep = Mock() transport = proto_helpers.StringTransport() def connect(factory): factory.startFactory() proto = factory.buildProtocol("addr") proto.makeConnection(transport) self.assertEqual(b'\x05\x01\x00', transport.value()) proto.dataReceived(b'\x05\x00') proto.dataReceived(b'\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00') return proto socks_ep.connect = connect protocol = Mock() factory = Mock() factory.buildProtocol = Mock(return_value=protocol) ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443) proto = yield ep.connect(factory) self.assertEqual(proto, protocol) @defer.inlineCallbacks def test_connect_deferred_proxy(self): socks_ep = Mock() transport = proto_helpers.StringTransport() def connect(factory): factory.startFactory() proto = factory.buildProtocol("addr") proto.makeConnection(transport) self.assertEqual(b'\x05\x01\x00', transport.value()) proto.dataReceived(b'\x05\x00') proto.dataReceived(b'\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00') return proto socks_ep.connect = connect protocol = Mock() factory = Mock() factory.buildProtocol = Mock(return_value=protocol) ep = socks.TorSocksEndpoint( socks_endpoint=defer.succeed(socks_ep), host=u'meejah.ca', port=443, ) proto = yield ep.connect(factory) self.assertEqual(proto, protocol) @defer.inlineCallbacks def test_connect_tls(self): socks_ep = Mock() transport = proto_helpers.StringTransport() def connect(factory): factory.startFactory() proto = factory.buildProtocol("addr") proto.makeConnection(transport) self.assertEqual(b'\x05\x01\x00', transport.value()) proto.dataReceived(b'\x05\x00') proto.dataReceived(b'\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00') return proto socks_ep.connect = connect protocol = Mock() factory = Mock() factory.buildProtocol = Mock(return_value=protocol) ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443, tls=True) proto = yield ep.connect(factory) self.assertEqual(proto, protocol) @defer.inlineCallbacks def test_connect_socks_error(self): socks_ep = Mock() transport = proto_helpers.StringTransport() def connect(factory): factory.startFactory() proto = factory.buildProtocol("addr") proto.makeConnection(transport) self.assertEqual(b'\x05\x01\x00', transport.value()) proto.dataReceived(b'\x05\x00') proto.dataReceived(b'\x05\x01\x00\x01\x00\x00\x00\x00') return proto socks_ep.connect = connect protocol = Mock() factory = Mock() factory.buildProtocol = Mock(return_value=protocol) ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443, tls=True) with self.assertRaises(Exception) as ctx: yield ep.connect(factory) self.assertTrue(isinstance(ctx.exception, socks.GeneralServerFailureError)) @defer.inlineCallbacks def test_connect_socks_error_unknown(self): socks_ep = Mock() transport = proto_helpers.StringTransport() def connect(factory): factory.startFactory() proto = factory.buildProtocol("addr") proto.makeConnection(transport) self.assertEqual(b'\x05\x01\x00', transport.value()) proto.dataReceived(b'\x05\x00') proto.dataReceived(b'\x05\xff\x00\x01\x00\x00\x00\x00') return proto socks_ep.connect = connect protocol = Mock() factory = Mock() factory.buildProtocol = Mock(return_value=protocol) ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443, tls=True) with self.assertRaises(Exception) as ctx: yield ep.connect(factory) self.assertTrue('Unknown SOCKS error-code' in str(ctx.exception)) @defer.inlineCallbacks def test_connect_socks_illegal_byte(self): socks_ep = Mock() transport = proto_helpers.StringTransport() def connect(factory): factory.startFactory() proto = factory.buildProtocol("addr") proto.makeConnection(transport) self.assertEqual(b'\x05\x01\x00', transport.value()) proto.dataReceived(b'\x05\x00') proto.dataReceived(b'\x05\x01\x00\x01\x00\x00\x00\x00') return proto socks_ep.connect = connect protocol = Mock() factory = Mock() factory.buildProtocol = Mock(return_value=protocol) ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443, tls=True) with self.assertRaises(Exception) as ctx: yield ep.connect(factory) self.assertTrue(isinstance(ctx.exception, socks.GeneralServerFailureError)) @defer.inlineCallbacks def test_get_address_endpoint(self): socks_ep = Mock() transport = proto_helpers.StringTransport() delayed_addr = [] def connect(factory): delayed_addr.append(factory._get_address()) delayed_addr.append(factory._get_address()) factory.startFactory() proto = factory.buildProtocol("addr") proto.makeConnection(transport) self.assertEqual(b'\x05\x01\x00', transport.value()) proto.dataReceived(b'\x05\x00') proto.dataReceived(b'\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00') return proto socks_ep.connect = connect protocol = Mock() factory = Mock() factory.buildProtocol = Mock(return_value=protocol) ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443, tls=True) yield ep.connect(factory) addr = yield ep._get_address() self.assertEqual(addr, IPv4Address('TCP', '10.0.0.1', 12345)) self.assertEqual(2, len(delayed_addr)) self.assertTrue(delayed_addr[0] is not delayed_addr[1]) self.assertTrue(all([d.called for d in delayed_addr])) @defer.inlineCallbacks def test_get_address(self): # normally, ._get_address is only called via the # attach_stream() method on Circuit addr = object() factory = socks._TorSocksFactory() d = factory._get_address() self.assertFalse(d.called) factory._did_connect(addr) maybe_addr = yield d self.assertEqual(addr, maybe_addr) # if we do it a second time, should be immediate d = factory._get_address() self.assertTrue(d.called) self.assertEqual(d.result, addr) class SocksResolveTests(unittest.TestCase): @defer.inlineCallbacks def test_resolve(self): socks_ep = Mock() transport = proto_helpers.StringTransport() def connect(factory): factory.startFactory() proto = factory.buildProtocol("addr") proto.makeConnection(transport) # XXX sadness: we probably "should" just feed the right # bytes to the protocol to convince it a connection is # made ... *or* we can cheat and just do the callback # directly... proto._machine._when_done.fire("the dns answer") return proto socks_ep.connect = connect hn = yield socks.resolve(socks_ep, u'meejah.ca') self.assertEqual(hn, "the dns answer") @defer.inlineCallbacks def test_resolve_ptr(self): socks_ep = Mock() transport = proto_helpers.StringTransport() def connect(factory): factory.startFactory() proto = factory.buildProtocol("addr") proto.makeConnection(transport) # XXX sadness: we probably "should" just feed the right # bytes to the protocol to convince it a connection is # made ... *or* we can cheat and just do the callback # directly... proto._machine._when_done.fire(u"the dns answer") return proto socks_ep.connect = connect hn = yield socks.resolve_ptr(socks_ep, u'meejah.ca') self.assertEqual(hn, "the dns answer") @patch('txtorcon.socks._TorSocksFactory') def test_resolve_ptr_str(self, fac): socks_ep = Mock() d = socks.resolve_ptr(socks_ep, 'meejah.ca') self.assertEqual(1, len(fac.mock_calls)) self.assertTrue( isinstance(fac.mock_calls[0][1][0], text_type) ) return d @patch('txtorcon.socks._TorSocksFactory') def test_resolve_str(self, fac): socks_ep = Mock() d = socks.resolve(socks_ep, 'meejah.ca') self.assertEqual(1, len(fac.mock_calls)) self.assertTrue( isinstance(fac.mock_calls[0][1][0], text_type) ) return d @patch('txtorcon.socks._TorSocksFactory') def test_resolve_ptr_bytes(self, fac): socks_ep = Mock() d = socks.resolve_ptr(socks_ep, b'meejah.ca') self.assertEqual(1, len(fac.mock_calls)) self.assertTrue( isinstance(fac.mock_calls[0][1][0], text_type) ) return d @patch('txtorcon.socks._TorSocksFactory') def test_resolve_bytes(self, fac): socks_ep = Mock() d = socks.resolve(socks_ep, b'meejah.ca') self.assertEqual(1, len(fac.mock_calls)) self.assertTrue( isinstance(fac.mock_calls[0][1][0], text_type) ) return d class SocksErrorTests(unittest.TestCase): def _check_error(self, error, cls_, code, message): self.assertTrue(isinstance(error, cls_)) self.assertEqual(error.code, code) self.assertEqual(error.message, message) self.assertEqual(str(error), message) def test_error_factory(self): for cls in socks.SocksError.__subclasses__(): error = socks._create_socks_error(cls.code) self._check_error(error, cls, cls.code, cls.message) def test_custom_error(self): code = 0xFF message = 'Custom error message' self._check_error(socks.SocksError(message), socks.SocksError, None, message) self._check_error(socks.SocksError(message=message), socks.SocksError, None, message) self._check_error(socks.SocksError(code=code), socks.SocksError, code, '') self._check_error(socks.SocksError(message, code=code), socks.SocksError, code, message) self._check_error(socks.SocksError(message=message, code=code), socks.SocksError, code, message) txtorcon-0.19.3/test/test_torcontrolprotocol.py0000644000175000017500000011712713106645477021765 0ustar mikemike00000000000000from __future__ import print_function from __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 functools import tempfile import base64 from binascii import b2a_hex, a2b_hex 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): assert type(line) == bytes self.protocol.dataReceived(line.strip() + b"\r\n") def test_authenticate_cookie(self): self.protocol.makeConnection(self.transport) self.assertEqual(self.transport.value(), b'PROTOCOLINFO 1\r\n') self.transport.clear() cookie_data = b'cookiedata!cookiedata!cookiedata' with open('authcookie', 'wb') as f: f.write(cookie_data) self.send(b'250-PROTOCOLINFO 1') self.send(b'250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="authcookie"') self.send(b'250-VERSION Tor="0.2.2.34"') self.send(b'250 OK') self.assertEqual( self.transport.value(), b'AUTHENTICATE ' + b2a_hex(cookie_data) + b'\r\n', ) def test_authenticate_password(self): self.protocol.password_function = lambda: 'foo' self.protocol.makeConnection(self.transport) self.assertEqual(self.transport.value(), b'PROTOCOLINFO 1\r\n') self.transport.clear() self.send(b'250-PROTOCOLINFO 1') self.send(b'250-AUTH METHODS=HASHEDPASSWORD') self.send(b'250-VERSION Tor="0.2.2.34"') self.send(b'250 OK') self.assertEqual( self.transport.value(), b'AUTHENTICATE ' + b2a_hex(b'foo') + b'\r\n' ) def test_authenticate_password_not_bytes(self): self.protocol.password_function = lambda: u'foo' self.protocol.makeConnection(self.transport) self.assertEqual(self.transport.value(), b'PROTOCOLINFO 1\r\n') self.transport.clear() self.send(b'250-PROTOCOLINFO 1') self.send(b'250-AUTH METHODS=HASHEDPASSWORD') self.send(b'250-VERSION Tor="0.2.2.34"') self.send(b'250 OK') self.assertEqual( self.transport.value(), b'AUTHENTICATE ' + b2a_hex(b'foo') + b'\r\n' ) def test_authenticate_null(self): self.protocol.makeConnection(self.transport) self.assertEqual(self.transport.value(), b'PROTOCOLINFO 1\r\n') self.transport.clear() self.send(b'250-PROTOCOLINFO 1') self.send(b'250-AUTH METHODS=NULL') self.send(b'250-VERSION Tor="0.2.2.34"') self.send(b'250 OK') self.assertEqual(self.transport.value(), b'AUTHENTICATE\r\n') 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(), b'PROTOCOLINFO 1\r\n') self.transport.clear() self.send(b'250-PROTOCOLINFO 1') self.send(b'250-AUTH METHODS=HASHEDPASSWORD') self.send(b'250-VERSION Tor="0.2.2.34"') self.send(b'250 OK') # make sure we haven't tried to authenticate before getting # the password callback self.assertEqual(self.transport.value(), b'') d.callback('foo') # now make sure we DID try to authenticate self.assertEqual( self.transport.value(), b'AUTHENTICATE ' + b2a_hex(b"foo") + b'\r\n' ) 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(), b'PROTOCOLINFO 1\r\n') self.transport.clear() self.send(b'250-PROTOCOLINFO 1') self.send(b'250-AUTH METHODS=HASHEDPASSWORD') self.send(b'250-VERSION Tor="0.2.2.34"') self.send(b'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(), b'PROTOCOLINFO 1\r\n') self.send(b'250-PROTOCOLINFO 1') self.send(b'250-AUTH METHODS=HASHEDPASSWORD') self.send(b'250-VERSION Tor="0.2.2.34"') self.send(b'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): assert type(line) == bytes self.protocol.dataReceived(line.strip() + b"\r\n") def test_statemachine_broadcast_no_code(self): try: self.protocol._broadcast_response("foo") self.fail() except RuntimeError as 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 as 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 as 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 as e: self.assertTrue('Unexpected code' in str(e)) def test_response_with_no_request(self): with self.assertRaises(RuntimeError) as ctx: self.protocol.code = 200 self.protocol._broadcast_response('200 OK') self.assertTrue( "didn't issue a command" in str(ctx.exception) ) 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(b'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 as e: self.assertTrue('find AUTH line' in str(e)) def test_authenticate_not_enough_cookie_data(self): with tempfile.NamedTemporaryFile() as cookietmp: cookietmp.write(b'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 as 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(b'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 as e: self.assertTrue('cookie to be 32' in str(e)) def test_authenticate_safecookie(self): with tempfile.NamedTemporaryFile() as cookietmp: cookiedata = bytes(bytearray([0] * 32)) cookietmp.write(cookiedata) cookietmp.flush() self.protocol._do_authenticate('''PROTOCOLINFO 1 AUTH METHODS=SAFECOOKIE COOKIEFILE="{}" VERSION Tor="0.2.2.35" OK'''.format(cookietmp.name)) self.assertTrue( b'AUTHCHALLENGE SAFECOOKIE ' in self.transport.value() ) x = self.transport.value().split()[-1] client_nonce = a2b_hex(x) self.transport.clear() server_nonce = bytes(bytearray([0] * 32)) server_hash = hmac_sha256( b"Tor safe cookie authentication server-to-controller hash", cookiedata + client_nonce + server_nonce, ) self.send( b'250 AUTHCHALLENGE SERVERHASH=' + base64.b16encode(server_hash) + b' SERVERNONCE=' + base64.b16encode(server_nonce) + b'\r\n' ) self.assertTrue(b'AUTHENTICATE ' in self.transport.value()) def test_authenticate_cookie_without_reading(self): server_nonce = bytes(bytearray([0] * 32)) server_hash = bytes(bytearray([0] * 32)) try: self.protocol._safecookie_authchallenge( '250 AUTHCHALLENGE SERVERHASH=%s SERVERNONCE=%s' % (base64.b16encode(server_hash), base64.b16encode(server_nonce)) ) self.assertTrue(False) except RuntimeError as e: self.assertTrue('not read' in str(e)) def test_authenticate_unexisting_cookie_file(self): unexisting_file = __file__ + "-unexisting" try: self.protocol._do_authenticate('''PROTOCOLINFO 1 AUTH METHODS=COOKIE COOKIEFILE="%s" VERSION Tor="0.2.2.35" OK''' % unexisting_file) self.assertTrue(False) except RuntimeError: pass def test_authenticate_unexisting_safecookie_file(self): unexisting_file = __file__ + "-unexisting" try: self.protocol._do_authenticate('''PROTOCOLINFO 1 AUTH METHODS=SAFECOOKIE COOKIEFILE="{}" VERSION Tor="0.2.2.35" OK'''.format(unexisting_file)) self.assertTrue(False) except RuntimeError: pass def test_authenticate_dont_send_cookiefile(self): try: self.protocol._do_authenticate('''PROTOCOLINFO 1 AUTH METHODS=SAFECOOKIE VERSION Tor="0.2.2.35" OK''') self.assertTrue(False) except RuntimeError: pass def test_authenticate_password_when_cookie_unavailable(self): unexisting_file = __file__ + "-unexisting" self.protocol.password_function = lambda: 'foo' self.protocol._do_authenticate('''PROTOCOLINFO 1 AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="{}" VERSION Tor="0.2.2.35" OK'''.format(unexisting_file)) self.assertEqual( self.transport.value(), b'AUTHENTICATE ' + b2a_hex(b'foo') + b'\r\n', ) def test_authenticate_password_when_safecookie_unavailable(self): unexisting_file = __file__ + "-unexisting" self.protocol.password_function = lambda: 'foo' self.protocol._do_authenticate('''PROTOCOLINFO 1 AUTH METHODS=SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="{}" VERSION Tor="0.2.2.35" OK'''.format(unexisting_file)) self.assertEqual( self.transport.value(), b'AUTHENTICATE ' + b2a_hex(b'foo') + b'\r\n', ) def test_authenticate_safecookie_wrong_hash(self): cookiedata = bytes(bytearray([0] * 32)) server_nonce = bytes(bytearray([0] * 32)) server_hash = bytes(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={} SERVERNONCE={}'.format( b2a_hex(server_hash).decode('ascii'), b2a_hex(server_nonce).decode('ascii'), ) ) self.assertTrue(False) except RuntimeError as 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 = b'GUARD STREAM CIRC NS NEWCONSENSUS ORCONN NEWDESC ADDRMAP STATUS_GENERAL' self.protocol._bootstrap() # answer all the requests generated by boostrapping etc. self.send(b"250-signal/names=") self.send(b"250 OK") self.send(b"250-version=foo") self.send(b"250 OK") self.send(b"250-events/names=" + events) self.send(b"250 OK") self.send(b"250 OK") # for USEFEATURE return d def test_bootstrap_tor_does_not_support_signal_names(self): self.protocol._bootstrap() self.send(b'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(b"250 OK") d = self.protocol.get_conf("SOCKSPORT ORPORT") self.send(b"650 CIRC 1000 EXTENDED moria1,moria2") self.send(b"250-SOCKSPORT=9050") self.send(b"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(b"250 OK") d = self.protocol.get_conf("SOCKSPORT ORPORT") d.addCallback(CallbackChecker({"ORPORT": "0", "SOCKSPORT": "9050"})) self.send(b"650-CIRC 1000 EXTENDED moria1,moria2") self.send(b"650-EXTRAMAGIC=99") self.send(b"650 ANONYMITY=high") self.send(b"250-SOCKSPORT=9050") self.send(b"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(b"250+FOO=") self.send(b"a") self.send(b"b") self.send(b"c") self.send(b".") self.send(b"250 OK") return d def test_multiline_plus_embedded_equals(self): """ """ d = self.protocol.get_info("FOO") d.addCallback(CallbackChecker({"FOO": "\na="})) self.send(b"250+FOO=") self.send(b"a=") self.send(b".") self.send(b"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(b"250+FOO=") self.send(b"bar") self.send(b"bar") self.send(b".") self.send(b"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(b"250-FOO=") self.send(b"250-bar") self.send(b"250-bar") self.send(b"250 OK") return d def test_getinfo_one_line(self): d = self.protocol.get_info( "foo", ) self.send(b'250 foo=bar') d.addCallback(lambda _: functools.partial(self.incremental_check, "bar")) return d def test_getconf(self): d = self.protocol.get_conf("SOCKSPORT ORPORT") d.addCallback(CallbackChecker({'SocksPort': '9050', 'ORPort': '0'})) self.send(b"250-SocksPort=9050") self.send(b"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(b"250-SocksPort=9050") self.send(b"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(b"250 OK") self._wait(d) self.assertEqual(self.transport.value(), b"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(b"250 OK") self._wait(d) self.assertEqual( self.transport.value(), b'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(b"250 OK") self._wait(d) self.assertEqual( self.transport.value(), b"SETCONF foo=bar baz=1\r\n", ) def test_quit(self): d = self.protocol.quit() self.send(b"250 OK") self._wait(d) self.assertEqual( self.transport.value(), b"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(b"250-a=one") self.send(b"250-b=two") self.send(b"250 OK") self.send(b"250 bar") return d2 def test_signal_error(self): try: self.protocol.signal('FOO') self.fail() except Exception as 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(), b'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(b"250 OK") self.send(b"650-CONF_CHANGED") self.send(b"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(b"250 OK") d = self.protocol.get_info("a") d.addCallback(CallbackChecker({'a': 'one'})).addErrback(self.fail) self.send(b"250-a=one") self.send(b"250 OK") self.send(b"650 CIRC 1000 EXTENDED moria1,moria2") return d def test_notify_error(self): self.protocol._set_valid_events('CIRC') self.send(b"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(b"250-version=0.2.2.34") self.send(b"250 OK") self.assertEqual( self.transport.value(), b"GETINFO version\r\n", ) return d def test_getinfo_for_descriptor(self): descriptor_info = b"""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.decode('ascii').split('\n')[1:-2])})) d.addErrback(self.fail) for line in descriptor_info.split(b'\n'): self.send(line) return d def test_getinfo_multiline(self): descriptor_info = b"""250+desc/name/moria1= router moria1 128.31.0.34 9101 0 9131 platform Tor 0.2.5.0-alpha-dev on Linux . 250 OK""" d = self.protocol.get_info("desc/name/moria1") gold = "\nrouter moria1 128.31.0.34 9101 0 9131\nplatform Tor 0.2.5.0-alpha-dev on Linux" d.addCallback(CallbackChecker({'desc/name/moria1': gold})) d.addErrback(self.fail) for line in descriptor_info.split(b'\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(b"250 OK") self._wait(d) self.assertEqual( self.transport.value().split(b'\r\n')[-2], b"SETEVENTS FOO" ) self.transport.clear() self.protocol.add_event_listener('BAR', lambda _: None) d = self.protocol.defer self.send(b"250 OK") self.assertTrue( self.transport.value() == b"SETEVENTS FOO BAR\r\n" or self.transport.value() == b"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(b"250 OK") self._wait(d) self.send(b"650 STREAM 1234 NEW 4321 1.2.3.4:555 REASON=MISC") self.send(b"650 STREAM 2345 NEW 4321 2.3.4.5:666 REASON=MISC") self.assertEqual(listener.stream_events, 2) def test_eventlistener_error(self): self.protocol._set_valid_events('STREAM') class EventListener(object): stream_events = 0 do_error = False def __call__(self, data): self.stream_events += 1 if self.do_error: raise Exception("the bad thing happened") # we make sure the first listener has the errors to prove the # second one still gets called. listener0 = EventListener() listener0.do_error = True listener1 = EventListener() self.protocol.add_event_listener('STREAM', listener0) self.protocol.add_event_listener('STREAM', listener1) d = self.protocol.defer self.send(b"250 OK") self._wait(d) self.send(b"650 STREAM 1234 NEW 4321 1.2.3.4:555 REASON=MISC") self.send(b"650 STREAM 2345 NEW 4321 2.3.4.5:666 REASON=MISC") self.assertEqual(listener0.stream_events, 2) self.assertEqual(listener1.stream_events, 2) # should have logged the two errors logged = self.flushLoggedErrors() self.assertEqual(2, len(logged)) self.assertTrue("the bad thing happened" in str(logged[0])) self.assertTrue("the bad thing happened" in str(logged[1])) 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(), b'SETEVENTS STREAM\r\n') self.protocol.lineReceived(b"250 OK") self.transport.clear() self.protocol.remove_event_listener('STREAM', listener) self.assertEqual(self.transport.value(), b'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(), b'SETEVENTS STREAM\r\n') self.protocol.lineReceived(b"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(), b'') # remove one, should still not issue a tor command self.protocol.remove_event_listener('STREAM', listener0) self.assertEqual(self.transport.value(), b'') # remove the other one, NOW should issue a command self.protocol.remove_event_listener('STREAM', listener1) self.assertEqual(self.transport.value(), b'SETEVENTS \r\n') # try removing invalid event try: self.protocol.remove_event_listener('FOO', listener0) self.fail() except Exception as e: self.assertTrue('FOO' in str(e)) def test_continuation_line(self): d = self.protocol.get_info_raw("key") def check_continuation(v): self.assertEqual(v, "key=\nvalue0\nvalue1") d.addCallback(check_continuation) self.send(b"250+key=") self.send(b"value0") self.send(b"value1") self.send(b".") self.send(b"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(b"250+ns/id/624926802351575FF7E4E3D60EFA3BFB56E67E8A=") self.send(b"r fake YkkmgCNRV1/35OPWDvo7+1bmfoo tanLV/4ZfzpYQW0xtGFqAa46foo 2011-12-12 16:29:16 12.45.56.78 443 80") self.send(b"s Exit Fast Guard HSDir Named Running Stable V2Dir Valid") self.send(b"w Bandwidth=518000") self.send(b"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(b".") self.send(b"250 OK") return d def test_plus_line_no_command(self): self.protocol.lineReceived(b"650+NS\r\n") self.protocol.lineReceived(b"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(b"650-NS\r\n") self.protocol.lineReceived(b"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'], list)) 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'], list)) 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'], list)) 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 = sorted(x.keys()) 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.19.3/test/test_addrmap.py0000644000175000017500000001611613100171552017360 0ustar mikemike00000000000000import datetime from twisted.trial import unittest from twisted.internet import task from twisted.internet.interfaces import IReactorTime from zope.interface import implementer from txtorcon.addrmap import AddrMap from txtorcon.interface import IAddrListener @implementer(IAddrListener) class AddrMapTests(unittest.TestCase): 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.19.3/test/test_util_imports.py0000644000175000017500000000262513106645477020524 0ustar mikemike00000000000000from twisted.trial import unittest import sys import functools from unittest import skipUnless import six def fake_import(orig, name, *args, **kw): if name in ['GeoIP']: raise ImportError('testing!') return orig(*((name,) + args), **kw) class TestImports(unittest.TestCase): @skipUnless(six.PY2 and 'pypy' not in sys.version.lower(), "Doesn't work in PYPY, Py3") 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 loc = txtorcon.util.NetLocation('127.0.0.1') self.assertEqual(loc.city, None) self.assertEqual(loc.asn, None) self.assertEqual(loc.countrycode, '') finally: __import__ = orig txtorcon-0.19.3/test/test_stream.py0000644000175000017500000003551713106645477017273 0ustar mikemike00000000000000from __future__ import print_function from txtorcon.util import maybe_ip_addr from twisted.trial import unittest from twisted.internet import defer from zope.interface import implementer from txtorcon import Stream from txtorcon import IStreamListener from txtorcon import ICircuitContainer from txtorcon import StreamListenerMixin from txtorcon import AddrMap class FakeCircuit: def __init__(self, id=-999): self.streams = [] self.id = id @implementer(IStreamListener) class Listener(object): 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) @implementer(ICircuitContainer) class StreamTests(unittest.TestCase): 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_listener_exception(self): """A listener throws an exception during notify""" exc = Exception("the bad stuff happened") class Bad(StreamListenerMixin): def stream_new(*args, **kw): raise exc listener = Bad() 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()) errors = self.flushLoggedErrors() self.assertEqual(1, len(errors)) self.assertEqual(errors[0].value, exc) def test_stream_addrmap_remap(self): addrmap = AddrMap() addrmap.update('meejah.ca 1.2.3.4 never') stream = Stream(self, addrmap) stream.update("1604 NEW 0 1.2.3.4:0 PURPOSE=DNS_REQUEST".split()) self.assertEqual(stream.target_host, "meejah.ca") 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 as 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 as 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.19.3/test/__init__.py0000644000175000017500000000000012777316067016456 0ustar mikemike00000000000000txtorcon-0.19.3/test/verify-release.py0000644000175000017500000000350513076556022017644 0ustar mikemike00000000000000# this does the "download and verify release from web + hidden-service # machine" for txtorcon release-checklist. import sys import hashlib from os.path import join, split, exists import txtorcon from twisted.internet import defer, task from twisted.web.client import readBody from twisted.python.failure import Failure @task.react @defer.inlineCallbacks def main(reactor): if len(sys.argv) != 2: print('usage: {} '.format(__file__)) raise SystemExit(1) version = sys.argv[1] announce = join(split(__file__)[0], '..', 'release-announce-{}'.format(version)) if not exists(announce): print('no announcement file: {}'.format(announce)) raise SystemExit(2) sums = None with open(announce, 'r') as f: for line in f.readlines(): if line.strip() == 'cat < callback self.pending_events = {} #: event type -> list self.is_owned = -1 self.commands = [] self.version = "0.2.8.0" def queue_command(self, cmd): d = defer.Deferred() self.commands.append((cmd, d)) return d 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.events: self.events[event_type](*args) elif 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): 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_create(self): @implementer(ITorControlProtocol) class FakeProtocol(object): post_bootstrap = defer.succeed(None) def add_event_listener(*args, **kw): pass def get_info_raw(*args, **kw): return defer.succeed('config/names=') TorConfig.from_protocol(FakeProtocol()) 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_save_boolean(self): self.protocol.answers.append('config/names=\nfoo Boolean\nbar Boolean') self.protocol.answers.append({'foo': '0'}) self.protocol.answers.append({'bar': '1'}) conf = TorConfig(self.protocol) # save some boolean value conf.foo = True conf.bar = False conf.save() self.assertEqual(set(self.protocol.sets), set([('foo', 1), ('bar', 0)])) def test_read_boolean_after_save(self): self.protocol.answers.append('config/names=\nfoo Boolean\nbar Boolean') self.protocol.answers.append({'foo': '0'}) self.protocol.answers.append({'bar': '1'}) conf = TorConfig(self.protocol) # save some boolean value conf.foo = True conf.bar = False conf.save() self.assertTrue(conf.foo is True, msg="foo not True: %s" % conf.foo) self.assertTrue(conf.bar is False, msg="bar not False: %s" % conf.bar) def test_save_boolean_with_strange_values(self): self.protocol.answers.append('config/names=\nfoo Boolean\nbar Boolean') self.protocol.answers.append({'foo': '0'}) self.protocol.answers.append({'bar': '1'}) conf = TorConfig(self.protocol) # save some non-boolean value conf.foo = "Something True" conf.bar = 0 conf.save() self.assertEqual(set(self.protocol.sets), set([('foo', 1), ('bar', 0)])) 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_save_boolean_auto(self): self.protocol.answers.append( 'config/names=\nfoo Boolean+Auto\nbar Boolean+Auto\nbaz Boolean+Auto\nqux Boolean+Auto' ) self.protocol.answers.append({'foo': '1'}) self.protocol.answers.append({'bar': '1'}) self.protocol.answers.append({'baz': '1'}) self.protocol.answers.append({'qux': '1'}) conf = TorConfig(self.protocol) conf.foo = 1 conf.bar = 0 conf.baz = True conf.qux = -1 conf.save() self.assertEqual(set(self.protocol.sets), set([('foo', 1), ('bar', 0), ('baz', 1), ('qux', 'auto')])) self.assertTrue(conf.foo is 1) self.assertTrue(conf.bar is 0) self.assertTrue(conf.baz is 1) self.assertTrue(conf.qux is -1) def test_save_invalid_boolean_auto(self): self.protocol.answers.append( 'config/names=\nfoo Boolean+Auto' ) self.protocol.answers.append({'foo': '1'}) conf = TorConfig(self.protocol) for value in ('auto', 'True', 'False', None): try: conf.foo = value except (ValueError, TypeError): pass else: self.fail("Invalid value '%s' allowed" % value) conf.save() self.assertEqual(self.protocol.sets, []) 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_validator(self): self.protocol.answers.append('config/names=\nfoo Integer') self.protocol.answers.append({'foo': '123'}) conf = TorConfig(self.protocol) conf.foo = 2.33 conf.save() self.assertEqual(conf.foo, 2) conf.foo = '1' conf.save() self.assertEqual(conf.foo, 1) conf.foo = '-100' conf.save() self.assertEqual(conf.foo, -100) conf.foo = 0 conf.save() self.assertEqual(conf.foo, 0) conf.foo = '0' conf.save() self.assertEqual(conf.foo, 0) for value in ('no', 'Not a value', None): try: conf.foo = value except (ValueError, TypeError): pass else: self.fail("No excpetion thrown") 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 as 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 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 as 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) def test_conf_changed_parsed(self): ''' Create a configuration which holds boolean types. These types have to be parsed as booleans. ''' protocol = FakeControlProtocol([]) protocol.answers.append('config/names=\nFoo Boolean\nBar Boolean') protocol.answers.append({'Foo': '0'}) protocol.answers.append({'Bar': '1'}) config = TorConfig(protocol) # Initial value is not tested here protocol.events['CONF_CHANGED']('Foo=1\nBar=0') msg = "Foo is not True: %r" % config.Foo self.assertTrue(config.Foo is True, msg=msg) msg = "Foo is not False: %r" % config.Bar self.assertTrue(config.Bar is False, msg=msg) def test_conf_changed_invalid_values(self): protocol = FakeControlProtocol([]) protocol.answers.append('config/names=\nFoo Integer\nBar Integer') protocol.answers.append({'Foo': '0'}) protocol.answers.append({'Bar': '1'}) # Doing It For The Side Effects. Hoo boy. TorConfig(protocol) # Initial value is not tested here try: protocol.events['CONF_CHANGED']('Foo=INVALID\nBar=VALUES') except (ValueError, TypeError): pass else: self.fail("No excpetion thrown") 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 SocksEndpointTests(unittest.TestCase): def setUp(self): self.reactor = Mock() self.config = TorConfig() self.config.SocksPort = [] def test_nothing_configurd(self): with self.assertRaises(Exception) as ctx: self.config.socks_endpoint(self.reactor, '1234') self.assertTrue('No SOCKS ports configured' in str(ctx.exception)) def test_default(self): self.config.SocksPort = ['1234', '4321'] ep = self.config.socks_endpoint(self.reactor) factory = Mock() ep.connect(factory) self.assertEqual(1, len(self.reactor.mock_calls)) call = self.reactor.mock_calls[0] self.assertEqual('connectTCP', call[0]) self.assertEqual('127.0.0.1', call[1][0]) self.assertEqual(1234, call[1][1]) def test_explicit_host(self): self.config.SocksPort = ['127.0.0.20:1234'] ep = self.config.socks_endpoint(self.reactor) factory = Mock() ep.connect(factory) self.assertEqual(1, len(self.reactor.mock_calls)) call = self.reactor.mock_calls[0] self.assertEqual('connectTCP', call[0]) self.assertEqual('127.0.0.20', call[1][0]) self.assertEqual(1234, call[1][1]) def test_something_not_configured(self): self.config.SocksPort = ['1234', '4321'] with self.assertRaises(Exception) as ctx: self.config.socks_endpoint(self.reactor, '1111') self.assertTrue('No SOCKSPort configured' in str(ctx.exception)) def test_unix_socks(self): self.config.SocksPort = ['unix:/foo'] self.config.socks_endpoint(self.reactor, 'unix:/foo') def test_with_options(self): self.config.SocksPort = ['9150 IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth'] ep = self.config.socks_endpoint(self.reactor, 9150) factory = Mock() ep.connect(factory) self.assertEqual(1, len(self.reactor.mock_calls)) call = self.reactor.mock_calls[0] self.assertEqual('connectTCP', call[0]) self.assertEqual('127.0.0.1', call[1][0]) self.assertEqual(9150, call[1][1]) def test_with_options_in_ask(self): self.config.SocksPort = ['9150 IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth'] with self.assertRaises(Exception) as ctx: self.config.socks_endpoint(self.reactor, '9150 KeepAliveIsolateSOCKSAuth') self.assertTrue("Can't specify options" in str(ctx.exception)) class CreateSocksEndpointTests(unittest.TestCase): def setUp(self): self.reactor = Mock() self.config = TorConfig() self.config.SocksPort = [] self.config.bootstrap = defer.succeed(self.config) @defer.inlineCallbacks def test_create_default_no_ports(self): with self.assertRaises(Exception) as ctx: yield self.config.create_socks_endpoint(self.reactor, None) self.assertTrue('no SocksPorts configured' in str(ctx.exception)) @defer.inlineCallbacks def test_create_default(self): self.config.SocksPort = ['9150'] ep = yield self.config.create_socks_endpoint(self.reactor, None) factory = Mock() ep.connect(factory) self.assertEqual(1, len(self.reactor.mock_calls)) call = self.reactor.mock_calls[0] self.assertEqual('connectTCP', call[0]) self.assertEqual('127.0.0.1', call[1][0]) self.assertEqual(9150, call[1][1]) @defer.inlineCallbacks def test_create_tcp(self): ep = yield self.config.create_socks_endpoint( self.reactor, "9050", ) factory = Mock() ep.connect(factory) self.assertEqual(1, len(self.reactor.mock_calls)) call = self.reactor.mock_calls[0] self.assertEqual('connectTCP', call[0]) self.assertEqual('127.0.0.1', call[1][0]) self.assertEqual(9050, call[1][1]) @defer.inlineCallbacks def test_create_error_on_save(self): self.config.SocksPort = [] def boom(*args, **kw): raise TorProtocolError(551, "Something bad happened") with patch.object(TorConfig, 'save', boom): with self.assertRaises(Exception) as ctx: yield self.config.create_socks_endpoint(self.reactor, 'unix:/foo') err = str(ctx.exception) self.assertTrue('error from Tor' in err) self.assertTrue('specific ownership/permissions requirements' in err) 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_single_client(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('gobledegook\n') conf = TorConfig(self.protocol) hs = HiddenService(conf, d, []) self.assertEqual(1, len(hs.clients)) self.assertEqual('default', hs.clients[0][0]) self.assertEqual('gobledegook', hs.clients[0][1]) finally: shutil.rmtree(d, ignore_errors=True) def test_stealth_clients(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('oniona cookiea\n') f.write('onionb cookieb\n') conf = TorConfig(self.protocol) hs = HiddenService(conf, d, []) self.assertEqual(2, len(hs.clients)) self.assertEqual('oniona', hs.clients[0][0]) self.assertEqual('cookiea', hs.clients[0][1]) self.assertEqual('onionb', hs.clients[1][0]) self.assertEqual('cookieb', hs.clients[1][1]) self.assertRaises(RuntimeError, getattr, hs, 'hostname') 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 as 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 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 LegacyLaunchTorTests(unittest.TestCase): """ Test backwards-compatibility on launch_tor() """ @patch('txtorcon.controller.find_tor_binary', return_value=None) @patch('twisted.python.deprecate.warn') @defer.inlineCallbacks def test_happy_path(self, warn, ftb): self.transport = proto_helpers.StringTransport() 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([]) trans = Mock() trans.protocol = self.protocol creator = functools.partial(Connector(), self.protocol, self.transport) reactor = Mock() config = Mock() fake_tor = Mock() fake_tor.process = TorProcessProtocol(creator) with patch('txtorcon.controller.launch', return_value=fake_tor) as launch: directlyProvides(reactor, IReactorCore) tpp = yield launch_tor( config, reactor, connection_creator=creator ) self.assertEqual(1, len(launch.mock_calls)) self.assertTrue( isinstance(tpp, TorProcessProtocol) ) self.assertIs(tpp, fake_tor.process) calls = warn.mock_calls self.assertEqual(1, len(calls)) self.assertEqual(calls[0][1][1], DeprecationWarning) class ErrorTests(unittest.TestCase): @patch('txtorcon.controller.find_tor_binary', return_value=None) @defer.inlineCallbacks def test_no_tor_binary(self, ftb): self.transport = proto_helpers.StringTransport() 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([]) trans = Mock() trans.protocol = self.protocol creator = functools.partial(Connector(), self.protocol, self.transport) reactor = Mock() directlyProvides(reactor, IReactorCore) try: yield launch( reactor, connection_creator=creator ) self.fail() except TorNotFound: pass # success! # 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('RSA1024: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, 'RSA1024: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 ) class EphemeralHiddenServiceTest(unittest.TestCase): def test_defaults(self): eph = torconfig.EphemeralHiddenService("80 localhost:80") self.assertEqual(eph._ports, ["80,localhost:80"]) def test_wrong_blob(self): wrong_blobs = ["", " ", "foo", ":", " : ", "foo:", ":foo", 0] for b in wrong_blobs: try: torconfig.EphemeralHiddenService("80 localhost:80", b) self.fail("should get exception") except ValueError: pass def test_add(self): eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80") proto = Mock() proto.queue_command = Mock(return_value="PrivateKey=blam\nServiceID=ohai") eph.add_to_tor(proto) self.assertEqual("blam", eph.private_key) self.assertEqual("ohai.onion", eph.hostname) def test_add_keyblob(self): eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80", "alg:blam") proto = Mock() proto.queue_command = Mock(return_value="ServiceID=ohai") eph.add_to_tor(proto) self.assertEqual("alg:blam", eph.private_key) self.assertEqual("ohai.onion", eph.hostname) def test_descriptor_wait(self): eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80") proto = Mock() proto.queue_command = Mock(return_value=defer.succeed("PrivateKey=blam\nServiceID=ohai\n")) eph.add_to_tor(proto) # get the event-listener callback that torconfig code added; # the last call [-1] was to add_event_listener; we want the # [1] arg of that cb = proto.method_calls[-1][1][1] # Tor doesn't actually provide the .onion, but we can test it anyway cb('UPLOADED ohai UNKNOWN somehsdir') cb('UPLOADED UNKNOWN UNKNOWN somehsdir') self.assertEqual("blam", eph.private_key) self.assertEqual("ohai.onion", eph.hostname) def test_remove(self): eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80") eph.hostname = 'foo.onion' proto = Mock() proto.queue_command = Mock(return_value="OK") eph.remove_from_tor(proto) @defer.inlineCallbacks def test_remove_error(self): eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80") eph.hostname = 'foo.onion' proto = Mock() proto.queue_command = Mock(return_value="it's not ok") try: yield eph.remove_from_tor(proto) self.fail("should have gotten exception") except RuntimeError: pass def test_failed_upload(self): eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80") proto = Mock() proto.queue_command = Mock(return_value=defer.succeed("PrivateKey=seekrit\nServiceID=42\n")) d = eph.add_to_tor(proto) # get the event-listener callback that torconfig code added; # the last call [-1] was to add_event_listener; we want the # [1] arg of that cb = proto.method_calls[-1][1][1] # Tor leads with UPLOAD events for each attempt; we queue 2 of # these... cb('UPLOAD 42 UNKNOWN hsdir0') cb('UPLOAD 42 UNKNOWN hsdir1') # ...but fail them both cb('FAILED 42 UNKNOWN hsdir1 REASON=UPLOAD_REJECTED') cb('FAILED 42 UNKNOWN hsdir0 REASON=UPLOAD_REJECTED') self.assertEqual("seekrit", eph.private_key) self.assertEqual("42.onion", eph.hostname) self.assertTrue(d.called) d.addErrback(lambda e: self.assertTrue('Failed to upload' in str(e))) def test_single_failed_upload(self): eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80") proto = Mock() proto.queue_command = Mock(return_value=defer.succeed("PrivateKey=seekrit\nServiceID=42\n")) d = eph.add_to_tor(proto) # get the event-listener callback that torconfig code added; # the last call [-1] was to add_event_listener; we want the # [1] arg of that cb = proto.method_calls[-1][1][1] # Tor leads with UPLOAD events for each attempt; we queue 2 of # these... cb('UPLOAD 42 UNKNOWN hsdir0') cb('UPLOAD 42 UNKNOWN hsdir1') # ...then fail one cb('FAILED 42 UNKNOWN hsdir1 REASON=UPLOAD_REJECTED') # ...and succeed on the last. cb('UPLOADED 42 UNKNOWN hsdir0') self.assertEqual("seekrit", eph.private_key) self.assertEqual("42.onion", eph.hostname) self.assertTrue(d.called) txtorcon-0.19.3/requirements.txt0000644000175000017500000000032013106645477016654 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[tls]>=15.5.0 ipaddress>=1.0.16 zope.interface>=3.6.1 incremental automat txtorcon-0.19.3/Makefile0000644000175000017500000001023613111226072015015 0ustar mikemike00000000000000.PHONY: test html counts coverage sdist clean install doc integration diagrams default: test VERSION = 0.19.3 test: PYTHONPATH=. trial --reporter=text test tox: tox -i http://localhost:3141/root/pypi diagrams: automat-visualize --image-directory ./diagrams --image-type png txtorcon diagrams: automat-visualize --image-directory ./diagrams --image-type png txtorcon # 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 # see also http://docs.docker.io/en/latest/use/baseimages/ dockerbase-jessie: @echo 'Building a minimal "jessie" system.' @echo "This may take a while...and will consume about 240MB when done." debootstrap jessie dockerbase-jessie dockerbase-jessie-image: dockerbase-jessie @echo 'Importing dockerbase-jessie into docker' tar -C dockerbase-jessie -c . | docker import - dockerbase-jessie docker run dockerbase-jessie cat /etc/issue txtorcon-tester: Dockerfile dockerbase-jessie-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-ipaddress graphviz python setup.py install doc: docs/*.rst cd docs && make html -cp dist/txtorcon-${VERSION}.tar.gz docs/_build/html coverage: PYTHONPATH=. coverage run --source=txtorcon `which trial` test cuv graph htmlcoverage: coverage run --source=txtorcon `which trial` test 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 twisted/plugins/dropin.cache -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-jessie -docker rmi txtorcon-tester -docker rmi dockerbase-jessie counts: ohcount -s txtorcon/*.py test-release: dist ./scripts/test-release.sh $(shell pwd) ${VERSION} dist: dist/txtorcon-${VERSION}-py2.py3-none-any.whl dist/txtorcon-${VERSION}.tar.gz dist-sigs: dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc dist/txtorcon-${VERSION}.tar.gz.asc sdist: setup.py python setup.py sdist dist/txtorcon-${VERSION}-py2.py3-none-any.whl: python setup.py bdist_wheel --universal dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc: dist/txtorcon-${VERSION}-py2.py3-none-any.whl gpg --verify dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc || gpg --no-version --detach-sign --armor --local-user meejah@meejah.ca dist/txtorcon-${VERSION}-py2.py3-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.py3-none-any.whl dist/txtorcon-${VERSION}-py2.py3-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/*.rst cd docs && make html txtorcon-0.19.3/LICENSE0000644000175000017500000000204012752747562014402 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.19.3/meejah.asc0000644000175000017500000000323012312757205015303 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.19.3/TODO0000644000175000017500000001262412312757205014061 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.19.3/txtorcon.egg-info/0000755000175000017500000000000013111226470016727 5ustar mikemike00000000000000txtorcon-0.19.3/txtorcon.egg-info/SOURCES.txt0000644000175000017500000000647713111226470020631 0ustar mikemike00000000000000INSTALL LICENSE MANIFEST.in Makefile README.rst TODO dev-requirements.txt meejah.asc requirements.txt setup.py docs/Makefile docs/apilinks_sphinxext.py docs/conf.py docs/examples.rst docs/guide.rst docs/hacking.rst docs/index.rst docs/installing.rst docs/introduction.rst docs/release-checklist.rst docs/releases.rst docs/txtorcon-config.rst docs/txtorcon-controller.rst docs/txtorcon-endpoints.rst docs/txtorcon-interface.rst docs/txtorcon-protocol.rst docs/txtorcon-socks.rst docs/txtorcon-state.rst docs/txtorcon-util.rst docs/txtorcon.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/add_hiddenservice_to_system_tor.py.orig examples/attach_streams_by_country.py.orig examples/circuit_failure_rates.py.orig examples/circuit_for_next_stream.py.orig examples/connect.py examples/connect.py.orig examples/disallow_streams_by_port.py examples/dns_lookups.py examples/dns_lookups.py.orig examples/dump_config.py.orig examples/hello_darkweb.py.orig examples/hidden_echo.py examples/hidden_echo.py.orig examples/launch_tor.py examples/launch_tor.py.orig examples/launch_tor2web.py examples/launch_tor_endpoint.py examples/launch_tor_endpoint.py.orig examples/launch_tor_endpoint2.py examples/launch_tor_endpoint2.py.orig examples/launch_tor_unix_sockets.py examples/launch_tor_with_hiddenservice.py.orig examples/launch_tor_with_simplehttpd.py examples/minimal_endpoint.py examples/monitor.py examples/readme.py examples/readme3.py examples/stem_relay_descriptor.py examples/stream_circuit_logger.py examples/tor_info.py examples/txtorcon.tac examples/web_client.py examples/web_client.py.orig examples/web_client_custom_circuit.py examples/web_client_custom_circuit.py.orig examples/web_client_treq.py examples/web_client_treq.py.orig examples/webui_server.py scripts/asciinema-demo0.py scripts/asciinema-demo1.py test/__init__.py test/profile_startup.py test/py3_torstate.py test/test_addrmap.py test/test_attacher.py test/test_circuit.py test/test_controller.py test/test_endpoints.py test/test_fsm.py test/test_log.py test/test_microdesc.py test/test_router.py test/test_socks.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/test_web.py test/util.py test/verify-release.py twisted/plugins/txtorcon_endpoint_parser.py txtorcon/__init__.py txtorcon/_metadata.py txtorcon/_microdesc_parser.py txtorcon/addrmap.py txtorcon/attacher.py txtorcon/circuit.py txtorcon/controller.py txtorcon/endpoints.py txtorcon/interface.py txtorcon/log.py txtorcon/router.py txtorcon/socks.py txtorcon/spaghetti.py txtorcon/stream.py txtorcon/torconfig.py txtorcon/torcontrolprotocol.py txtorcon/torinfo.py txtorcon/torstate.py txtorcon/util.py txtorcon/web.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.19.3/txtorcon.egg-info/top_level.txt0000644000175000017500000000002613111226470021457 0ustar mikemike00000000000000test twisted txtorcon txtorcon-0.19.3/txtorcon.egg-info/PKG-INFO0000644000175000017500000001560113111226470020027 0ustar mikemike00000000000000Metadata-Version: 1.1 Name: txtorcon Version: 0.19.3 Summary: Twisted-based Tor controller client, with state-tracking and configuration abstractions. https://txtorcon.readthedocs.org https://github.com/meejah/txtorcon Home-page: https://github.com/meejah/txtorcon Author: meejah Author-email: meejah@meejah.ca License: MIT Description: .. _NOTE: see docs/index.rst for the starting-point .. _ALSO: https://txtorcon.readthedocs.org for rendered docs .. image:: https://travis-ci.org/meejah/txtorcon.png?branch=master :target: https://www.travis-ci.org/meejah/txtorcon :alt: travis .. image:: https://coveralls.io/repos/meejah/txtorcon/badge.png :target: https://coveralls.io/r/meejah/txtorcon :alt: coveralls .. image:: http://codecov.io/github/meejah/txtorcon/coverage.svg?branch=master :target: http://codecov.io/github/meejah/txtorcon?branch=master :alt: codecov .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=stable :target: https://txtorcon.readthedocs.io/en/stable :alt: ReadTheDocs .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=latest :target: https://txtorcon.readthedocs.io/en/latest :alt: ReadTheDocs .. image:: http://api.flattr.com/button/flattr-badge-large.png :target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub :alt: flattr .. image:: https://landscape.io/github/meejah/txtorcon/master/landscape.svg?style=flat :target: https://landscape.io/github/meejah/txtorcon/master :alt: Code Health txtorcon ======== - **docs**: https://txtorcon.readthedocs.org or http://timaq4ygg2iegci7.onion - **code**: https://github.com/meejah/txtorcon - ``torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git`` - MIT-licensed; - Python 2.7, PyPy 5.0.0+, Python 3.4+; - depends on `Twisted `_, `Automat `_, (and the `ipaddress `_ backport for non Python 3) .. caution:: Several large, new features have landed on master. If you're working directly from master, note that some of these APIs may change before the next release. Ten Thousand Feet ----------------- txtorcon is an implementation of the `control-spec `_ for `Tor `_ using the `Twisted `_ networking library for `Python `_. This is useful for writing utilities to control or make use of Tor in event-based Python programs. If your Twisted program supports endpoints (like ``twistd`` does) your server or client can make use of Tor immediately, with no code changes. Start your own Tor or connect to one and get live stream, circuit, relay updates; read and change config; monitor events; build circuits; create onion services; etcetera (`ReadTheDocs `_). Some Possibly Motivational Example Code --------------------------------------- `download `_ (also `python3 style `_) .. code:: python from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks from twisted.internet.endpoints import UNIXClientEndpoint import treq import txtorcon @react @inlineCallbacks def main(reactor): tor = yield txtorcon.connect( reactor, UNIXClientEndpoint(reactor, "/var/run/tor/control") ) print("Connected to Tor version {}".format(tor.version)) url = 'https://www.torproject.org:443' print("Downloading {}".format(url)) resp = yield treq.get(url, agent=tor.web_agent()) print(" {} bytes".format(resp.length)) data = yield resp.text() print("Got {} bytes:\n{}\n[...]{}".format( len(data), data[:120], data[-120:], )) print("Creating a circuit") state = yield tor.create_state() circ = yield state.build_circuit() yield circ.when_built() print(" path: {}".format(" -> ".join([r.ip for r in circ.path]))) print("Downloading meejah's public key via above circuit...") resp = yield treq.get( 'https://meejah.ca/meejah.asc', agent=circ.web_agent(reactor, tor.config.socks_endpoint(reactor)), ) data = yield resp.text() print(data) Try It Now On Debian/Ubuntu --------------------------- For example, serve some files via an onion service (*aka* hidden service): .. code-block:: shell-session $ sudo apt-get install python-txtorcon $ twistd -n web --port "onion:80" --path ~/public_html Read More --------- All the documentation starts `in docs/index.rst `_. Also hosted at `txtorcon.rtfd.org `_. You'll want to start with `the introductions `_ (`hosted at RTD `_). 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.19.3/txtorcon.egg-info/requires.txt0000644000175000017500000000035713111226470021334 0ustar mikemike00000000000000Twisted[tls]>=15.5.0 ipaddress>=1.0.16 zope.interface>=3.6.1 incremental automat [dev] tox coverage cuvner setuptools>=0.8.0 Sphinx repoze.sphinx.autointerface>=0.4 coveralls codecov wheel twine pyflakes pep8 mock ipaddress>=1.0.16 geoip txtorcon-0.19.3/txtorcon.egg-info/dependency_links.txt0000644000175000017500000000000113111226470022775 0ustar mikemike00000000000000 txtorcon-0.19.3/txtorcon.egg-info/pbr.json0000644000175000017500000000005712727452131020416 0ustar mikemike00000000000000{"is_release": false, "git_version": "0f966c2"}txtorcon-0.19.3/MANIFEST.in0000644000175000017500000000114013035473427015122 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.19.3/txtorcon/0000755000175000017500000000000013111226470015235 5ustar mikemike00000000000000txtorcon-0.19.3/txtorcon/spaghetti.py0000644000175000017500000000746613106645477017634 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.19.3/txtorcon/endpoints.py0000644000175000017500000010121113111226032017600 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 from txtorcon.socks import TorSocksEndpoint from twisted.internet.interfaces import IStreamClientEndpointStringParserWithReactor from twisted.internet import defer, error 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.endpoints import UNIXClientEndpoint # 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 .torconfig import TorConfig, launch_tor, HiddenService from .torconfig import _endpoint_from_socksport_line from .util import SingleObserver _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.mkdtemp`` """ @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 """ from txtorcon.controller import connect tor = connect(reactor, control_endpoint) tor.addCallback(lambda t: t.get_config()) # tor is a Deferred return TCPHiddenServiceEndpoint(reactor, tor, 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, stealth_auth=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 :meth:`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). :param stealth_auth: None, or a list of strings -- one for each stealth authenticator you require. """ 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 it in # the listen() call r = TCPHiddenServiceEndpoint( reactor, config, public_port, hidden_service_dir=hidden_service_dir, local_port=local_port, stealth_auth=stealth_auth, ) 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, stealth_auth=None): """ :param reactor: :api:`twisted.internet.interfaces.IReactorTCP` provider :param config: :class:`txtorcon.TorConfig` instance. :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.mkdtemp() AND DELETED when the reactor shuts down. :param stealth_auth: A list of strings, one name for each stealth authenticator you want. Like: ``['alice', 'bob']`` :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.stealth_auth = stealth_auth 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) self.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)) # we re-adjust the percentage-scale, using 105% and 110% for # the two parts of waiting for descriptor upload. That is, we # want: 110 * constant == 100.0 for p in self.progress_listeners: p(prog * (100.0 / 110.0), 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) hs_dirs = [hs.dir for hs in self.config.HiddenServices] if self.hidden_service_dir not in hs_dirs: authlines = [] if self.stealth_auth: # like "stealth name0,name1" authlines = ['stealth ' + ','.join(self.stealth_auth)] self.hiddenservice = HiddenService( self.config, self.hidden_service_dir, ['%d 127.0.0.1:%d' % (self.public_port, self.local_port)], group_readable=1, auth=authlines, ) 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.') # FIXME XXX need to work out what happens here on stealth-auth'd # things. maybe we need a separate StealthHiddenService # vs. HiddenService ?! # XXX that is, self.onion_uri isn't always avaialble :/ uri = None if self.hiddenservice is not None: log.msg('Started hidden service port %d' % self.public_port) for client in self.hiddenservice.clients: # XXX FIXME just taking the first one on multi-client services if uri is None: uri = client[1] log.msg(' listening on %s.onion' % client[1]) defer.returnValue( TorOnionListeningPort( self.tcp_listening_port, self.hidden_service_dir, 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.mkdtemp()``. 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) @defer.inlineCallbacks def _create_socks_endpoint(reactor, control_protocol, socks_config=None): """ Internal helper. This uses an already-configured SOCKS endpoint from the attached Tor, or creates a new TCP one (and configures Tor with it). If socks_config is non-None, it is a SOCKSPort line and will either be used if it already exists or will be created. """ socks_ports = yield control_protocol.get_conf('SOCKSPort') if socks_ports: socks_ports = list(socks_ports.values())[0] if not isinstance(socks_ports, list): socks_ports = [socks_ports] else: # return from get_conf was an empty dict; we want a list socks_ports = [] # everything in the SocksPort list can include "options" after the # initial value. We don't care about those, but do need to strip # them. socks_ports = [port.split()[0] for port in socks_ports] # could check platform? but why would you have unix ports on a # platform that doesn't? unix_ports = set([p.startswith('unix:') for p in socks_ports]) tcp_ports = set(socks_ports) - unix_ports socks_endpoint = None for p in list(unix_ports) + list(tcp_ports): # prefer unix-ports if socks_config and p != socks_config: continue try: socks_endpoint = _endpoint_from_socksport_line(reactor, p) except Exception as e: log.msg("clientFromString('{}') failed: {}".format(p, e)) # if we still don't have an endpoint, nothing worked (or there # were no SOCKSPort lines at all) so we add config to tor if socks_endpoint is None: if socks_config is None: # is a unix-socket in /tmp on a supported platform better than # this? port = yield available_tcp_port(reactor) socks_config = str(port) socks_ports.append(socks_config) # NOTE! We must set all the ports in one command or we'll # destroy pre-existing config args = [] for p in socks_ports: args.append('SOCKSPort') args.append(p) yield control_protocol.set_conf(*args) socks_endpoint = _endpoint_from_socksport_line(reactor, socks_config) defer.returnValue(socks_endpoint) @implementer(IStreamClientEndpoint) class TorClientEndpoint(object): """ An IStreamClientEndpoint which establishes a connection via Tor. You should not instantiate these directly; use ``clientFromString()``, :meth:`txtorcon.Tor.stream_via` or :meth:`txtorcon.Circuit.stream_via` :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 socks_endpoint: An IStreamClientEndpoint pointing at (one of) our Tor's SOCKS ports. These can be instantiated with :meth:`txtorcon.TorConfig.socks_endpoint`. :param tls: Can be False or True (to get default Browser-like hostname verification) or the result of calling optionsForClientTLS() yourself. Default is True. """ socks_ports_to_try = [9050, 9150] @classmethod def from_connection(cls, reactor, control_protocol, host, port, tls=None, socks_endpoint=None): if socks_endpoint is None: socks_endpoint = _create_socks_endpoint(reactor, control_protocol) return TorClientEndpoint( host, port, socks_endpoint=socks_endpoint, tls=tls, reactor=reactor, ) def __init__(self, host, port, socks_endpoint=None, # can be Deferred tls=False, # XXX our custom SOCKS stuff doesn't support auth (yet?) socks_username=None, socks_password=None, reactor=None, **kw): if host is None or port is None: raise ValueError('host and port must be specified') self.host = host self.port = int(port) self._socks_endpoint = socks_endpoint self._socks_username = socks_username self._socks_password = socks_password self._tls = tls # XXX FIXME we 'should' probably include 'reactor' as the # first arg to this class, but technically that's a # breaking change :( self._reactor = reactor if reactor is None: from twisted.internet import reactor self._reactor = reactor # backwards-compatibility: you used to specify a TCP SOCKS # endpoint via socks_host= and socks_port= kwargs if self._socks_endpoint is None: try: self._socks_endpoint = TCP4ClientEndpoint( reactor, kw['socks_hostname'], kw['socks_port'], ) # XXX should deprecation-warn here except KeyError: pass # this is a separate "if" from above in case socks_endpoint # was None but the user specified the (old) # socks_hostname/socks_port (in which case we do NOT want # guessing_enabled if self._socks_endpoint is None: self._socks_port_iter = iter(self.socks_ports_to_try) self._socks_guessing_enabled = True else: self._socks_guessing_enabled = False # XXX think, do we want to expose these like this? Or some # other way (because they're for stream-isolation, not actual # auth) self._socks_username = socks_username self._socks_password = socks_password self._when_address = SingleObserver() def _get_address(self): """ internal helper. *le sigh*. This is basically just to support TorCircuitEndpoint; see TorSocksEndpoint._get_address(). There shouldn't be any need for "actual users" to need this! This returns a Deferred that fires once: - we have an underlying SOCKS5 endpoint - ...and it has received a local connection (and hence the address/port) """ return self._when_address.when_fired() @defer.inlineCallbacks def connect(self, protocolfactory): last_error = None # XXX fix in socks.py stuff for socks_username, socks_password if self._socks_username or self._socks_password: raise RuntimeError( "txtorcon socks support doesn't yet do username/password" ) if self._socks_endpoint is not None: socks_ep = TorSocksEndpoint( self._socks_endpoint, self.host, self.port, self._tls, ) # forward the address to any listeners we have socks_ep._get_address().addCallback(self._when_address.fire) proto = yield socks_ep.connect(protocolfactory) defer.returnValue(proto) else: for socks_port in self._socks_port_iter: tor_ep = TCP4ClientEndpoint( self._reactor, "127.0.0.1", # XXX socks_hostname, no? socks_port, ) socks_ep = TorSocksEndpoint(tor_ep, self.host, self.port, self._tls) # forward the address to any listeners we have socks_ep._get_address().addCallback(self._when_address.fire) try: proto = yield socks_ep.connect(protocolfactory) defer.returnValue(proto) except error.ConnectError as e0: last_error = e0 if last_error is not None: raise last_error @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, we ... XXX? 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, reactor, host=None, port=None, socksHostname=None, socksPort=None, socksUsername=None, socksPassword=None): if port is not None: port = int(port) ep = None if socksPort is not None: # Tor can speak SOCKS over unix, too, but this doesn't let # us pass one ... ep = TCP4ClientEndpoint(reactor, socksHostname, int(socksPort)) return TorClientEndpoint( host, port, socks_endpoint=ep, 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 return self._parseClient(*args, **kwargs) txtorcon-0.19.3/txtorcon/torcontrolprotocol.py0000644000175000017500000010000613106645477021613 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import with_statement from binascii import b2a_hex, hexlify 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 twisted.python.failure import Failure from zope.interface import implementer from txtorcon.util import hmac_sha256, compare_via_hash, unescape_quoted_string from txtorcon.log import txtorlog from txtorcon.interface import ITorControlProtocol from .spaghetti import FSM, State, Transition from .util import maybe_coroutine 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:: ep = TCP4ClientEndpoint(reactor, "localhost", 9051) ep.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): for cb in self.callbacks: try: cb(data) except Exception as e: log.err(Failure()) log.err( "Notifying '{callback}' for '{name}' failed: {e}".format( callback=cb, name=self.name, e=e, ) ) 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, key_hints=None): """ 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 sp = line.split('=', 1) found_key = ('=' in line and ' ' not in sp[0]) if found_key and key_hints and sp[0] not in key_hints: found_key = False if found_key: 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._cookie_data = None """Data read from cookie file used to authenticate.""" 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) ep = TCP4ClientEndpoint(reactor, "localhost", 9051) ep.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 ` """ return self.queue_command('GETINFO %s' % ' '.join(args)) 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. 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. """ d = self.get_info_raw(*args) d.addCallback(parse_keywords, key_hints=args) return d 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): """ 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. :param evt: event name, see also :attr:`txtorcon.TorControlProtocol.events` .keys(). These event names are queried from Tor (with `GETINFO events/names`) :param callback: any callable that takes a single argument which receives 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:: - should have 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): """ The opposite of :meth:`TorControlProtocol.add_event_listener` :param evt: the event name (or an Event object) :param cb: the callback object to remove """ 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. Quoting torspec/control-spec.txt: "The authentication token can be specified as either a quoted ASCII string, or as an unquoted hexadecimal encoding of that same string (to avoid escaping issues)." """ if not isinstance(passphrase, bytes): passphrase = passphrase.encode() phrase = b2a_hex(passphrase) return self.queue_command(b'AUTHENTICATE ' + phrase) 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. """ if not isinstance(cmd, bytes): cmd = cmd.encode('ascii') 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 + b'\n') self.debuglog.flush() self.fsm.process(line.decode('ascii')) 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() name = args[0] if name in self.events: self.events[name].got_update(rest[len(name) + 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 + b'\n') self.debuglog.flush() data = cmd + b'\r\n' txtorlog.msg("cmd: {}".format(data.strip())) self.transport.write(data) 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 """ if self._cookie_data is None: raise RuntimeError("Cookie data not read.") 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( b"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( b"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(b'AUTHENTICATE ' + client_hash_hex) def _read_cookie(self, cookiefile): """ Open and read a cookie file :param cookie: Path to the cookie file """ self._cookie_data = None self._cookie_data = open(cookiefile, 'rb').read() if len(self._cookie_data) != 32: raise RuntimeError( "Expected authentication cookie to be 32 bytes, got %d" % len(self._cookie_data) ) def _do_authenticate(self, protoinfo): """ Callback on PROTOCOLINFO to actually authenticate once we know what's supported. """ methods = None cookie_auth = False 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 or 'COOKIE' in methods: cookiefile_match = re.search(r'COOKIEFILE=("(?:[^"\\]|\\.)*")', protoinfo) if cookiefile_match: cookiefile = cookiefile_match.group(1) cookiefile = unescape_quoted_string(cookiefile) try: self._read_cookie(cookiefile) cookie_auth = True except IOError as why: txtorlog.msg("Reading COOKIEFILE failed: " + str(why)) if self.password_function and 'HASHEDPASSWORD' in methods: txtorlog.msg("Falling back to password") else: raise RuntimeError( "Failed to read COOKIEFILE '{fname}': {msg}\n".format( fname=cookiefile, msg=str(why), ) # "On Debian, join the debian-tor group" ) else: txtorlog.msg("Didn't get COOKIEFILE") raise RuntimeError( "Got 'COOKIE' or 'SAFECOOKIE' method, but no 'COOKIEFILE'" ) if cookie_auth: if 'SAFECOOKIE' in methods: txtorlog.msg("Using SAFECOOKIE authentication", cookiefile, len(self._cookie_data), "bytes") self.client_nonce = os.urandom(32) cmd = b'AUTHCHALLENGE SAFECOOKIE ' + \ hexlify(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: txtorlog.msg("Using COOKIE authentication", cookiefile, len(self._cookie_data), "bytes") d = self.authenticate(self._cookie_data) d.addCallback(self._bootstrap) d.addErrback(self._auth_failed) return if self.password_function and 'HASHEDPASSWORD' in methods: d = defer.maybeDeferred(self.password_function) d.addCallback(maybe_coroutine) d.addCallback(self._do_password_authentication) d.addErrback(self._auth_failed) return if 'NULL' in methods: d = self.queue_command('AUTHENTICATE') d.addCallback(self._bootstrap) d.addErrback(self._auth_failed) return raise RuntimeError( "The Tor I connected to doesn't support SAFECOOKIE nor COOKIE" " authentication (or we can't read the cookie files) 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" 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" 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" 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 is None: raise RuntimeError("No code set yet in broadcast response.") elif 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 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.19.3/txtorcon/util.py0000644000175000017500000003435413106645477016615 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 ipaddress import struct import re import six from twisted.internet import defer from twisted.internet.interfaces import IProtocolFactory from twisted.internet.endpoints import serverFromString from twisted.web.http_headers import Headers from zope.interface import implementer from zope.interface import Interface if six.PY3: import asyncio try: import GeoIP as _GeoIP GeoIP = _GeoIP except ImportError: GeoIP = None city = None country = None asn = None def create_tbb_web_headers(): """ Returns a new `twisted.web.http_headers.Headers` instance populated with tags to mimic Tor Browser. These include values for `User-Agent`, `Accept`, `Accept-Language` and `Accept-Encoding`. """ return Headers({ b"User-Agent": [b"Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0"], b"Accept": [b"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"], b"Accept-Language": [b"en-US,en;q=0.5"], b"Accept-Encoding": [b"gzip, deflate"], }) def version_at_least(version_string, major, minor, micro, patch): """ This returns True if the version_string represents a Tor version of at least ``major``.``minor``.``micro``.``patch`` version, ignoring any trailing specifiers. """ parts = re.match( r'^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+).*$', version_string, ) for ver, gold in zip(parts.group(1, 2, 3, 4), (major, minor, micro, patch)): if int(ver) < int(gold): return False return True 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 = maybe_create_db("/usr/share/GeoIP/GeoLiteCity.dat") asn = maybe_create_db("/usr/share/GeoIP/GeoIPASNum.dat") country = maybe_create_db("/usr/share/GeoIP/GeoIP.dat") 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 six.PY2 and isinstance(addr, str): addr = unicode(addr) # noqa try: return ipaddress.ip_address(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. :param args: a list of strings, each with one key=value pair :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(b'\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(object): """ 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) def unescape_quoted_string(string): r''' This function implementes the recommended functionality described in the tor control-spec to be compatible with older tor versions: * Read \\n \\t \\r and \\0 ... \\377 as C escapes. * Treat a backslash followed by any other character as that character. Except the legacy support for the escape sequences above this function implements parsing of QuotedString using qcontent from QuotedString = DQUOTE *qcontent DQUOTE :param string: The escaped quoted string. :returns: The unescaped string. :raises ValueError: If the string is in a invalid form (e.g. a single backslash) ''' match = re.match(r'''^"((?:[^"\\]|\\.)*)"$''', string) if not match: raise ValueError("Invalid quoted string", string) string = match.group(1) # remove backslash before all characters which should not be # handeled as escape codes by string.decode('string-escape'). # This is needed so e.g. '\x00' is not unescaped as '\0' string = re.sub(r'((?:^|[^\\])(?:\\\\)*)\\([^ntr0-7\\])', r'\1\2', string) if six.PY3: # XXX hmmm? return bytes(string, 'ascii').decode('unicode-escape') return string.decode('string-escape') def default_control_port(): """ This returns a default control port, which respects an environment variable `TX_CONTROL_PORT`. Without the environment variable, this returns 9151 (the Tor Browser Bundle default). You shouldn't use this in "normal" code, this is a convenience for the examples. """ try: return int(os.environ['TX_CONTROL_PORT']) except KeyError: return 9151 class IListener(Interface): def add(callback): """ Add a listener. The arguments to the callback are determined by whomever calls notify() """ def remove(callback): """ Add a listener. The arguments to the callback are determined by whomever calls notify() """ def notify(*args, **kw): """ Calls every listener with the given args and keyword-args. XXX errors? just log? """ def maybe_coroutine(obj): """ If 'obj' is a coroutine and we're using Python3, wrap it in ensureDeferred. Otherwise return the original object. (This is to insert in all callback chains from user code, in case that user code is Python3 and used 'async def') """ if six.PY3 and asyncio.iscoroutine(obj): return defer.ensureDeferred(obj) return obj @implementer(IListener) class _Listener(object): """ Internal helper. """ def __init__(self): self._listeners = set() def add(self, callback): """ Add a callback to this listener """ self._listeners.add(callback) __call__ = add #: alias for "add" def remove(self, callback): """ Remove a callback from this listener """ self._listeners.remove(callback) def notify(self, *args, **kw): """ Calls all listeners with the specified args. Returns a Deferred which callbacks when all the listeners which return Deferreds have themselves completed. """ calls = [] def failed(fail): # XXX use logger fail.printTraceback() for cb in self._listeners: d = defer.maybeDeferred(cb, *args, **kw) d.addCallback(maybe_coroutine) d.addErrback(failed) calls.append(d) return defer.DeferredList(calls) class _ListenerCollection(object): """ Internal helper. This collects all your valid event listeners together in one object if you want. """ def __init__(self, valid_events): self._valid_events = valid_events for e in valid_events: setattr(self, e, _Listener()) def __call__(self, event, callback): if event not in self._valid_events: raise Exception("Invalid event '{}'".format(event)) getattr(self, event).add(callback) def remove(self, event, callback): if event not in self._valid_events: raise Exception("Invalid event '{}'".format(event)) getattr(self, event).remove(callback) def notify(self, event, *args, **kw): if event not in self._valid_events: raise Exception("Invalid event '{}'".format(event)) getattr(self, event).notify(*args, **kw) # similar to OneShotObserverList in Tahoe-LAFS class SingleObserver(object): """ A helper for ".when_*()" sort of functions. """ _NotFired = object() def __init__(self): self._observers = [] self._fired = self._NotFired def when_fired(self): d = defer.Deferred() if self._fired is not self._NotFired: d.callback(self._fired) else: self._observers.append(d) return d def fire(self, value): if self._observers is None: return # raise RuntimeError("already fired") ? self._fired = value for d in self._observers: d.callback(self._fired) self._observers = None return value # so we're transparent if used as a callback txtorcon-0.19.3/txtorcon/log.py0000644000175000017500000000107112752747562016413 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.19.3/txtorcon/interface.py0000644000175000017500000003511513106645477017574 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 ITor(Interface): """ Represents a tor instance. This high-level API should provide all objects you need to interact with tor. """ process = Attribute("TorProcessProtocol instance if we launched this Tor") protocol = Attribute("A TorControlProtocol connected to our Tor") version = Attribute("The version of the Tor we're connected to") def quit(self): """ Closes the control connection, and if we launched this Tor instance we'll send it a TERM and wait until it exits. :return: a Deferred that fires when we've quit """ def get_config(self): """ :return: a Deferred that fires with a TorConfig instance. This instance represents up-to-date configuration of the tor instance (even if another controller is connected). If you call this more than once you'll get the same TorConfig back. """ def create_state(self): """ returns a Deferred that fires with a ready-to-go :class:`txtorcon.TorState` instance. """ def web_agent(self, pool=None, _socks_endpoint=None): """ :param _socks_endpoint: If ``None`` (the default), a suitable SOCKS port is chosen from our config (or added). If supplied, should be a Deferred which fires an IStreamClientEndpoint (e.g. the return-value from :meth:`txtorcon.TorConfig.socks_endpoint`) or an immediate IStreamClientEndpoint You probably don't need to mess with this. :param pool: passed on to the Agent (as ``pool=``) """ def dns_resolve(self, hostname): """ :param hostname: a string :returns: a Deferred that calbacks with the hostname as looked-up via Tor (or errback). This uses Tor's custom extension to the SOCKS5 protocol. """ def dns_resolve_ptr(self, ip): """ :param ip: a string, like "127.0.0.1" :returns: a Deferred that calbacks with the IP address as looked-up via Tor (or errback). This uses Tor's custom extension to the SOCKS5 protocol. """ def stream_via(self, host, port, tls=False, _socks_endpoint=None): """ This returns an IStreamClientEndpoint instance that will use this Tor (via SOCKS) to visit the ``(host, port)`` indicated. :param host: The host to connect to. You MUST pass host-names to this. If you absolutely know that you've not leaked DNS (e.g. you save IPs in your app's configuration or similar) then you can pass an IP. :param port: Port to connect to. :param tls: If True, it will wrap the return endpoint in one that does TLS (default: False). :param _socks_endpoint: Normally not needed (default: None) but you can pass an IStreamClientEndpoint_ directed at one of the local Tor's SOCKS5 ports (e.g. created with :meth:`txtorcon.TorConfig.create_socks_endpoint`). Can be a Deferred. """ 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. Only advanced use-cases should need to use this directly; for most users, using the :func:`txtorcon.Circuit.stream_via` interface should be preferred. """ def attach_stream_failure(stream, fail): """ :param stream: The stream we were trying to attach. :param fail: A Failure instance. A failure has occurred while trying to attach the stream. """ 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. 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_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.19.3/txtorcon/torstate.py0000644000175000017500000010126013106646076017470 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import with_statement import collections import os import stat import types import warnings from twisted.internet import defer from twisted.python.failure import Failure 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.torcontrolprotocol 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 ._microdesc_parser import MicrodescriptorParser from .router import hexIdFromHash from .util import maybe_coroutine 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 def _extract_reason(kw): """ Internal helper. Extracts a reason (possibly both reasons!) from the kwargs for a circuit failed or closed event. """ try: # we "often" have a REASON reason = kw['REASON'] try: # ...and sometimes even have a REMOTE_REASON reason = '{}, {}'.format(reason, kw['REMOTE_REASON']) except KeyError: pass # should still be the 'REASON' error if we had it except KeyError: reason = "unknown" return reason @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() #: keys on id (integer) self.circuits = {} #: keys on id (integer) self.streams = {} #: list of unique routers self.all_routers = set() #: keys by hexid (string) and by unique names self.routers = {} #: keys on name, value always list (many duplicate "Unnamed" #: routers, for example) self.routers_by_name = {} #: keys by hexid (string) self.routers_by_hash = {} #: potentially-usable as entry guards, I think? (any router #: with 'Guard' flag) self.guards = {} #: from GETINFO entry-guards, our current entry guards self.entry_guards = {} #: list of entry guards we didn't parse out self.unusable_entry_guards = [] #: keys by name self.authorities = {} #: see set_attacher self._cleanup = None self._network_status_parser = MicrodescriptorParser(self._create_router) self.post_bootstrap = defer.Deferred() if bootstrap: self.protocol.post_bootstrap.addCallback(self._bootstrap) self.protocol.post_bootstrap.addErrback(self.post_bootstrap.errback) def _create_router(self, **kw): id_hex = hexIdFromHash(kw['idhash']) try: router = self.routers[id_hex] except KeyError: router = Router(self.protocol) self.routers[router.id_hex] = router router.from_consensus = True router.update( kw['nickname'], kw['idhash'], kw['orhash'], kw['modified'], kw['ip'], kw['orport'], kw['dirport'], ) router.flags = kw.get('flags', []) if 'bandwidth' in kw: router.bandwidth = kw['bandwidth'] if 'ip_v6' in kw: router.ip_v6.extend(kw['ip_v6']) if 'guard' in router.flags: self.guards[router.id_hex] = router if 'authority' in router.flags: self.authorities[router.name] = router if router.id_hex in self.routers: # FIXME should I do an update() on this one?? router = self.routers[router.id_hex] else: if router.name in self.routers: self.routers[router.name] = None else: self.routers[router.name] = router if router.name in self.routers_by_name: self.routers_by_name[router.name].append(router) else: self.routers_by_name[router.name] = [router] self.routers[router.id_hex] = router self.routers_by_hash[router.id_hex] = router self.all_routers.add(router) @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) # look out! we're depending on get_info_incremental returning # *lines*, which isn't documented -- but will be true because # TorControlProtocol is a LineReceiver... yield self.protocol.get_info_incremental( 'ns/all', self._network_status_parser.feed_line, ) # 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. You are Strongly Encouraged to **not** use this API directly, and instead use :meth:`txtorcon.Circuit.stream_via` or :meth:`txtorcon.Circuit.web_agent` instead. If you do need to use this API, it's an error if you call either of the other two methods. 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: if self._attacher is attacher: return if self._attacher is not None: raise RuntimeError( "set_attacher called but we already have an 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 # noqa 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 += ',' # XXX should we really accept bytes here? if isinstance(router, bytes) and len(router) == 40 \ and hashFromHexId(router): cmd += router.decode('utf8') 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, ) circ_d.addCallback(maybe_coroutine) # 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 or circ is TorState.DO_NOT_ATTACH: # tell Tor to do what it likes return self.protocol.queue_command(b"ATTACHSTREAM %d 0" % stream.id) 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( b"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 NEWCONSENSUS events. """ # XXX why are we getting this with 0 data? if len(data): self.all_routers = set() for line in data.split('\n'): self._network_status_parser.feed_line(line) self._network_status_parser.done() txtorlog.msg(len(self.routers_by_name), "named routers found.") # remove any names we added that turned out to have dups remove_keys = set() for (k, v) in self.routers.items(): if v is None: txtorlog.msg(len(self.routers_by_name[k]), "dups:", k) remove_keys.add(k) for k in remove_keys: 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) for listener in self.circuit_listeners: c.listen(listener) 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. """ 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.addrmap) self.streams[stream_id] = stream stream.listen(self) for x in self.stream_listeners: stream.listen(x) 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, '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 try: bound = types.MethodType(func, self, TorState) except TypeError: # python3 bound = types.MethodType(func, self) yield self.protocol.add_event_listener( event, bound, ) # 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) circuit._when_built.fire( Failure(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) circuit._when_built.fire( Failure(Exception("Circuit closed ('{}')".format(_extract_reason(kw)))) ) self.circuit_destroy(circuit) def circuit_failed(self, circuit, **kw): "ICircuitListener API" txtorlog.msg("circuit_failed", circuit, str(kw)) circuit._when_built.fire( Failure(Exception("Circuit failed ('{}')".format(_extract_reason(kw)))) ) self.circuit_destroy(circuit) txtorcon-0.19.3/txtorcon/circuit.py0000644000175000017500000004475413106646076017303 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 six import time from datetime import datetime from twisted.python.failure import Failure from twisted.python import log from twisted.internet import defer from twisted.internet.interfaces import IStreamClientEndpoint from zope.interface import implementer from .interface import IRouterContainer, IStreamAttacher from txtorcon.util import find_keywords, maybe_ip_addr, SingleObserver # look like "2014-01-25T02:12:14.593772" TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' @implementer(IStreamAttacher) class _CircuitAttacher(object): """ Internal helper. If we've ever called .stream_via or .web_agent, then one of these is added as "the" stream-attacher. """ def __init__(self): # map real_host (IPAddress) -> circuit self._circuit_targets = dict() def add_endpoint(self, target_ep, circuit): """ Returns a Deferred that fires when we've attached this endpoint to the provided circuit. """ # This can seem a little .. convulted. What's going on is # we're asking the TorCircuitEndpoint to tell us when it gets # the local address (i.e. when "whomever created the endpoit" # actually connects locally). We need this address to # successfully map incoming streams. d = defer.Deferred() target_ep._get_address().addCallback(self._add_real_target, circuit, d) return d def _add_real_target(self, real_addr, circuit, d): # joy oh joy, ipaddress wants unicode, Twisted gives us bytes... real_host = maybe_ip_addr(six.text_type(real_addr.host)) real_port = real_addr.port self._circuit_targets[(real_host, real_port)] = (circuit, d) def attach_stream_failure(self, stream, fail): """ IStreamAttacher API """ k = (stream.source_addr, stream.source_port) try: (circ, d) = self._circuit_targets.pop(k) d.errback(fail) except KeyError: pass # so this means ... we got an error, but on a stream we either # don't care about or already .callback()'d so should we log # it? or ignore? return None @defer.inlineCallbacks def attach_stream(self, stream, circuits): """ IStreamAttacher API """ k = (stream.source_addr, stream.source_port) try: circuit, d = self._circuit_targets.pop(k) except KeyError: return try: yield circuit.when_built() if circuit.state in ['FAILED', 'CLOSED', 'DETACHED']: d.errback(Failure(RuntimeError( "Circuit {circuit.id} in state {circuit.state} so unusable".format( circuit=circuit, ) ))) return d.callback(None) defer.returnValue(circuit) except Exception: d.errback(Failure()) @defer.inlineCallbacks def _get_circuit_attacher(reactor, state): if _get_circuit_attacher.attacher is None: _get_circuit_attacher.attacher = _CircuitAttacher() yield state.set_attacher(_get_circuit_attacher.attacher, reactor) defer.returnValue(_get_circuit_attacher.attacher) _get_circuit_attacher.attacher = None @implementer(IStreamClientEndpoint) class TorCircuitEndpoint(object): def __init__(self, reactor, torstate, circuit, target_endpoint): self._reactor = reactor self._state = torstate self._target_endpoint = target_endpoint # a TorClientEndpoint self._circuit = circuit @defer.inlineCallbacks def connect(self, protocol_factory): """IStreamClientEndpoint API""" # need to: # 1. add 'our' attacher to state # 2. do the "underlying" connect # 3. recognize our stream # 4. attach it to our circuit attacher = yield _get_circuit_attacher(self._reactor, self._state) # note that we'll only ever add an attacher once, and then it # stays there "forever". so if you never call the .stream_via # or .web_agent APIs, set_attacher won't get called .. but if # you *do*, then you can't call set_attacher yourself (because # that's an error). See discussion in set_attacher on # TorState or issue #169 yield self._circuit.when_built() connect_d = self._target_endpoint.connect(protocol_factory) attached_d = attacher.add_endpoint(self._target_endpoint, self._circuit) proto = yield connect_d yield attached_d defer.returnValue(proto) 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.1, these are: - LAUNCHED: circuit ID assigned to new circuit - BUILT: all hops finished, can now accept streams - EXTENDED: one more hop has been completed - FAILED: circuit closed (was not built) - CLOSED: circuit closed (was built) :ivar purpose: The reason this circuit was built. For most purposes, you'll want to look at `GENERAL` circuits only. 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 :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 # XXX FIXME 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 # XXX ^ should probably be when_closed() etc etc... # caches parsed value for time_created() self._time_created = None # all notifications for when_built, when_closed self._when_built = SingleObserver() self._when_closed = SingleObserver() # 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. If the circuit will never hit BUILT (e.g. it is abandoned by Tor before it gets to BUILT) you will receive an errback """ # XXX note to self: we never do an errback; fix this behavior if self.state == 'BUILT': return defer.succeed(self) return self._when_built.when_fired() def when_closed(self): """ Returns a Deferred that callback()'s (with this Circuit instance) when this circuit hits CLOSED or FAILED. """ if self.state in ['CLOSED', 'FAILED']: return defer.succeed(self) return self._when_closed.when_fired() def web_agent(self, reactor, socks_endpoint, pool=None): """ :param socks_endpoint: create one with :meth:`txtorcon.TorConfig.create_socks_endpoint`. Can be a Deferred. :param pool: passed on to the Agent (as ``pool=``) """ # local import because there isn't Agent stuff on some # platforms we support, so this will only error if you try # this on the wrong platform (pypy [??] and old-twisted) from txtorcon import web return web.tor_agent( reactor, socks_endpoint, circuit=self, pool=pool, ) # XXX should make this API match above web_agent (i.e. pass a # socks_endpoint) or change the above... def stream_via(self, reactor, host, port, socks_endpoint, use_tls=False): """ This returns an `IStreamClientEndpoint`_ that will connect to the given ``host``, ``port`` via Tor -- and via this parciular circuit. We match the streams up using their source-ports, so even if there are many streams in-flight to the same destination they will align correctly. For example, to cause a stream to go to ``torproject.org:443`` via a particular circuit:: @inlineCallbacks def main(reactor): circ = yield torstate.build_circuit() # lets Tor decide the path yield circ.when_built() tor_ep = circ.stream_via(reactor, 'torproject.org', 443) # 'factory' is for your protocol proto = yield tor_ep.connect(factory) Note that if you're doing client-side Web requests, you probably want to use `treq `_ or ``Agent`` directly so call :meth:`txtorcon.Circuit.web_agent` instead. :param socks_endpoint: should be a Deferred firing a valid IStreamClientEndpoint pointing at a Tor SOCKS port (or an IStreamClientEndpoint already). .. _istreamclientendpoint: https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IStreamClientEndpoint.html """ from .endpoints import TorClientEndpoint ep = TorClientEndpoint( host, port, socks_endpoint, tls=use_tls, reactor=reactor, ) return TorCircuitEndpoint(reactor, self._torstate, self, ep) @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(*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. """ # we're already closed; nothing to do if self.state == 'CLOSED': return defer.succeed(None) # someone already called close() but we're not closed yet if self._closing_deferred: d = defer.Deferred() def closed(arg): d.callback(arg) return arg self._closing_deferred.addBoth(closed) return d # actually-close the circuit 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 d def age(self, now=None): """ 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 if now is None: now = datetime.utcnow() 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]) for x in self.listeners: x.circuit_new(self) 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 = [] for x in self.listeners: x.circuit_launched(self) else: if self.state != 'FAILED' and self.state != 'CLOSED': if len(args) > 2: self.update_path(args[2].split(',')) if self.state == 'BUILT': for x in self.listeners: x.circuit_built(self) self._when_built.fire(self) elif self.state == 'CLOSED': if len(self.streams) > 0: # it seems this can/does happen if a remote router # crashes or otherwise shuts down a circuit with # streams on it still .. also if e.g. you "carml circ # --delete " the circuit while the stream is # in-progress...can we do better than logging? # *should* we do anything else (the stream should get # closed still by Tor). log.msg( "Circuit is {} but still has {} streams".format( self.state, len(self.streams) ) ) flags = self._create_flags(kw) self.maybe_call_closing_deferred() for x in self.listeners: x.circuit_closed(self, **flags) 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() for x in self.listeners: x.circuit_failed(self, **flags) 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 self._when_closed.fire(self) 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 the path grew, notify listeners if len(self.path) > len(oldpath): for x in self.listeners: x.circuit_extend(self, router) oldpath = self.path def __str__(self): path = ' '.join([x.ip for x in self.path]) return "" % (self.id, self.state, path, self.purpose) class CircuitBuildTimedOutError(Exception): """ This exception is thrown when using `timed_circuit_build` and the circuit build times-out. """ def build_timeout_circuit(tor_state, reactor, path, timeout, using_guards=False): """ Build a new circuit within a timeout. CircuitBuildTimedOutError will be raised unless we receive a circuit build result (success or failure) within the `timeout` duration. :returns: a Deferred which fires when the circuit build succeeds (or fails to build). """ timed_circuit = [] d = tor_state.build_circuit(routers=path, using_guards=using_guards) def get_circuit(c): timed_circuit.append(c) return c def trap_cancel(f): f.trap(defer.CancelledError) if timed_circuit: d2 = timed_circuit[0].close() else: d2 = defer.succeed(None) d2.addCallback(lambda ign: Failure(CircuitBuildTimedOutError("circuit build timed out"))) return d2 d.addCallback(get_circuit) d.addCallback(lambda circ: circ.when_built()) d.addErrback(trap_cancel) reactor.callLater(timeout, d.cancel) return d txtorcon-0.19.3/txtorcon/torinfo.py0000644000175000017500000002244613073070110017272 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.19.3/txtorcon/controller.py0000644000175000017500000010704713111225003017772 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 six import shlex import tempfile import functools import ipaddress from io import StringIO from collections import Sequence from os.path import dirname, exists from twisted.python import log from twisted.python.failure import Failure from twisted.internet.defer import inlineCallbacks, returnValue, Deferred, succeed, fail from twisted.internet import protocol, error from twisted.internet.endpoints import TCP4ClientEndpoint from twisted.internet.endpoints import UNIXClientEndpoint from twisted.internet.interfaces import IReactorTime, IReactorCore from twisted.internet.interfaces import IStreamClientEndpoint from zope.interface import implementer from txtorcon.util import delete_file_or_tree, find_keywords from txtorcon.util import find_tor_binary, available_tcp_port from txtorcon.log import txtorlog from txtorcon.torcontrolprotocol import TorProtocolFactory from txtorcon.torstate import TorState from txtorcon.torconfig import TorConfig from txtorcon.endpoints import TorClientEndpoint, _create_socks_endpoint from . import socks from .interface import ITor if sys.platform in ('linux', 'linux2', 'darwin'): import pwd @inlineCallbacks def launch(reactor, progress_updates=None, control_port=None, data_directory=None, socks_port=None, stdout=None, stderr=None, timeout=None, tor_binary=None, user=None, # XXX like the config['User'] special-casing from before # 'users' probably never need these: connection_creator=None, kill_on_stderr=True, _tor_config=None, # a TorConfig instance, mostly for tests ): """ launches a new Tor process, and returns a Deferred that fires with a new :class:`txtorcon.Tor` instance. From this instance, you can create or get any "interesting" instances you need: the :class:`txtorcon.TorConfig` instance, create endpoints, create :class:`txtorcon.TorState` instance(s), etc. Note that there is NO way to pass in a config; we only expost a couple of basic Tor options. If you need anything beyond these, you can access the ``TorConfig`` instance (via ``.config``) and make any changes there, reflecting them in tor with ``.config.save()``. You can igore all the options and safe defaults will be provided. However, **it is recommended to pass data_directory** especially if you will be starting up Tor frequently, as it saves a bunch of time (and bandwidth for the directory authorities). "Safe defaults" means: - a tempdir for a ``DataDirectory`` is used (respecting ``TMP``) and is deleted when this tor is shut down (you therefore *probably* want to supply the ``data_directory=`` kwarg); - a random, currently-unused local TCP port is used as the ``SocksPort`` (specify ``socks_port=`` if you want your own). If you want no SOCKS listener at all, pass ``socks_port=0`` - we set ``__OwningControllerProcess`` and call ``TAKEOWNERSHIP`` so that if our control connection goes away, tor shuts down (see `control-spec `_ 3.23). - the launched Tor will use ``COOKIE`` authentication. :param reactor: a Twisted IReactorCore implementation (usually twisted.internet.reactor) :param progress_updates: a callback which gets progress updates; gets 3 args: percent, tag, summary (FIXME make an interface for this). :param data_directory: set as the ``DataDirectory`` option to Tor, this is where tor keeps its state information (cached relays, etc); starting with an already-populated state directory is a lot faster. If ``None`` (the default), we create a tempdir for this **and delete it on exit**. It is recommended you pass something here. :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 "kill_on_stderr=False" if you don't want this behavior. :param tor_binary: path to the Tor binary to run. If None (the default), we try to find the tor binary. :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 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 :class:`txtorcon.Tor` instance, from which you can retrieve the TorControlProtocol instance via the ``.protocol`` property. 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. XXX this "User" thing was, IIRC, a feature for root-using scripts (!!) that were going to launch tor, but where tor would drop to a different user. Do we still want to support this? Probably relevant to Docker (where everything is root! yay!) ``User``: if this exists, we attempt to set ownership of the tempdir to this user (but only if our effective UID is 0). """ # 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. if not IReactorCore.providedBy(reactor): raise ValueError( "'reactor' argument must provide IReactorCore" " (got '{}': {})".format( type(reactor).__class__.__name__, repr(reactor) ) ) 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 (XXX is there a "better" way to check for file-like object?) for arg in [stderr, stdout]: if arg and not getattr(arg, "write", None): raise RuntimeError( 'File-like object needed for stdout or stderr args.' ) config = _tor_config or TorConfig() if data_directory is not None: user_set_data_directory = True config.DataDirectory = data_directory try: os.mkdir(data_directory, 0o0700) except OSError: pass else: user_set_data_directory = False data_directory = tempfile.mkdtemp(prefix='tortmp') config.DataDirectory = data_directory # note: we also set up the ProcessProtocol to delete this when # Tor exits, this is "just in case" fallback: reactor.addSystemEventTrigger( 'before', 'shutdown', functools.partial(delete_file_or_tree, data_directory) ) # things that used launch_tor() had to set ControlPort and/or # SocksPort on the config to pass them, so we honour that here. if control_port is None and _tor_config is not None: try: control_port = config.ControlPort except KeyError: control_port = None if socks_port is None and _tor_config is not None: try: socks_port = config.SocksPort except KeyError: socks_port = None if socks_port is None: socks_port = yield available_tcp_port(reactor) config.SOCKSPort = socks_port try: our_user = user or config.User except KeyError: pass else: if sys.platform in ('linux', 'linux2', 'darwin') and os.geteuid() == 0: os.chown(data_directory, pwd.getpwnam(our_user).pw_uid, -1) # user can pass in a control port, or we set one up here if control_port is None: # on posix-y systems, we can use a unix-socket if sys.platform in ('linux', 'linux2', 'darwin'): # note: tor will not accept a relative path for ControlPort control_port = 'unix:{}'.format( os.path.join(os.path.realpath(data_directory), 'control.socket') ) else: control_port = yield available_tcp_port(reactor) else: if str(control_port).startswith('unix:'): control_path = control_port.lstrip('unix:') containing_dir = dirname(control_path) if not exists(containing_dir): raise ValueError( "The directory containing '{}' must exist".format( containing_dir ) ) # Tor will be sad if the directory isn't 0700 mode = (0o0777 & os.stat(containing_dir).st_mode) if mode & ~(0o0700): raise ValueError( "The directory containing a unix control-socket ('{}') " "must only be readable by the user".format(containing_dir) ) config.ControlPort = control_port config.CookieAuthentication = 1 config.__OwningControllerProcess = os.getpid() if connection_creator is None: if str(control_port).startswith('unix:'): connection_creator = functools.partial( UNIXClientEndpoint(reactor, control_port[5:]).connect, TorProtocolFactory() ) else: connection_creator = functools.partial( TCP4ClientEndpoint(reactor, 'localhost', control_port).connect, TorProtocolFactory() ) # not an "else" on purpose; if we passed in "control_port=0" *and* # a custom connection creator, we should still set this to None so # it's never called (since we can't connect with ControlPort=0) if control_port == 0: connection_creator = None # NOTE well, that if we don't pass "-f" then Tor will merrily load # its default torrc, and apply our options over top... :/ should # file a bug probably? --no-defaults or something maybe? (does # --defaults-torrc - or something work?) config_args = ['-f', '/dev/null/non-existant-on-purpose', '--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) process_protocol = TorProcessProtocol( connection_creator, progress_updates, config, reactor, timeout, kill_on_stderr, stdout, stderr, ) if control_port == 0: connected_cb = succeed(None) else: connected_cb = process_protocol.when_connected() # 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) ) log.msg('Spawning tor process with DataDirectory', data_directory) args = [tor_binary] + config_args # XXX note to self; we create data_directory above, so when this # is master we can close # https://github.com/meejah/txtorcon/issues/178 transport = reactor.spawnProcess( process_protocol, tor_binary, args=args, env={'HOME': data_directory}, path=data_directory if os.path.exists(data_directory) else None, # XXX error if it doesn't exist? ) # FIXME? don't need rest of the args: uid, gid, usePTY, childFDs) transport.closeStdin() proto = yield connected_cb # note "proto" here is a TorProcessProtocol # we might need to attach this protocol to the TorConfig if config.protocol is None and proto is not None and proto.tor_protocol is not None: # proto is None in the ControlPort=0 case yield config.attach_protocol(proto.tor_protocol) # note that attach_protocol waits for the protocol to be # boostrapped if necessary returnValue( Tor( reactor, config.protocol, _tor_config=config, _process_proto=process_protocol, ) ) # XXX # what about control_endpoint_or_endpoints? (i.e. allow a list to try?) # what about if it's None (default?) and we try some candidates? @inlineCallbacks def connect(reactor, control_endpoint=None, password_function=None): """ Creates a :class:`txtorcon.Tor` instance by connecting to an already-running tor's control port. For example, a common default tor uses is UNIXClientEndpoint(reactor, '/var/run/tor/control') or TCP4ClientEndpoint(reactor, 'localhost', 9051) If only password authentication is available in the tor we connect to, the ``password_function`` is called (if supplied) to retrieve a valid password. This function can return a Deferred. For example:: import txtorcon from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks @inlineCallbacks def main(reactor): tor = yield txtorcon.connect( TCP4ClientEndpoint(reactor, "localhost", 9051) ) state = yield tor.create_state() for circuit in state.circuits: print(circuit) :param control_endpoint: None, an IStreamClientEndpoint to connect to, or a Sequence of IStreamClientEndpoint instances to connect to. If None, a list of defaults are tried. :param password_function: See :class:`txtorcon.TorControlProtocol` :return: a Deferred that fires with a :class:`txtorcon.Tor` instance """ @inlineCallbacks def try_endpoint(control_ep): assert IStreamClientEndpoint.providedBy(control_ep) proto = yield control_ep.connect( TorProtocolFactory( password_function=password_function ) ) config = yield TorConfig.from_protocol(proto) tor = Tor(reactor, proto, _tor_config=config) returnValue(tor) if control_endpoint is None: to_try = [ UNIXClientEndpoint(reactor, '/var/run/tor/control'), TCP4ClientEndpoint(reactor, '127.0.0.1', 9051), TCP4ClientEndpoint(reactor, '127.0.0.1', 9151), ] elif IStreamClientEndpoint.providedBy(control_endpoint): to_try = [control_endpoint] elif isinstance(control_endpoint, Sequence): to_try = control_endpoint for ep in control_endpoint: if not IStreamClientEndpoint.providedBy(ep): raise ValueError( "For control_endpoint=, '{}' must provide" " IStreamClientEndpoint".format(ep) ) else: raise ValueError( "For control_endpoint=, '{}' must provide" " IStreamClientEndpoint".format(control_endpoint) ) errors = [] for idx, ep in enumerate(to_try): try: tor = yield try_endpoint(ep) txtorlog.msg("Connected via '{}'".format(ep)) returnValue(tor) except Exception as e: errors.append(e) if len(errors) == 1: raise errors[0] raise RuntimeError( 'Failed to connect to: {}'.format( ', '.join( '{}: {}'.format(ep, err) for ep, err in zip(to_try, errors) ) ) ) @implementer(ITor) class Tor(object): """ I represent a single instance of Tor and act as a Builder/Factory for several useful objects you will probably want. There are two ways to create a Tor instance: - :func:`txtorcon.connect` to connect to a Tor that is already running (e.g. Tor Browser Bundle, a system Tor, ...). - :func:`txtorcon.launch` to launch a fresh Tor instance The stable API provided by this class is :class:`txtorcon.interface.ITor` If you desire more control, there are "lower level" APIs which are the very ones used by this class. However, this "highest level" API should cover many use-cases:: import txtorcon @inlineCallbacks def main(reactor): # tor = yield txtorcon.connect(UNIXClientEndpoint(reactor, "/var/run/tor/control")) tor = yield txtorcon.launch(reactor) onion_ep = tor.create_onion_endpoint(port=80) port = yield onion_ep.listen(Site()) print(port.getHost()) """ def __init__(self, reactor, control_protocol, _tor_config=None, _process_proto=None): """ don't instantiate this class yourself -- instead use the factory methods :func:`txtorcon.launch` or :func:`txtorcon.connect` """ self._protocol = control_protocol self._config = _tor_config self._reactor = reactor # this only passed/set when we launch() self._process_protocol = _process_proto # cache our preferred socks port (please use # self._default_socks_endpoint() to get one) self._socks_endpoint = None @inlineCallbacks def quit(self): """ Closes the control connection, and if we launched this Tor instance we'll send it a TERM and wait until it exits. """ if self._protocol is not None: yield self._protocol.quit() if self._process_protocol is not None: yield self._process_protocol.quit() if self._protocol is None and self._process_protocol is None: raise RuntimeError( "This Tor has no protocol instance; we can't quit" ) # XXX bikeshed on this name? @property def process(self): if self._process_protocol: return self._process_protocol raise RuntimeError( "This Tor instance was not launched by us; no process to return" ) @property def protocol(self): """ The TorControlProtocol instance that is communicating with this Tor instance. """ return self._protocol @property def version(self): return self._protocol.version @inlineCallbacks def get_config(self): """ :return: a Deferred that fires with a TorConfig instance. This instance represents up-to-date configuration of the tor instance (even if another controller is connected). If you call this more than once you'll get the same TorConfig back. """ if self._config is None: self._config = yield TorConfig.from_protocol(self._protocol) returnValue(self._config) def web_agent(self, pool=None, socks_endpoint=None): """ :param socks_endpoint: If ``None`` (the default), a suitable SOCKS port is chosen from our config (or added). If supplied, should be a Deferred which fires an IStreamClientEndpoint (e.g. the return-value from :meth:`txtorcon.TorConfig.socks_endpoint`) or an immediate IStreamClientEndpoint You probably don't need to mess with this. :param pool: passed on to the Agent (as ``pool=``) """ # local import since not all platforms have this from txtorcon import web if socks_endpoint is None: socks_endpoint = _create_socks_endpoint(self._reactor, self._protocol) if not isinstance(socks_endpoint, Deferred): if not IStreamClientEndpoint.providedBy(socks_endpoint): raise ValueError( "'socks_endpoint' should be a Deferred or an IStreamClient" "Endpoint (got '{}')".format(type(socks_endpoint)) ) return web.tor_agent( self._reactor, socks_endpoint, pool=pool, ) @inlineCallbacks def dns_resolve(self, hostname): """ :param hostname: a string :returns: a Deferred that calbacks with the hostname as looked-up via Tor (or errback). This uses Tor's custom extension to the SOCKS5 protocol. """ socks_ep = yield self._default_socks_endpoint() ans = yield socks.resolve(socks_ep, hostname) returnValue(ans) @inlineCallbacks def dns_resolve_ptr(self, ip): """ :param ip: a string, like "127.0.0.1" :returns: a Deferred that calbacks with the IP address as looked-up via Tor (or errback). This uses Tor's custom extension to the SOCKS5 protocol. """ socks_ep = yield self._default_socks_endpoint() ans = yield socks.resolve_ptr(socks_ep, ip) returnValue(ans) def stream_via(self, host, port, tls=False, socks_endpoint=None): """ This returns an IStreamClientEndpoint_ instance that will use this Tor (via SOCKS) to visit the ``(host, port)`` indicated. :param host: The host to connect to. You MUST pass host-names to this. If you absolutely know that you've not leaked DNS (e.g. you save IPs in your app's configuration or similar) then you can pass an IP. :param port: Port to connect to. :param tls: If True, it will wrap the return endpoint in one that does TLS (default: False). :param socks_endpoint: Normally not needed (default: None) but you can pass an IStreamClientEndpoint_ directed at one of the local Tor's SOCKS5 ports (e.g. created with :meth:`txtorcon.TorConfig.create_socks_endpoint`). Can be a Deferred. .. _IStreamClientEndpoint: https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IStreamClientEndpoint.html """ if _is_non_public_numeric_address(host): raise ValueError("'{}' isn't going to work over Tor".format(host)) if socks_endpoint is None: socks_endpoint = self._default_socks_endpoint() # socks_endpoint may be a a Deferred, but TorClientEndpoint handles it return TorClientEndpoint( host, port, socks_endpoint=socks_endpoint, tls=tls, reactor=self._reactor, ) # XXX note to self: insert onion endpoint-creation functions when # merging onion.py # XXX or get_state()? and make there be always 0 or 1 states; cf. convo w/ Warner @inlineCallbacks def create_state(self): """ returns a Deferred that fires with a ready-to-go :class:`txtorcon.TorState` instance. """ state = TorState(self.protocol) yield state.post_bootstrap returnValue(state) def __str__(self): return "".format( tor_version=self._protocol.version, ) @inlineCallbacks def _default_socks_endpoint(self): """ Returns a Deferred that fires with our default SOCKS endpoint (which might mean setting one up in our attacked Tor if it doesn't have one) """ if self._socks_endpoint is None: self._socks_endpoint = yield _create_socks_endpoint(self._reactor, self._protocol) returnValue(self._socks_endpoint) # XXX from magic-wormhole def _is_non_public_numeric_address(host): # for numeric hostnames, skip RFC1918 addresses, since no Tor exit # node will be able to reach those. Likewise ignore IPv6 addresses. try: a = ipaddress.ip_address(six.text_type(host)) except ValueError: return False # non-numeric, let Tor try it if a.is_loopback or a.is_multicast or a.is_private or a.is_reserved \ or a.is_unspecified: return True # too weird, don't connect return False 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 after the Deferred returned from :meth:`TorProcessProtocol.when_connected` is triggered. """ self.config = config self.tor_protocol = None self.progress_updates = progress_updates # XXX if connection_creator is not None .. is connected_cb # tied to connection_creator...? if connection_creator: self.connection_creator = connection_creator else: self.connection_creator = None # use SingleObserver self._connected_listeners = [] # list of Deferred (None when we're connected) 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 self._on_exit = [] # Deferred's we owe a call/errback to when we exit 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 when_connected(self): if self._connected_listeners is None: return succeed(self) d = Deferred() self._connected_listeners.append(d) return d def _maybe_notify_connected(self, arg): """ Internal helper. .callback or .errback on all Deferreds we've returned from `when_connected` """ if self._connected_listeners is None: return for d in self._connected_listeners: # Twisted will turn this into an errback if "arg" is a # Failure d.callback(arg) self._connected_listeners = None def quit(self): """ This will terminate (with SIGTERM) the underlying Tor process. :returns: a Deferred that callback()'s (with None) when the process has actually exited. """ try: self.transport.signalProcess('TERM') d = Deferred() self._on_exit.append(d) except error.ProcessExitedAlready: self.transport.loseConnection() d = succeed(None) except Exception: d = fail() return d def _signal_on_exit(self, reason): to_notify = self._on_exit self._on_exit = [] for d in to_notify: d.callback(None) def outReceived(self, data): """ :api:`twisted.internet.protocol.ProcessProtocol ` API """ if self.stdout: self.stdout.write(data.decode('ascii')) # 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 b'Bootstrap' in data: self.attempted_connect = True # hmmm, we don't "do" anything with this Deferred? # (should it be connected to the when_connected # Deferreds?) d = self.connection_creator() d.addCallback(self._tor_connected) d.addErrback(self._tor_connection_failed) # XXX 'should' be able to improve the error-handling by directly tying # this Deferred into the notifications -- BUT we might try again, so # we need to know "have we given up -- had an error" and only in that # case send to the connected things. I think? # d.addCallback(self._maybe_notify_connected) def _timeout_expired(self): """ A timeout was supplied during setup, and the time has run out. """ self._did_timeout = True try: self.transport.signalProcess('TERM') except error.ProcessExitedAlready: # XXX why don't we just always do this? self.transport.loseConnection() fail = Failure(RuntimeError("timeout while launching Tor")) self._maybe_notify_connected(fail) 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 processExited(self, reason): self._signal_on_exit(reason) 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) # hmmm, this log() should probably go away...not always an # error (e.g. .quit() log.err(err) self._maybe_notify_connected(Failure(err)) 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) # XXX also, should check if the failure is e.g. a syntax error # or an actually connection failure # okay, so this is a little trickier than I thought at first: # we *can* just relay this back to the # connection_creator()-returned Deferred, *but* we don't know # if this is "the last" error and we're going to try again # (and thus e.g. should fail all the when_connected() # Deferreds) or not. log.err(failure) self.attempted_connect = False return None 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 self._maybe_notify_connected(self) @inlineCallbacks def _tor_connected(self, proto): txtorlog.msg("tor_connected %s" % proto) self.tor_protocol = proto self.tor_protocol.is_owned = self.transport.pid yield self.tor_protocol.post_bootstrap txtorlog.msg("Protocol is bootstrapped") yield self.tor_protocol.add_event_listener('STATUS_CLIENT', self._status_client) yield self.tor_protocol.queue_command('TAKEOWNERSHIP') yield self.tor_protocol.queue_command('RESETCONF __OwningControllerProcess') if self.config is not None and self.config.protocol is None: yield self.config.attach_protocol(proto) returnValue(self) # XXX or "proto"? txtorcon-0.19.3/txtorcon/stream.py0000644000175000017500000002547013106645477017132 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, addrmap=None): """ :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.IStreamListener` 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??)""" self._addrmap = addrmap 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): 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:]) # target_host is often an IP address (newer tors? did # this change?) so we attempt to look it up in our # AddrMap and make it a name no matter what. if self._addrmap: try: h = self._addrmap.find(self.target_host) self.target_host = h.name except KeyError: pass 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")) self._notify('stream_new', self) else: self._notify('stream_succeeded', self) 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) self._notify('stream_closed', self, **flags) 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) self._notify('stream_failed', self, **flags) 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) self._notify('stream_detach', self, **flags) 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) self._notify('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 _notify(self, func, *args, **kw): """ Internal helper. Calls the IStreamListener function 'func' with the given args, guarding around errors. """ for x in self.listeners: try: getattr(x, func)(*args, **kw) except Exception: log.err() 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.19.3/txtorcon/__init__.py0000644000175000017500000000634613106645477017377 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._metadata import __version__, __author__, __contact__ from txtorcon._metadata import __license__, __copyright__, __url__ from txtorcon.router import Router from txtorcon.circuit import Circuit from txtorcon.circuit import build_timeout_circuit from txtorcon.circuit import CircuitBuildTimedOutError from txtorcon.stream import Stream from txtorcon.controller import connect 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 EphemeralHiddenService from txtorcon.torconfig import launch_tor # this one depreceated, use launch() from txtorcon.controller import TorProcessProtocol from txtorcon.controller import launch # this is "newer" one from txtorcon.controller import Tor from txtorcon.controller 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, IProgressProvider from txtorcon.endpoints import get_global_tor from . import util from . import interface from txtorcon.interface import ( ITorControlProtocol, IStreamListener, IStreamAttacher, StreamListenerMixin, ICircuitContainer, ICircuitListener, CircuitListenerMixin, IRouterContainer, IAddrListener, ITor ) __all__ = [ "connect", "launch", # connect, launch return instance of Tor()... "Tor", "ITor", # ...which is the preferred high-level API "Router", "Circuit", "Stream", "TorControlProtocol", "TorProtocolError", "TorProtocolFactory", "TorState", "DEFAULT_VALUE", "TorInfo", "build_tor_connection", "build_local_tor_connection", "launch_tor", "TorNotFound", "TorConfig", "HiddenService", "EphemeralHiddenService", "TorProcessProtocol", "TorInfo", "TCPHiddenServiceEndpoint", "TCPHiddenServiceEndpointParser", "TorClientEndpoint", "TorClientEndpointStringParser", "IHiddenService", "IProgressProvider", "TorOnionAddress", "TorOnionListeningPort", "get_global_tor", "build_timeout_circuit", "CircuitBuildTimedOutError", "AddrMap", "util", "interface", "ITorControlProtocol", "IStreamListener", "IStreamAttacher", "StreamListenerMixin", "ICircuitContainer", "ICircuitListener", "CircuitListenerMixin", "IRouterContainer", "IAddrListener", "IProgressProvider", "__version__", "__author__", "__contact__", "__license__", "__copyright__", "__url__", ] txtorcon-0.19.3/txtorcon/_microdesc_parser.py0000644000175000017500000001060513106645477021314 0ustar mikemike00000000000000 from .util import find_keywords # putting the old parser back in here for now until there's a solution # making Automat faster from .spaghetti import FSM, State, Transition class MicrodescriptorParser(object): """ Parsers microdescriptors line by line. New relays are emitted via the 'create_relay' callback. """ def __init__(self, create_relay): self._create_relay = create_relay self._relay_attrs = None 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._machine = FSM([waiting_r, waiting_s, waiting_w, waiting_p]) self._relay_attrs = None def feed_line(self, line): """ A line has been received. """ self._machine.process(line) def done(self, *args): """ All lines have been fed. """ self._maybe_callback_router() def _maybe_callback_router(self): if self._relay_attrs is not None: self._create_relay(**self._relay_attrs) self._relay_attrs = None def _router_begin(self, data): self._maybe_callback_router() args = data.split()[1:] self._relay_attrs = dict( nickname=args[0], idhash=args[1], orhash=args[2], modified=args[3] + ' ' + args[4], ip=args[5], orport=args[6], dirport=args[7], ) def _router_flags(self, data): args = data.split()[1:] self._relay_attrs['flags'] = args def _router_address(self, data): """only for IPv6 addresses""" args = data.split()[1:] try: self._relay_attrs['ip_v6'].extend(args) except KeyError: self._relay_attrs['ip_v6'] = list(args) def _router_bandwidth(self, data): args = data.split()[1:] kw = find_keywords(args) self._relay_attrs['bandwidth'] = kw['Bandwidth'] def _router_policy(self, data): pass txtorcon-0.19.3/txtorcon/addrmap.py0000644000175000017500000000771512777342107017246 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) # add both name and IP address self.addr[params[0]] = a self.addr[params[1]] = 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.19.3/txtorcon/web.py0000644000175000017500000001250213106645477016404 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import with_statement from twisted.web.iweb import IAgentEndpointFactory from twisted.web.client import Agent from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.endpoints import TCP4ClientEndpoint, UNIXClientEndpoint from zope.interface import implementer from txtorcon.socks import TorSocksEndpoint from txtorcon.log import txtorlog @implementer(IAgentEndpointFactory) class _AgentEndpointFactoryUsingTor(object): def __init__(self, reactor, tor_socks_endpoint): self._reactor = reactor self._proxy_ep = tor_socks_endpoint # XXX could accept optional "tls=" to do something besides # optionsForClientTLS(hostname)? def endpointForURI(self, uri): return TorSocksEndpoint( self._proxy_ep, uri.host, uri.port, tls=(uri.scheme == b'https'), ) @implementer(IAgentEndpointFactory) class _AgentEndpointFactoryForCircuit(object): def __init__(self, reactor, tor_socks_endpoint, circ): self._reactor = reactor self._socks_ep = tor_socks_endpoint self._circ = circ def endpointForURI(self, uri): """IAgentEndpointFactory API""" torsocks = TorSocksEndpoint( self._socks_ep, uri.host, uri.port, tls=uri.scheme == b'https', ) from txtorcon.circuit import TorCircuitEndpoint return TorCircuitEndpoint( self._reactor, self._circ._torstate, self._circ, torsocks, ) def tor_agent(reactor, socks_endpoint, circuit=None, pool=None): """ This is the low-level method used by :meth:`txtorcon.Tor.web_agent` and :meth:`txtorcon.Circuit.web_agent` -- probably you should call one of those instead. :returns: a Deferred that fires with an object that implements :class:`twisted.web.iweb.IAgent` and is thus suitable for passing to ``treq`` as the ``agent=`` kwarg. Of course can be used directly; see `using Twisted web cliet `_. :param reactor: the reactor to use :param circuit: If supplied, a particular circuit to use :param socks_endpoint: Deferred that fires w/ IStreamClientEndpoint (or IStreamClientEndpoint instance) which points at a SOCKS5 port of our Tor :param pool: passed on to the Agent (as ``pool=``) """ if socks_endpoint is None: raise ValueError( "Must provide socks_endpoint as Deferred or IStreamClientEndpoint" ) if circuit is not None: factory = _AgentEndpointFactoryForCircuit(reactor, socks_endpoint, circuit) else: factory = _AgentEndpointFactoryUsingTor(reactor, socks_endpoint) return Agent.usingEndpointFactory(reactor, factory, pool=pool) @inlineCallbacks def agent_for_socks_port(reactor, torconfig, socks_config, pool=None): """ This returns a Deferred that fires with an object that implements :class:`twisted.web.iweb.IAgent` and is thus suitable for passing to ``treq`` as the ``agent=`` kwarg. Of course can be used directly; see `using Twisted web cliet `_. If you have a :class:`txtorcon.Tor` instance already, the preferred API is to call :meth:`txtorcon.Tor.web_agent` on it. :param torconfig: a :class:`txtorcon.TorConfig` instance. :param socks_config: anything valid for Tor's ``SocksPort`` option. This is generally just a TCP port (e.g. ``9050``), but can also be a unix path like so ``unix:/path/to/socket`` (Tor has restrictions on the ownership/permissions of the directory containing ``socket``). If the given SOCKS option is not already available in the underlying Tor instance, it is re-configured to add the SOCKS option. """ # :param tls: True (the default) will use Twisted's default options # with the hostname in the URI -- that is, TLS verification # similar to a Browser. Otherwise, you can pass whatever Twisted # returns for `optionsForClientTLS # `_ socks_config = str(socks_config) # sadly, all lists are lists-of-strings to Tor :/ if socks_config not in torconfig.SocksPort: txtorlog.msg("Adding SOCKS port '{}' to Tor".format(socks_config)) torconfig.SocksPort.append(socks_config) try: yield torconfig.save() except Exception as e: raise RuntimeError( "Failed to reconfigure Tor with SOCKS port '{}': {}".format( socks_config, str(e) ) ) if socks_config.startswith('unix:'): socks_ep = UNIXClientEndpoint(reactor, socks_config[5:]) else: if ':' in socks_config: host, port = socks_config.split(':', 1) else: host = '127.0.0.1' port = int(socks_config) socks_ep = TCP4ClientEndpoint(reactor, host, port) returnValue( Agent.usingEndpointFactory( reactor, _AgentEndpointFactoryUsingTor(reactor, socks_ep), pool=pool, ) ) txtorcon-0.19.3/txtorcon/router.py0000644000175000017500000002117713106645477017157 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 json from datetime import datetime from .util import NetLocation import six from base64 import b64encode, b64decode from binascii import b2a_hex, a2b_hex from twisted.internet.defer import inlineCallbacks, returnValue from twisted.web.client import readBody def hexIdFromHash(thehash): """ From the base-64 encoded hashes Tor uses, this produces the longer hex-encoded hashes. :param thehash: base64-encoded str :return: hex-encoded hash """ return '$' + b2a_hex(b64decode(thehash + '=')).decode('ascii').upper() def hashFromHexId(hexid): """ From a hex fingerprint id, convert back to base-64 encoded value. """ if hexid[0] == '$': hexid = hexid[1:] return b64encode(a2b_hex(hexid))[:-1].decode('ascii') class PortRange(object): """ Represents a range of ports for Router policies. """ def __init__(self, a, b): self.min = a self.max = b def __eq__(self, b): return b >= self.min and b <= self.max 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): """ This is the time of 'the publication time of its most recent descriptor' (in UTC). See also dir-spec.txt. """ # "... in the form YYYY-MM-DD HH:MM:SS, in UTC" 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) # for py3, these should be valid (but *not* py2) # assert type(idhash) is not bytes # assert type(orhash) is not bytes @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, (six.text_type, bytes)): 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) @inlineCallbacks def get_onionoo_details(self, agent): """ Requests the 'details' document from onionoo.torproject.org via the given `twisted.web.iweb.IAgent` -- you can get a suitable instance to pass here by calling either :meth:`txtorcon.Tor.web_agent` or :meth:`txtorcon.Circuit.web_agent`. """ uri = 'https://onionoo.torproject.org/details?lookup={}'.format(self.id_hex[1:]).encode('ascii') resp = yield agent.request(b'GET', uri) if resp.code != 200: raise RuntimeError( 'Failed to lookup relay details for {}'.format(self.id_hex) ) body = yield readBody(resp) data = json.loads(body.decode('ascii')) if len(data['relays']) != 1: raise RuntimeError( 'Got multiple relays for {}'.format(self.id_hex) ) relay_data = data['relays'][0] if relay_data['fingerprint'].lower() != self.id_hex[1:].lower(): raise RuntimeError( 'Expected "{}" but got data for "{}"'.format(self.id_hex, relay_data['fingerprint']) ) returnValue(relay_data) # note that exit-policy is no longer included in the # microdescriptors by default, so this stuff is mostly here as a # historic artifact. If you want to use exit-policy for things # your best bet is to tell your tor to download full descriptors # (SETCONF UseMicrodescriptors 0) instead. @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.19.3/txtorcon/_metadata.py0000644000175000017500000000026713111226101017522 0ustar mikemike00000000000000__version__ = '0.19.3' __author__ = 'meejah' __contact__ = 'meejah@meejah.ca' __url__ = 'https://github.com/meejah/txtorcon' __license__ = 'MIT' __copyright__ = 'Copyright 2012-2017' txtorcon-0.19.3/txtorcon/socks.py0000644000175000017500000005443413106645477016763 0ustar mikemike00000000000000# in-progress; implementing SOCKS5 client-side stuff as extended by # tor because txsocksx will not be getting Python3 support any time # soon, and its underlying dependency (Parsely) also doesn't support # Python3. Also, Tor's SOCKS5 implementation is especially simple, # since it doesn't do BIND or UDP ASSOCIATE. from __future__ import print_function import six import struct from socket import inet_pton, inet_ntoa, inet_aton, AF_INET6, AF_INET from twisted.internet.defer import inlineCallbacks, returnValue, Deferred from twisted.internet.protocol import Protocol, Factory from twisted.internet.address import IPv4Address, IPv6Address, HostnameAddress from twisted.python.failure import Failure from twisted.protocols import portforward from twisted.protocols import tls from twisted.internet.interfaces import IStreamClientEndpoint from zope.interface import implementer import ipaddress import automat from txtorcon import util __all__ = ( 'resolve', 'resolve_ptr', 'SocksError', 'GeneralServerFailureError', 'ConnectionNotAllowedError', 'NetworkUnreachableError', 'HostUnreachableError', 'ConnectionRefusedError', 'TtlExpiredError', 'CommandNotSupportedError', 'AddressTypeNotSupportedError', 'TorSocksEndpoint', ) def _create_ip_address(host, port): if not isinstance(host, six.text_type): raise ValueError( "'host' must be {}, not {}".format(six.text_type, type(host)) ) try: a = ipaddress.ip_address(host) except ValueError: a = None if isinstance(a, ipaddress.IPv4Address): return IPv4Address('TCP', host, port) if isinstance(a, ipaddress.IPv6Address): return IPv6Address('TCP', host, port) addr = HostnameAddress(host, port) addr.host = host return addr class _SocksMachine(object): """ trying to prototype the SOCKS state-machine in automat This is a SOCKS state machine to make a single request. """ _machine = automat.MethodicalMachine() SUCCEEDED = 0x00 REPLY_IPV4 = 0x01 REPLY_HOST = 0x03 REPLY_IPV6 = 0x04 # XXX address = (host, port) instead def __init__(self, req_type, host, port=0, on_disconnect=None, on_data=None, create_connection=None): if req_type not in self._dispatch: raise ValueError( "Unknown request type '{}'".format(req_type) ) if req_type == 'CONNECT' and create_connection is None: raise ValueError( "create_connection function required for '{}'".format( req_type ) ) if not isinstance(host, (bytes, str, six.text_type)): raise ValueError( "'host' must be text".format(type(host)) ) # XXX what if addr is None? self._req_type = req_type self._addr = _create_ip_address(six.text_type(host), port) self._data = b'' self._on_disconnect = on_disconnect self._create_connection = create_connection # XXX FIXME do *one* of these: self._on_data = on_data self._outgoing_data = [] # the other side of our proxy self._sender = None self._when_done = util.SingleObserver() def when_done(self): """ Returns a Deferred that fires when we're done """ return self._when_done.when_fired() def _data_to_send(self, data): if self._on_data: self._on_data(data) else: self._outgoing_data.append(data) def send_data(self, callback): """ drain all pending data by calling `callback()` on it """ # a "for x in self._outgoing_data" would potentially be more # efficient, but then there's no good way to bubble exceptions # from callback() out without lying about how much data we # processed .. or eat the exceptions in here. while len(self._outgoing_data): data = self._outgoing_data.pop(0) callback(data) def feed_data(self, data): # I feel like maybe i'm doing all this buffering-stuff # wrong. but I also don't want a bunch of "received 1 byte" # etc states hanging off everything that can "get data" self._data += data self.got_data() @_machine.output() def _parse_version_reply(self): "waiting for a version reply" if len(self._data) >= 2: reply = self._data[:2] self._data = self._data[2:] (version, method) = struct.unpack('BB', reply) if version == 5 and method in [0x00, 0x02]: self.version_reply(method) else: if version != 5: self.version_error(SocksError( "Expected version 5, got {}".format(version))) else: self.version_error(SocksError( "Wanted method 0 or 2, got {}".format(method))) def _parse_ipv4_reply(self): if len(self._data) >= 10: addr = inet_ntoa(self._data[4:8]) port = struct.unpack('H', self._data[8:10])[0] self._data = self._data[10:] if self._req_type == 'CONNECT': self.reply_ipv4(addr, port) else: self.reply_domain_name(addr) def _parse_ipv6_reply(self): if len(self._data) >= 22: addr = self._data[4:20] port = struct.unpack('H', self._data[20:22])[0] self._data = self._data[22:] self.reply_ipv6(addr, port) def _parse_domain_name_reply(self): assert len(self._data) >= 8 # _parse_request_reply checks this addrlen = struct.unpack('B', self._data[4:5])[0] # may simply not have received enough data yet... if len(self._data) < (5 + addrlen + 2): return addr = self._data[5:5 + addrlen] # port = struct.unpack('H', self._data[5 + addrlen:5 + addrlen + 2])[0] self._data = self._data[5 + addrlen + 2:] self.reply_domain_name(addr) @_machine.output() def _parse_request_reply(self): "waiting for a reply to our request" # we need at least 6 bytes of data: 4 for the "header", such # as it is, and 2 more if it's DOMAINNAME (for the size) or 4 # or 16 more if it's an IPv4/6 address reply. plus there's 2 # bytes on the end for the bound port. if len(self._data) < 8: return msg = self._data[:4] # not changing self._data yet, in case we've not got # enough bytes so far. (version, reply, _, typ) = struct.unpack('BBBB', msg) if version != 5: self.reply_error(SocksError( "Expected version 5, got {}".format(version))) return if reply != self.SUCCEEDED: self.reply_error(_create_socks_error(reply)) return reply_dispatcher = { self.REPLY_IPV4: self._parse_ipv4_reply, self.REPLY_HOST: self._parse_domain_name_reply, self.REPLY_IPV6: self._parse_ipv6_reply, } try: method = reply_dispatcher[typ] except KeyError: self.reply_error(SocksError( "Unexpected response type {}".format(typ))) return method() @_machine.output() def _make_connection(self, addr, port): "make our proxy connection" sender = self._create_connection(addr, port) # XXX look out! we're depending on this "sender" implementing # certain Twisted APIs, and the state-machine shouldn't depend # on that. # XXX also, if sender implements producer/consumer stuff, we # should register ourselves (and implement it to) -- but this # should really be taking place outside the state-machine in # "the I/O-doing" stuff self._sender = sender self._when_done.fire(sender) @_machine.output() def _domain_name_resolved(self, domain): self._when_done.fire(domain) @_machine.input() def connection(self): "begin the protocol (i.e. connection made)" @_machine.input() def disconnected(self, error): "the connection has gone away" @_machine.input() def got_data(self): "we recevied some data and buffered it" @_machine.input() def version_reply(self, auth_method): "the SOCKS server replied with a version" @_machine.input() def version_error(self, error): "the SOCKS server replied, but we don't understand" @_machine.input() def reply_error(self, error): "the SOCKS server replied with an error" @_machine.input() def reply_ipv4(self, addr, port): "the SOCKS server told me an IPv4 addr, port" @_machine.input() def reply_ipv6(self, addr, port): "the SOCKS server told me an IPv6 addr, port" @_machine.input() def reply_domain_name(self, domain): "the SOCKS server told me a domain-name" @_machine.input() def answer(self): "the SOCKS server replied with an answer" @_machine.output() def _send_version(self): "sends a SOCKS version reply" self._data_to_send( # for anonymous(0) *and* authenticated (2): struct.pack('BBBB', 5, 2, 0, 2) struct.pack('BBB', 5, 1, 0) ) @_machine.output() def _disconnect(self, error): "done" if self._on_disconnect: self._on_disconnect(str(error)) if self._sender: self._sender.connectionLost(Failure(error)) self._when_done.fire(Failure(error)) @_machine.output() def _send_request(self, auth_method): "send the request (connect, resolve or resolve_ptr)" assert auth_method == 0x00 # "no authentication required" return self._dispatch[self._req_type](self) @_machine.output() def _relay_data(self): "relay any data we have" if self._data: d = self._data self._data = b'' # XXX this is "doing I/O" in the state-machine and it # really shouldn't be ... probably want a passed-in # "relay_data" callback or similar? self._sender.dataReceived(d) def _send_connect_request(self): "sends CONNECT request" # XXX needs to support v6 ... or something else does host = self._addr.host port = self._addr.port if isinstance(self._addr, (IPv4Address, IPv6Address)): is_v6 = isinstance(self._addr, IPv6Address) self._data_to_send( struct.pack( '!BBBB4sH', 5, # version 0x01, # command 0x00, # reserved 0x04 if is_v6 else 0x01, inet_pton(AF_INET6 if is_v6 else AF_INET, host), port, ) ) else: host = host.encode('ascii') self._data_to_send( struct.pack( '!BBBBB{}sH'.format(len(host)), 5, # version 0x01, # command 0x00, # reserved 0x03, len(host), host, port, ) ) @_machine.output() def _send_resolve_request(self): "sends RESOLVE_PTR request (Tor custom)" host = self._addr.host.encode() self._data_to_send( struct.pack( '!BBBBB{}sH'.format(len(host)), 5, # version 0xF0, # command 0x00, # reserved 0x03, # DOMAINNAME len(host), host, 0, # self._addr.port? ) ) @_machine.output() def _send_resolve_ptr_request(self): "sends RESOLVE_PTR request (Tor custom)" addr_type = 0x04 if isinstance(self._addr, ipaddress.IPv4Address) else 0x01 encoded_host = inet_aton(self._addr.host) self._data_to_send( struct.pack( '!BBBB4sH', 5, # version 0xF1, # command 0x00, # reserved addr_type, encoded_host, 0, # port; unused? SOCKS is fun ) ) @_machine.state(initial=True) def unconnected(self): "not yet connected" @_machine.state() def sent_version(self): "we've sent our version request" @_machine.state() def sent_request(self): "we've sent our stream/etc request" @_machine.state() def relaying(self): "received our response, now we can relay" @_machine.state() def abort(self, error_message): "we've encountered an error" @_machine.state() def done(self): "operations complete" unconnected.upon( connection, enter=sent_version, outputs=[_send_version], ) sent_version.upon( got_data, enter=sent_version, outputs=[_parse_version_reply], ) sent_version.upon( version_error, enter=abort, outputs=[_disconnect], ) sent_version.upon( version_reply, enter=sent_request, outputs=[_send_request], ) sent_version.upon( disconnected, enter=unconnected, outputs=[_disconnect] ) sent_request.upon( got_data, enter=sent_request, outputs=[_parse_request_reply], ) sent_request.upon( reply_ipv4, enter=relaying, outputs=[_make_connection], ) sent_request.upon( reply_ipv6, enter=relaying, outputs=[_make_connection], ) # XXX this isn't always a _domain_name_resolved -- if we're a # req_type CONNECT then it's _make_connection_domain ... sent_request.upon( reply_domain_name, enter=done, outputs=[_domain_name_resolved], ) sent_request.upon( reply_error, enter=abort, outputs=[_disconnect], ) # XXX FIXME this needs a test sent_request.upon( disconnected, enter=abort, outputs=[_disconnect], # ... or is this redundant? ) relaying.upon( got_data, enter=relaying, outputs=[_relay_data], ) relaying.upon( disconnected, enter=done, outputs=[_disconnect], ) abort.upon( got_data, enter=abort, outputs=[], ) abort.upon( disconnected, enter=abort, outputs=[], ) done.upon( disconnected, enter=done, outputs=[], ) _dispatch = { 'CONNECT': _send_connect_request, 'RESOLVE': _send_resolve_request, 'RESOLVE_PTR': _send_resolve_ptr_request, } class _TorSocksProtocol(Protocol): def __init__(self, host, port, socks_method, factory): self._machine = _SocksMachine( req_type=socks_method, host=host, # noqa unicode() on py3, py2? we want idna, actually? port=port, on_disconnect=self._on_disconnect, on_data=self._on_data, create_connection=self._create_connection, ) self._factory = factory def when_done(self): return self._machine.when_done() def connectionMade(self): self._machine.connection() # we notify via the factory that we have teh # locally-connecting host -- this is e.g. used by the "stream # over one particular circuit" code to determine the local # port that "our" SOCKS connection went to self.factory._did_connect(self.transport.getHost()) def connectionLost(self, reason): self._machine.disconnected(SocksError(reason)) def dataReceived(self, data): self._machine.feed_data(data) def _on_data(self, data): self.transport.write(data) def _create_connection(self, addr, port): addr = IPv4Address('TCP', addr, port) sender = self._factory.buildProtocol(addr) client_proxy = portforward.ProxyClient() sender.makeConnection(self.transport) # portforward.ProxyClient is going to call setPeer but this # probably doesn't have it... setattr(sender, 'setPeer', lambda _: None) client_proxy.setPeer(sender) self._sender = sender return sender def _on_disconnect(self, error_message): self.transport.loseConnection() # self.transport.abortConnection()#SocksError(error_message)) ? class _TorSocksFactory(Factory): protocol = _TorSocksProtocol def __init__(self, *args, **kw): self._args = args self._kw = kw self._host = None self._when_connected = util.SingleObserver() def _get_address(self): """ Returns a Deferred that fires with the transport's getHost() when this SOCKS protocol becomes connected. """ return self._when_connected.when_fired() def _did_connect(self, host): self._host = host self._when_connected.fire(host) def buildProtocol(self, addr): p = self.protocol(*self._args, **self._kw) p.factory = self return p class SocksError(Exception): code = None message = '' def __init__(self, message='', code=None): super(SocksError, self).__init__(message or self.message) self.message = message or self.message self.code = code or self.code class GeneralServerFailureError(SocksError): code = 0x01 message = 'general SOCKS server failure' class ConnectionNotAllowedError(SocksError): code = 0x02 message = 'connection not allowed by ruleset' class NetworkUnreachableError(SocksError): code = 0x03 message = 'Network unreachable' class HostUnreachableError(SocksError): code = 0x04 message = 'Host unreachable' class ConnectionRefusedError(SocksError): code = 0x05 message = 'Connection refused' class TtlExpiredError(SocksError): code = 0x06 message = 'TTL expired' class CommandNotSupportedError(SocksError): code = 0x07 message = 'Command not supported' class AddressTypeNotSupportedError(SocksError): code = 0x08 message = 'Address type not supported' _socks_errors = {cls.code: cls for cls in SocksError.__subclasses__()} def _create_socks_error(code): try: return _socks_errors[code]() except KeyError: return SocksError("Unknown SOCKS error-code {}".format(code), code=code) @inlineCallbacks def resolve(tor_endpoint, hostname): """ This is easier to use via :meth:`txtorcon.Tor.dns_resolve` :param tor_endpoint: the Tor SOCKS endpoint to use. :param hostname: the hostname to look up. """ if six.PY2 and isinstance(hostname, str): hostname = unicode(hostname) # noqa elif six.PY3 and isinstance(hostname, bytes): hostname = hostname.decode('ascii') factory = _TorSocksFactory( hostname, 0, 'RESOLVE', None, ) proto = yield tor_endpoint.connect(factory) result = yield proto.when_done() returnValue(result) @inlineCallbacks def resolve_ptr(tor_endpoint, ip): """ This is easier to use via :meth:`txtorcon.Tor.dns_resolve_ptr` :param tor_endpoint: the Tor SOCKS endpoint to use. :param ip: the IP address to look up. """ if six.PY2 and isinstance(ip, str): ip = unicode(ip) # noqa elif six.PY3 and isinstance(ip, bytes): ip = ip.decode('ascii') factory = _TorSocksFactory( ip, 0, 'RESOLVE_PTR', None, ) proto = yield tor_endpoint.connect(factory) result = yield proto.when_done() returnValue(result) @implementer(IStreamClientEndpoint) class TorSocksEndpoint(object): """ Represents an endpoint which will talk to a Tor SOCKS port. These should usually not be instantiated directly, instead use :meth:`txtorcon.TorConfig.socks_endpoint`. """ # XXX host, port args should be (host, port) tuple, or # IAddress-implementer? def __init__(self, socks_endpoint, host, port, tls=False): self._proxy_ep = socks_endpoint # can be Deferred if six.PY2 and isinstance(host, str): host = unicode(host) # noqa if six.PY3 and isinstance(host, bytes): host = host.decode('ascii') self._host = host self._port = port self._tls = tls self._socks_factory = None self._when_address = util.SingleObserver() def _get_address(self): """ Returns a Deferred that fires with the source IAddress of the underlying SOCKS connection (i.e. usually a twisted.internet.address.IPv4Address) circuit.py uses this; better suggestions welcome! """ return self._when_address.when_fired() @inlineCallbacks def connect(self, factory): # further wrap the protocol if we're doing TLS. # "pray i do not wrap the protocol further". if self._tls: # XXX requires Twisted 14+ from twisted.internet.ssl import optionsForClientTLS context = optionsForClientTLS(self._host) tls_factory = tls.TLSMemoryBIOFactory(context, True, factory) socks_factory = _TorSocksFactory( self._host, self._port, 'CONNECT', tls_factory, ) else: socks_factory = _TorSocksFactory( self._host, self._port, 'CONNECT', factory, ) self._socks_factory = socks_factory # forward our address (when we get it) to any listeners self._socks_factory._get_address().addBoth(self._when_address.fire) # XXX isn't this just maybeDeferred() if isinstance(self._proxy_ep, Deferred): proxy_ep = yield self._proxy_ep else: proxy_ep = self._proxy_ep # socks_proto = yield proxy_ep.connect(socks_factory) proto = yield proxy_ep.connect(socks_factory) wrapped_proto = yield proto.when_done() if self._tls: returnValue(wrapped_proto.wrappedProtocol) else: returnValue(wrapped_proto) txtorcon-0.19.3/txtorcon/attacher.py0000644000175000017500000000561413106645477017430 0ustar mikemike00000000000000import itertools import heapq from zope.interface import implementer from .interface import IStreamAttacher # note to self: might be better to make this Way Simpler and just say # "order matters", *or* do a simple sort -- so that we can actually # remove things. @implementer(IStreamAttacher) class PriorityAttacher(object): """ This can fill the role of an IStreamAttacher to which you can add and remove "sub" attachers. These are consulted in order and the first one to return something besides None wins. We use a "heapq" priority queue, with 0 being the "most important" and higher numbers indicating less important. For example:: tor = yield txtorcon.connect(..) attachers = txtorcon.attacher.PriorityAttacher() @implementer(IStreamAttacher) class MyAttacher(object): def __init__(self, interesting_host, circuit): self._host = interesting_host self._circuit = circuit def attach_stream(self, stream, circuits): if stream.target_host == self._host: return self._circuit return None attachers.add_attacher(MyAttacher('torproject.org', circ1)) attachers.add_attacher(MyAttacher('meejah.ca', circ2)) """ def __init__(self): # use only heapq.* to modify this; 0th item is "smallest" # item. contains 3-tuples of (priority, number, attacher) self._attacher_heap = [] # need to keep a map so we can delete from the priority-queue :( self._attacher_to_entry = dict() # need to keep a counter so the sorting has a tie-breaker self._counter = itertools.count(0, 1) def add_attacher(self, attacher, priority=0): """ Add a new IStreamAttacher at a certain priortiy; lower priority values mean more important (that is, 0 is the most important). """ item = [priority, next(self._counter), IStreamAttacher(attacher)] self._attacher_to_entry[attacher] = item heapq.heappush(self._attacher_heap, item) def remove_attacher(self, attacher): try: item = self._attacher_to_entry.pop(attacher) except KeyError: raise ValueError( "attacher {} not found".format(attacher) ) item[-1] = None # we can't actually remove it from the heap ... def attach_stream_failure(self, stream, fail): pass # hmm, should we try to remember which attacher answered # 'something' for this stream, and then report the failure via # it...? or just log all failures here? def attach_stream(self, stream, circuits): for _, _, attacher in self._attacher_heap: if attacher is not None: answer = attacher.attach_stream(stream, circuits) if answer is not None: return answer return None txtorcon-0.19.3/txtorcon/torconfig.py0000644000175000017500000012634713106646076017632 0ustar mikemike00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import print_function from __future__ import with_statement import os import re import six import functools import warnings from io import StringIO from collections import OrderedDict from twisted.python import log from twisted.python.compat import nativeString from twisted.python.deprecate import deprecated from twisted.internet import defer from twisted.internet.endpoints import TCP4ClientEndpoint, UNIXClientEndpoint from txtorcon.torcontrolprotocol import parse_keywords, DEFAULT_VALUE from txtorcon.torcontrolprotocol import TorProtocolError from txtorcon.interface import ITorControlProtocol from txtorcon.util import find_keywords class _Version(object): """ Replacement for incremental.Version until https://github.com/meejah/txtorcon/issues/233 and/or https://github.com/hawkowl/incremental/issues/31 is fixed. """ # as of latest incremental, it should only access .package and # .short() via the getVersionString() method that Twisted's # deprecated() uses... def __init__(self, package, major, minor, patch): self.package = package self.major = major self.minor = minor self.patch = patch def short(self): return '{}.{}.{}'.format(self.major, self.minor, self.patch) @defer.inlineCallbacks @deprecated(_Version("txtorcon", 0, 18, 0)) def launch_tor(config, reactor, tor_binary=None, progress_updates=None, connection_creator=None, timeout=None, kill_on_stderr=True, stdout=None, stderr=None): """ Deprecated; use launch() instead. See also controller.py """ from .controller import launch # XXX FIXME are we dealing with options in the config "properly" # as far as translating semantics from the old launch_tor to # launch()? DataDirectory, User, ControlPort, ...? tor = yield launch( reactor, stdout=stdout, stderr=stderr, progress_updates=progress_updates, tor_binary=tor_binary, connection_creator=connection_creator, timeout=timeout, kill_on_stderr=kill_on_stderr, _tor_config=config, ) defer.returnValue(tor.process) 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): "Boolean values are stored as 0 or 1." def parse(self, s): if int(s): return True return False def validate(self, s, instance, name): if s: return 1 return 0 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 def validate(self, s, instance, name): # FIXME: Is 'auto' an allowed value? (currently not) s = int(s) if s < 0: return 'auto' elif s: return 1 else: return 0 class Integer(TorConfigType): def parse(self, s): return int(s) def validate(self, s, instance, name): 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__) 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__() if six.PY2: setattr(_ListWrapper, '__setslice__', _wrapture(list.__setslice__)) 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): ''' FIXME can't we just move this to @property decorated methods instead? ''' # For stealth authentication, the .onion is per-client. So in # that case, we really have no choice here -- we can't have # "a" hostname. So we just barf; it's an error to access to # hostname this way. Instead, use .clients.{hostname, cookie} if name == 'private_key': with open(os.path.join(self.dir, name)) as f: data = f.read().strip() self.__dict__[name] = data elif name == 'clients': clients = [] try: with open(os.path.join(self.dir, 'hostname')) as f: for line in f.readlines(): args = line.split() # XXX should be a dict? if len(args) > 1: # tag, onion-uri? clients.append((args[0], args[1])) else: clients.append(('default', args[0])) except IOError: pass self.__dict__[name] = clients elif name == 'hostname': with open(os.path.join(self.dir, name)) as f: data = f.read().strip() host = None for line in data.split('\n'): h = line.split(' ')[0] if host is None: host = h elif h != host: raise RuntimeError( ".hostname accessed on stealth-auth'd hidden-service " "with multiple onion addresses." ) self.__dict__[name] = h 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 port in self.ports: rtn.append(('HiddenServicePort', str(port))) if self.version: rtn.append(('HiddenServiceVersion', str(self.version))) for authline in self.authorize_client: rtn.append(('HiddenServiceAuthorizeClient', str(authline))) return rtn class EphemeralHiddenService(object): ''' This uses the ephemeral hidden-service APIs (in comparison to torrc or SETCONF). This means your hidden-service private-key is never in a file. It also means that when the process exits, that HS goes away. See documentation for ADD_ONION in torspec: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n1295 ''' @classmethod def _is_valid_keyblob(cls, key_blob_or_type): try: key_blob_or_type = nativeString(key_blob_or_type) except (UnicodeError, TypeError): return False else: return re.match(r'[^ :]+:[^ :]+$', key_blob_or_type) # XXX the "ports" stuff is still kind of an awkward API, especialy # making the actual list public (since it'll have # "80,127.0.0.1:80" instead of with a space # XXX descriptor upload stuff needs more features from Tor (the # actual uploaded key; the event always says UNKNOWN) # XXX "auth" is unused (also, no Tor support I don't think?) def __init__(self, ports, key_blob_or_type='NEW:BEST', auth=[], ver=2): if not isinstance(ports, list): ports = [ports] # for "normal" HSes the port-config bit looks like "80 # 127.0.0.1:1234" whereas this one wants a comma, so we leave # the public API the same and fix up the space. Or of course # you can just use the "real" comma-syntax if you wanted. self._ports = [x.replace(' ', ',') for x in ports] if EphemeralHiddenService._is_valid_keyblob(key_blob_or_type): self._key_blob = nativeString(key_blob_or_type) else: raise ValueError( 'key_blob_or_type must be a string in the formats ' '"NEW:" or ":"') self.auth = auth # FIXME ununsed # FIXME nicer than assert, plz assert isinstance(ports, list) @defer.inlineCallbacks def add_to_tor(self, protocol): ''' Returns a Deferred which fires with 'self' after at least one descriptor has been uploaded. Errback if no descriptor upload succeeds. ''' ports = ' '.join(map(lambda x: 'Port=' + x.strip(), self._ports)) cmd = 'ADD_ONION %s %s' % (self._key_blob, ports) ans = yield protocol.queue_command(cmd) ans = find_keywords(ans.split('\n')) self.hostname = ans['ServiceID'] + '.onion' if self._key_blob.startswith('NEW:'): self.private_key = ans['PrivateKey'] else: self.private_key = self._key_blob log.msg('Created hidden-service at', self.hostname) # Now we want to wait for the descriptor uploads. This doesn't # quite work, as the UPLOADED events always say "UNKNOWN" for # the HSAddress so we can't correlate it to *this* onion for # sure :/ "yet", though. Yawning says on IRC this is coming. # XXX Hmm, still UPLOADED always says UNKNOWN, but the UPLOAD # events do say the address -- so we save all those, and # correlate to the target nodes. Not sure if this will really # even work, but better than nothing. uploaded = defer.Deferred() attempted_uploads = set() confirmed_uploads = set() failed_uploads = set() def hs_desc(evt): """ From control-spec: "650" SP "HS_DESC" SP Action SP HSAddress SP AuthType SP HsDir [SP DescriptorID] [SP "REASON=" Reason] [SP "REPLICA=" Replica] """ args = evt.split() subtype = args[0] if subtype == 'UPLOAD': if args[1] == self.hostname[:-6]: attempted_uploads.add(args[3]) elif subtype == 'UPLOADED': # we only need ONE successful upload to happen for the # HS to be reachable. (addr is args[1]) if args[3] in attempted_uploads: confirmed_uploads.add(args[3]) log.msg("Uploaded '{}' to '{}'".format(self.hostname, args[3])) uploaded.callback(self) elif subtype == 'FAILED': if args[1] == self.hostname[:-6]: failed_uploads.add(args[3]) if failed_uploads == attempted_uploads: msg = "Failed to upload '{}' to: {}".format( self.hostname, ', '.join(failed_uploads), ) uploaded.errback(RuntimeError(msg)) log.msg("Created '{}', waiting for descriptor uploads.".format(self.hostname)) yield protocol.add_event_listener('HS_DESC', hs_desc) yield uploaded yield protocol.remove_event_listener('HS_DESC', hs_desc) @defer.inlineCallbacks def remove_from_tor(self, protocol): ''' Returns a Deferred which fires with None ''' r = yield protocol.queue_command('DEL_ONION %s' % self.hostname[:-6]) if r.strip() != 'OK': raise RuntimeError('Failed to remove hidden service: "%s".' % r) def parse_rsa_blob(lines): return 'RSA1024:' + ''.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 def _endpoint_from_socksport_line(reactor, socks_config): """ Internal helper. Returns an IStreamClientEndpoint for the given config, which is of the same format expected by the SOCKSPort option in Tor. """ if socks_config.startswith('unix:'): # XXX wait, can SOCKSPort lines with "unix:/path" still # include options afterwards? What about if the path has a # space in it? return UNIXClientEndpoint(reactor, socks_config[5:]) # options like KeepAliveIsolateSOCKSAuth can be appended # to a SocksPort line... if ' ' in socks_config: socks_config = socks_config.split()[0] if ':' in socks_config: host, port = socks_config.split(':', 1) port = int(port) else: host = '127.0.0.1' port = int(socks_config) return TCP4ClientEndpoint(reactor, host, port) 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? """ @staticmethod @defer.inlineCallbacks def from_protocol(proto): """ This creates and returns a ready-to-go TorConfig instance from the given protocol, which should be an instance of TorControlProtocol. """ cfg = TorConfig(control=proto) yield cfg.post_bootstrap defer.returnValue(cfg) def __init__(self, control=None): self.config = {} '''Current configuration, by keys.''' if control is None: self._protocol = None self.__dict__['_accept_all_'] = None else: self._protocol = ITorControlProtocol(control) self.unsaved = OrderedDict() '''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(self.post_bootstrap.errback) else: self.bootstrap() else: self.do_post_bootstrap(self) self.__dict__['_setup_'] = None def socks_endpoint(self, reactor, port=None): """ Returns a TorSocksEndpoint configured to use an already-configured SOCKSPort from the Tor we're connected to. By default, this will be the very first SOCKSPort. :param port: a str, the first part of the SOCKSPort line (that is, a port like "9151" or a Unix socket config like "unix:/path". You may also specify a port as an int. If you need to use a particular port that may or may not already be configured, see the async method :meth:`txtorcon.TorConfig.create_socks_endpoint` """ if len(self.SocksPort) == 0: raise RuntimeError( "No SOCKS ports configured" ) socks_config = None if port is None: socks_config = self.SocksPort[0] else: port = str(port) # in case e.g. an int passed in if ' ' in port: raise ValueError( "Can't specify options; use create_socks_endpoint instead" ) for idx, port_config in enumerate(self.SocksPort): # "SOCKSPort" is a gnarly beast that can have a bunch # of options appended, so we have to split off the # first thing which *should* be the port (or can be a # string like 'unix:') if port_config.split()[0] == port: socks_config = port_config break if socks_config is None: raise RuntimeError( "No SOCKSPort configured for port {}".format(port) ) return _endpoint_from_socksport_line(reactor, socks_config) @defer.inlineCallbacks def create_socks_endpoint(self, reactor, socks_config): """ Creates a new TorSocksEndpoint instance given a valid configuration line for ``SocksPort``; if this configuration isn't already in the underlying tor, we add it. Note that this method may call :meth:`txtorcon.TorConfig.save()` on this instance. Note that calling this with `socks_config=None` is equivalent to calling `.socks_endpoint` (which is not async). XXX socks_config should be .. i dunno, but there's fucking options and craziness, e.g. default Tor Browser Bundle is: ['9150 IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth', '9155'] XXX maybe we should say "socks_port" as the 3rd arg, insist it's an int, and then allow/support all the other options (e.g. via kwargs) XXX we could avoid the "maybe call .save()" thing; worth it? (actually, no we can't or the Tor won't have it config'd) """ yield self.post_bootstrap if socks_config is None: if len(self.SocksPort) == 0: raise RuntimeError( "socks_port is None and Tor has no SocksPorts configured" ) socks_config = self.SocksPort[0] else: if not any([socks_config in port for port in self.SocksPort]): # need to configure Tor self.SocksPort.append(socks_config) try: yield self.save() except TorProtocolError as e: extra = '' if socks_config.startswith('unix:'): # XXX so why don't we check this for the # caller, earlier on? extra = '\nNote Tor has specific ownership/permissions ' +\ 'requirements for unix sockets and parent dir.' raise RuntimeError( "While configuring SOCKSPort to '{}', error from" " Tor: {}{}".format( socks_config, e, extra ) ) defer.returnValue( _endpoint_from_socksport_line(reactor, socks_config) ) # 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) tor_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__['_accept_all_'] self.__dict__['post_bootstrap'] = defer.Deferred() if proto.post_bootstrap: proto.post_bootstrap.addCallback(self.bootstrap) return self.__dict__['post_bootstrap'] 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. """ # appease flake8's hatred of lambda :/ def has_setup_attr(o): return '_setup_' in o.__dict__ def has_accept_all_attr(o): return '_accept_all_' in o.__dict__ def is_hidden_services(s): return s.lower() == "hiddenservices" if has_setup_attr(self): name = self._find_real_name(name) if not has_accept_all_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 '_accept_all_' 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 '_accept_all_' 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 real_name = self._find_real_name(k) if real_name in self.parsers: v = self.parsers[real_name].parse(v) self.config[real_name] = 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: # FIXME XXX if x is not DEFAULT_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? real_name = self._find_real_name(key) if not isinstance(value, list) and real_name in self.parsers: value = self.parsers[real_name].parse(value) self.config[real_name] = 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 # XXX for Virtual check that it's one of the *Ports things # (because if not it should be an error) if value in ('Dependant', 'Dependent', 'Virtual'): 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 an internal method, but see, e.g., launch_tor()'s implementation if you think you need to use this for something. See :meth:`txtorcon.TorConfig.create_torrc` which returns a string which is also a valid ``torrc`` file ''' everything = dict() everything.update(self.config) everything.update(self.unsaved) for (k, v) in list(everything.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.19.3/examples/0000755000175000017500000000000013111226470015173 5ustar mikemike00000000000000txtorcon-0.19.3/examples/launch_tor_with_simplehttpd.py0000755000175000017500000001073413106645477023403 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/ ''' from __future__ import print_function 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 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, ports=['%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.19.3/examples/launch_tor.py.orig0000755000175000017500000000370113100311040020630 0ustar mikemike00000000000000from __future__ import print_function """ Launch a private Tor instance. """ import sys import txtorcon from twisted.web.client import readBody from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks @react @inlineCallbacks def main(reactor): # note that you can pass a few options as kwargs # (e.g. data_directory=, or socks_port= ). For other torrc # changes, see below. tor = yield txtorcon.launch( reactor, data_directory="./tordata", stdout=sys.stdout, socks_port='unix:/tmp/tor2/socks', ) # tor = yield txtorcon.connect( # reactor, # clientFromString(reactor, "unix:/var/run/tor/control"), # ) print("Connected to Tor version '{}'".format(tor.protocol.version)) config = yield tor.get_config() state = yield tor.create_state() # or state = yield txtorcon.TorState.from_protocol(tor.protocol) print("This Tor has PID {}".format(state.tor_pid)) print("This Tor has the following {} Circuits:".format(len(state.circuits))) for c in state.circuits.values(): print(" {}".format(c)) endpoint_d = config.socks_endpoint(reactor, u'unix:/tmp/tor2/socks') agent = tor.web_agent(socks_endpoint=endpoint_d) uri = b'https://www.torproject.org' print("Downloading {}".format(uri)) resp = yield agent.request(b'GET', uri) print("Response has {} bytes".format(resp.length)) body = yield readBody(resp) print("received body ({} bytes)".format(len(body))) print("{}\n[...]\n{}\n".format(body[:200], body[-200:])) # SOCKSPort is 'really' a list of SOCKS ports in Tor now, so we # have to set it to a list ... :/ print("Changing our config (SOCKSPort=9876)") # config.SOCKSPort = ['unix:/tmp/foo/bar'] config.SOCKSPort = ['9876'] yield config.save() print("Querying to see it changed:") socksport = yield tor.protocol.get_conf("SOCKSPort") print("SOCKSPort", socksport) txtorcon-0.19.3/examples/hello_darkweb.py.orig0000644000175000017500000000000013100311040021256 0ustar mikemike00000000000000txtorcon-0.19.3/examples/connect.py0000644000175000017500000000145413106645477017222 0ustar mikemike00000000000000#!/usr/bin/env python from __future__ import print_function from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks from twisted.internet.endpoints import TCP4ClientEndpoint import txtorcon @react @inlineCallbacks def main(reactor): ep = TCP4ClientEndpoint(reactor, "localhost", 9051) # or (e.g. on Debian): # ep = UNIXClientEndpoint(reactor, "/var/run/tor/control") tor = yield txtorcon.connect(reactor, ep) print("Connected to Tor {version}".format(version=tor.protocol.version)) state = yield tor.create_state() # or: # state = yield txtorcon.TorState.from_protocol(tor.protocol) print("Tor state created. Circuits:") for circuit in state.circuits.values(): print(" {circuit.id}: {circuit.path}".format(circuit=circuit)) txtorcon-0.19.3/examples/launch_tor_endpoint.py.orig0000755000175000017500000000670413100311040022536 0ustar mikemike00000000000000from __future__ import print_function # Here we set up a Twisted Web server and then launch our own 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.web import server, resource from twisted.internet.defer import inlineCallbacks from twisted.internet.task import react, deferLater from twisted.internet.endpoints import serverFromString import txtorcon class Simple(resource.Resource): """ A really simple Web site. """ isLeaf = True def render_GET(self, request): return "Hello, world! I'm a hidden service!" @react @inlineCallbacks def main(reactor): # several ways to proceed here and what they mean: # # "onion:80": # launch a new Tor instance, configure a hidden service on some # port and pubish descriptor for port 80 # # "onion:80:controlPort=9051:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv": # 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. # # "onion:80:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv": # all the same as above, except we launch a new Tor (because no # "controlPort=9051") ep = "onion:80:controlPort=9051:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv" ep = "onion:80:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv" ep = "onion:80" hs_endpoint = serverFromString(reactor, ep) def progress(percent, tag, message): bar = int(percent / 10) print("[{}{}] {}".format("#" * bar, "." * (10 - bar), message)) 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()) port = yield hs_endpoint.listen(site) # XXX new accessor in newer API hs = port.onion_service # "port" is an IAddress implementor, in this case TorOnionAddress # so you can get most useful information from it -- but you can # also access .onion_service (see below) print( "I have set up a hidden service, advertised at:\n" "http://{host}:{port}\n" "locally listening on {local_address}\n" "Will stop in 60 seconds...".format( host=port.getHost().onion_uri, # or hs.hostname port=port.public_port, # port.local_address will be a twisted.internet.tcp.Port # or a twisted.internet.unix.Port -- both have .getHost() local_address=port.local_address.getHost(), ) ) # if you prefer, hs (port.onion_service) is an instance providing # IOnionService (there's no way to do authenticated services via # endpoints yet, but if there was then this would implement # IOnionClients instead) print("private key:\n{}".format(hs.private_key)) def sleep(s): return deferLater(reactor, s, lambda: None) yield sleep(50) for i in range(10): print("Stopping in {}...".format(10 - i)) yield sleep(1) txtorcon-0.19.3/examples/web_client_custom_circuit.py.orig0000644000175000017500000000576113100310526023736 0ustar mikemike00000000000000# this example shows how to use specific circuits over Tor (with # Twisted's web client or with a custom protocol) # # NOTE WELL: this functionality is for advanced use-cases and if you # do anything "special" to select your circuit hops you risk making it # easy to de-anonymize this (and all other) Tor circuits. from __future__ import print_function from twisted.internet.protocol import Protocol, Factory from twisted.internet.defer import inlineCallbacks, Deferred from twisted.internet.task import react from twisted.internet.endpoints import TCP4ClientEndpoint from twisted.web.client import readBody import txtorcon from txtorcon.util import default_control_port @react @inlineCallbacks def main(reactor): # use port 9051 for system tor instances, or: # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control') ep = TCP4ClientEndpoint(reactor, '127.0.0.1', default_control_port()) tor = yield txtorcon.connect(reactor, ep) print("Connected:", tor) config = yield tor.get_config() state = yield tor.create_state() socks = config.socks_endpoint(reactor) # create a custom circuit; in this case we're just letting Tor # decide the path but you *can* select a path (again: for advanced # use cases that will probably de-anonymize you) circ = yield state.build_circuit() print("Building a circuit:", circ) # at this point, the circuit will be "under way" but may not yet # be in BUILT state -- and hence usable. So, we wait. (Just for # demo purposes: the underlying connect will wait too) yield circ.when_built() print("Circuit is ready:", circ) if True: # create a web.Agent that will use this circuit (or fail) agent = circ.web_agent(reactor, socks) uri = 'https://www.torproject.org' print("Downloading {}".format(uri)) resp = yield agent.request('GET', uri) print("Response has {} bytes".format(resp.length)) body = yield readBody(resp) print("received body ({} bytes)".format(len(body))) print("{}\n[...]\n{}\n".format(body[:200], body[-200:])) if True: # make a plain TCP connection to a thing ep = circ.stream_via(reactor, 'torproject.org', 80, config.socks_endpoint(reactor)) d = Deferred() class ToyWebRequestProtocol(Protocol): def connectionMade(self): print("Connected via {}".format(self.transport.getHost())) self.transport.write( 'GET http://torproject.org/ HTTP/1.1\r\n' 'Host: torproject.org\r\n' '\r\n' ) def dataReceived(self, d): print(" received {} bytes".format(len(d))) def connectionLost(self, reason): print("disconnected: {}".format(reason.value)) d.callback(None) proto = yield ep.connect(Factory.forProtocol(ToyWebRequestProtocol)) yield d print("All done, closing the circuit") yield circ.close() txtorcon-0.19.3/examples/launch_tor.py0000755000175000017500000000370113106645477017727 0ustar mikemike00000000000000from __future__ import print_function """ Launch a private Tor instance. """ import sys import txtorcon from twisted.web.client import readBody from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks @react @inlineCallbacks def main(reactor): # note that you can pass a few options as kwargs # (e.g. data_directory=, or socks_port= ). For other torrc # changes, see below. tor = yield txtorcon.launch( reactor, data_directory="./tordata", stdout=sys.stdout, socks_port='unix:/tmp/tor2/socks', ) # tor = yield txtorcon.connect( # reactor, # clientFromString(reactor, "unix:/var/run/tor/control"), # ) print("Connected to Tor version '{}'".format(tor.protocol.version)) config = yield tor.get_config() state = yield tor.create_state() # or state = yield txtorcon.TorState.from_protocol(tor.protocol) print("This Tor has PID {}".format(state.tor_pid)) print("This Tor has the following {} Circuits:".format(len(state.circuits))) for c in state.circuits.values(): print(" {}".format(c)) endpoint_d = config.socks_endpoint(reactor, u'unix:/tmp/tor2/socks') agent = tor.web_agent(socks_endpoint=endpoint_d) uri = b'https://www.torproject.org' print("Downloading {}".format(uri)) resp = yield agent.request(b'GET', uri) print("Response has {} bytes".format(resp.length)) body = yield readBody(resp) print("received body ({} bytes)".format(len(body))) print("{}\n[...]\n{}\n".format(body[:200], body[-200:])) # SOCKSPort is 'really' a list of SOCKS ports in Tor now, so we # have to set it to a list ... :/ print("Changing our config (SOCKSPort=9876)") # config.SOCKSPort = ['unix:/tmp/foo/bar'] config.SOCKSPort = ['9876'] yield config.save() print("Querying to see it changed:") socksport = yield tor.protocol.get_conf("SOCKSPort") print("SOCKSPort", socksport) txtorcon-0.19.3/examples/stem_relay_descriptor.py0000755000175000017500000000360213106645477022173 0ustar mikemike00000000000000#!/usr/bin/env python # This shows how to get the detailed information about a # relay descriptor and parse it into Stem's RelayDescriptor # 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. # # Also you need to configure Tor to actually download these # descriptors -- by default Tor only downloads "microdescriptors" # (whose information is already available live via txtorcon.Router # instances). Set "UseMicrodescriptors 0" to download "full" descriptors from __future__ import print_function from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks import txtorcon try: from stem.descriptor.server_descriptor import RelayDescriptor except ImportError: print("You must install 'stem' to use this example:") print(" pip install stem") raise SystemExit(1) @react @inlineCallbacks def main(reactor): tor = yield txtorcon.connect(reactor) or_nickname = "moria1" print("Trying to get decriptor information about '{}'".format(or_nickname)) # If the fingerprint is used in place of nickname then, desc/id/ # should be used. try: descriptor_info = yield tor.protocol.get_info('desc/name/' + or_nickname) except txtorcon.TorProtocolError: print("No information found. Enable descriptor downloading by setting:") print(" UseMicrodescritors 0") print("In your torrc") raise SystemExit(1) descriptor_info = descriptor_info.values()[0] relay_info = RelayDescriptor(descriptor_info) print("The relay's fingerprint is: {}".format(relay_info.fingerprint)) print("Time in UTC when the descriptor was made: {}".format(relay_info.published)) txtorcon-0.19.3/examples/launch_tor_endpoint.py0000755000175000017500000000670413106645477021635 0ustar mikemike00000000000000from __future__ import print_function # Here we set up a Twisted Web server and then launch our own 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.web import server, resource from twisted.internet.defer import inlineCallbacks from twisted.internet.task import react, deferLater from twisted.internet.endpoints import serverFromString import txtorcon class Simple(resource.Resource): """ A really simple Web site. """ isLeaf = True def render_GET(self, request): return "Hello, world! I'm a hidden service!" @react @inlineCallbacks def main(reactor): # several ways to proceed here and what they mean: # # "onion:80": # launch a new Tor instance, configure a hidden service on some # port and pubish descriptor for port 80 # # "onion:80:controlPort=9051:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv": # 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. # # "onion:80:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv": # all the same as above, except we launch a new Tor (because no # "controlPort=9051") ep = "onion:80:controlPort=9051:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv" ep = "onion:80:localPort=8080:socksPort=9089:hiddenServiceDir=/home/human/src/txtorcon/hidserv" ep = "onion:80" hs_endpoint = serverFromString(reactor, ep) def progress(percent, tag, message): bar = int(percent / 10) print("[{}{}] {}".format("#" * bar, "." * (10 - bar), message)) 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()) port = yield hs_endpoint.listen(site) # XXX new accessor in newer API hs = port.onion_service # "port" is an IAddress implementor, in this case TorOnionAddress # so you can get most useful information from it -- but you can # also access .onion_service (see below) print( "I have set up a hidden service, advertised at:\n" "http://{host}:{port}\n" "locally listening on {local_address}\n" "Will stop in 60 seconds...".format( host=port.getHost().onion_uri, # or hs.hostname port=port.public_port, # port.local_address will be a twisted.internet.tcp.Port # or a twisted.internet.unix.Port -- both have .getHost() local_address=port.local_address.getHost(), ) ) # if you prefer, hs (port.onion_service) is an instance providing # IOnionService (there's no way to do authenticated services via # endpoints yet, but if there was then this would implement # IOnionClients instead) print("private key:\n{}".format(hs.private_key)) def sleep(s): return deferLater(reactor, s, lambda: None) yield sleep(50) for i in range(10): print("Stopping in {}...".format(10 - i)) yield sleep(1) txtorcon-0.19.3/examples/circuit_for_next_stream.py.orig0000644000175000017500000000000013100311040023375 0ustar mikemike00000000000000txtorcon-0.19.3/examples/stream_circuit_logger.py0000755000175000017500000000510213106645477022142 0ustar mikemike00000000000000#!/usr/bin/env python # This uses an IStreamListener and an ICircuitListener to log all # built circuits and all streams that succeed. from __future__ import print_function import sys from twisted.python import log from twisted.internet import reactor from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks, Deferred import txtorcon def log_circuit(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 log_stream(stream): circ = '' if stream.circuit: path = '->'.join(map(lambda x: str(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, ) 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 stream_attach(self, stream, circuit): log_stream(stream) def stream_failed(self, stream, reason='', remote_reason='', **kw): print('Stream %d failed because "%s"' % (stream.id, remote_reason)) def circuit_built(self, circuit): log_circuit(circuit) def circuit_failed(self, circuit, **kw): log.msg('Circuit %d failed "%s"' % (circuit.id, kw['REASON'])) @react @inlineCallbacks def main(reactor): log.startLogging(sys.stdout) tor = yield txtorcon.connect(reactor) log.msg('Connected to a Tor version %s' % tor.protocol.version) state = yield tor.create_state() listener = StreamCircuitLogger() state.add_circuit_listener(listener) state.add_stream_listener(listener) tor.protocol.add_event_listener('STATUS_GENERAL', log.msg) tor.protocol.add_event_listener('STATUS_SERVER', log.msg) tor.protocol.add_event_listener('STATUS_CLIENT', log.msg) log.msg('Existing state when we connected:') for s in state.streams.values(): log_stream(s) log.msg('Existing circuits:') for c in state.circuits.values(): log_circuit(c) yield Deferred() txtorcon-0.19.3/examples/connect.py.orig0000644000175000017500000000145413100310526020133 0ustar mikemike00000000000000#!/usr/bin/env python from __future__ import print_function from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks from twisted.internet.endpoints import TCP4ClientEndpoint import txtorcon @react @inlineCallbacks def main(reactor): ep = TCP4ClientEndpoint(reactor, "localhost", 9051) # or (e.g. on Debian): # ep = UNIXClientEndpoint(reactor, "/var/run/tor/control") tor = yield txtorcon.connect(reactor, ep) print("Connected to Tor {version}".format(version=tor.protocol.version)) state = yield tor.create_state() # or: # state = yield txtorcon.TorState.from_protocol(tor.protocol) print("Tor state created. Circuits:") for circuit in state.circuits.values(): print(" {circuit.id}: {circuit.path}".format(circuit=circuit)) txtorcon-0.19.3/examples/hidden_echo.py.orig0000644000175000017500000000203113100171552020727 0ustar mikemike00000000000000from __future__ import print_function from twisted.internet import protocol, reactor, endpoints # like the echo-server example on the front page of # https://twistedmatrix.com except this makes a Tor onion-service # that's an echo server. # # Note the *only* difference is the string we give to "serverFromString"! class Echo(protocol.Protocol): def connectionMade(self): print("Connection from {}".format(self.transport.getHost())) def dataReceived(self, data): print("echoing: '{}'".format(repr(data))) self.transport.write(data) class EchoFactory(protocol.Factory): def buildProtocol(self, addr): return Echo() print("Starting Tor, and onion service (can take a few minutes)") d = endpoints.serverFromString(reactor, "onion:1234").listen(EchoFactory()) def listening(port): # port is a Twisted IListeningPort print("Listening on: {} port 1234".format(port.getHost())) print("Try: torsocks telnet {} 1234".format(port.getHost().onion_uri)) d.addCallback(listening) reactor.run() txtorcon-0.19.3/examples/readme.py0000644000175000017500000000236313106645477017026 0ustar mikemike00000000000000from __future__ import print_function from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks from twisted.internet.endpoints import UNIXClientEndpoint import treq import txtorcon @react @inlineCallbacks def main(reactor): tor = yield txtorcon.connect( reactor, UNIXClientEndpoint(reactor, "/var/run/tor/control") ) print("Connected to Tor version {}".format(tor.version)) url = 'https://www.torproject.org:443' print("Downloading {}".format(url)) resp = yield treq.get(url, agent=tor.web_agent()) print(" {} bytes".format(resp.length)) data = yield resp.text() print("Got {} bytes:\n{}\n[...]{}".format( len(data), data[:120], data[-120:], )) print("Creating a circuit") state = yield tor.create_state() circ = yield state.build_circuit() yield circ.when_built() print(" path: {}".format(" -> ".join([r.ip for r in circ.path]))) print("Downloading meejah's public key via above circuit...") config = yield tor.get_config() resp = yield treq.get( 'https://meejah.ca/meejah.asc', agent=circ.web_agent(reactor, config.socks_endpoint(reactor)), ) data = yield resp.text() print(data) txtorcon-0.19.3/examples/launch_tor_endpoint2.py.orig0000755000175000017500000000267113100310526022627 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 __future__ import print_function 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.19.3/examples/add_hiddenservice_to_system_tor.py.orig0000644000175000017500000000000013100311040025072 0ustar mikemike00000000000000txtorcon-0.19.3/examples/web_client_treq.py.orig0000644000175000017500000000207513100310526021650 0ustar mikemike00000000000000# just copying over most of "carml checkpypi" because it's a good # example of "I want a stream over *this* circuit". from __future__ import print_function from twisted.internet.defer import inlineCallbacks from twisted.internet.task import react from twisted.internet.endpoints import TCP4ClientEndpoint import txtorcon from txtorcon.util import default_control_port try: import treq except ImportError: print("To use this example, please install 'treq':") print("pip install treq") raise SystemExit(1) @react @inlineCallbacks def main(reactor): ep = TCP4ClientEndpoint(reactor, '127.0.0.1', default_control_port()) # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control') tor = yield txtorcon.connect(reactor, ep) print("Connected:", tor) resp = yield treq.get( 'https://www.torproject.org:443', agent=tor.web_agent(), ) print("Retrieving {} bytes".format(resp.length)) data = yield resp.text() print("Got {} bytes:\n{}\n[...]{}".format( len(data), data[:120], data[-120:], )) txtorcon-0.19.3/examples/hidden_echo.py0000644000175000017500000000203113100311040017754 0ustar mikemike00000000000000from __future__ import print_function from twisted.internet import protocol, reactor, endpoints # like the echo-server example on the front page of # https://twistedmatrix.com except this makes a Tor onion-service # that's an echo server. # # Note the *only* difference is the string we give to "serverFromString"! class Echo(protocol.Protocol): def connectionMade(self): print("Connection from {}".format(self.transport.getHost())) def dataReceived(self, data): print("echoing: '{}'".format(repr(data))) self.transport.write(data) class EchoFactory(protocol.Factory): def buildProtocol(self, addr): return Echo() print("Starting Tor, and onion service (can take a few minutes)") d = endpoints.serverFromString(reactor, "onion:1234").listen(EchoFactory()) def listening(port): # port is a Twisted IListeningPort print("Listening on: {} port 1234".format(port.getHost())) print("Try: torsocks telnet {} 1234".format(port.getHost().onion_uri)) d.addCallback(listening) reactor.run() txtorcon-0.19.3/examples/launch_tor_unix_sockets.py0000644000175000017500000000376013106645477022527 0ustar mikemike00000000000000from __future__ import print_function """ Use the 'global_tor' instance from txtorcon; this is a Tor instance that either doesn't exist or is unique to this process' txtorcon library (i.e. a singleton for this process) """ import sys import txtorcon import tempfile import shutil from os import mkdir, chmod from os.path import join from twisted.web.client import readBody from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks @react @inlineCallbacks def main(reactor): # we must have a directory owned by us with 0700 permissions to # contain our Unix sockets or Tor is sad. tmp = tempfile.mkdtemp() reactor.addSystemEventTrigger( 'after', 'shutdown', shutil.rmtree, tmp, ) control_path = join(tmp, 'control_socket') socks_path = join(tmp, 'socks') # note that you can pass a few options as kwargs # (e.g. data_directory=, or socks_port= ). For other torrc # changes, see below. tor = yield txtorcon.launch( reactor, data_directory="./tordata", stdout=sys.stdout, control_port='unix:{}'.format(control_path), socks_port='unix:{}'.format(socks_path), ) print("Connected to Tor version '{}'".format(tor.protocol.version)) state = yield tor.create_state() print("This Tor has PID {}".format(state.tor_pid)) print("This Tor has the following {} Circuits:".format(len(state.circuits))) for c in state.circuits.values(): print(" {}".format(c)) config = yield tor.get_config() socks_ep = config.create_socks_endpoint(reactor, u'unix:{}'.format(socks_path)) agent = tor.web_agent(socks_endpoint=socks_ep) uri = b'https://www.torproject.org' print("Downloading {} via Unix socket".format(uri)) resp = yield agent.request(b'GET', uri) print("Response has {} bytes".format(resp.length)) body = yield readBody(resp) print("received body ({} bytes)".format(len(body))) print("{}\n[...]\n{}\n".format(body[:200], body[-200:])) txtorcon-0.19.3/examples/web_client_treq.py0000644000175000017500000000207513106645477020737 0ustar mikemike00000000000000# just copying over most of "carml checkpypi" because it's a good # example of "I want a stream over *this* circuit". from __future__ import print_function from twisted.internet.defer import inlineCallbacks from twisted.internet.task import react from twisted.internet.endpoints import TCP4ClientEndpoint import txtorcon from txtorcon.util import default_control_port try: import treq except ImportError: print("To use this example, please install 'treq':") print("pip install treq") raise SystemExit(1) @react @inlineCallbacks def main(reactor): ep = TCP4ClientEndpoint(reactor, '127.0.0.1', default_control_port()) # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control') tor = yield txtorcon.connect(reactor, ep) print("Connected:", tor) resp = yield treq.get( 'https://www.torproject.org:443', agent=tor.web_agent(), ) print("Retrieving {} bytes".format(resp.length)) data = yield resp.text() print("Got {} bytes:\n{}\n[...]{}".format( len(data), data[:120], data[-120:], )) txtorcon-0.19.3/examples/launch_tor_endpoint2.py0000755000175000017500000000267113106645477021716 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 __future__ import print_function 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.19.3/examples/tor_info.py0000755000175000017500000000506113106645477017411 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. from __future__ import print_function 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, end=' ') 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 as 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.19.3/examples/disallow_streams_by_port.py0000755000175000017500000000376213106645477022712 0ustar mikemike00000000000000from __future__ import print_function # # 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.task import react from twisted.internet.defer import inlineCallbacks, Deferred from twisted.internet.endpoints import clientFromString from zope.interface import implementer import txtorcon @implementer(txtorcon.IStreamAttacher) class PortFilterAttacher: def __init__(self, state): self.state = state self.disallow_ports = [80, 25] print( "Disallowing all streams to ports: {ports}".format( ports=",".join(map(str, self.disallow_ports)), ) ) def attach_stream(self, stream, circuits): """ IStreamAttacher API """ def stream_closed(x): print("Stream closed:", x) if stream.target_port in self.disallow_ports: print( "Disallowing {stream} to port {stream.target_port}".format( stream=stream, ) ) 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 @react @inlineCallbacks def main(reactor): control_ep = clientFromString(reactor, "tcp:localhost:9051") tor = yield txtorcon.connect(reactor, control_ep) print("Connected to a Tor version={version}".format( version=tor.protocol.version, )) state = yield tor.create_state() yield state.set_attacher(PortFilterAttacher(state), reactor) print("Existing streams:") for s in state.streams.values(): print(" ", s) yield Deferred() txtorcon-0.19.3/examples/circuit_failure_rates.py.orig0000644000175000017500000000000013100311040023023 0ustar mikemike00000000000000txtorcon-0.19.3/examples/web_client.py0000644000175000017500000000303213106645477017676 0ustar mikemike00000000000000# this example shows how to use Twisted's web client with Tor via # txtorcon from __future__ import print_function from twisted.internet.defer import inlineCallbacks from twisted.internet.task import react from twisted.internet.endpoints import TCP4ClientEndpoint from twisted.web.client import readBody import txtorcon from txtorcon.util import default_control_port @react @inlineCallbacks def main(reactor): # use port 9051 for system tor instances, or: # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control') # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control') ep = TCP4ClientEndpoint(reactor, '127.0.0.1', default_control_port()) tor = yield txtorcon.connect(reactor, ep) print("Connected to {tor} via localhost:{port}".format( tor=tor, port=default_control_port(), )) # create a web.Agent that will talk via Tor. If the socks port # given isn't yet configured, this will do so. It may also be # None, which means "the first configured SOCKSPort" # agent = tor.web_agent(u'9999') agent = tor.web_agent() uri = b'http://surely-this-has-not-been-registered-and-is-invalid.com' uri = b'https://www.torproject.org' uri = b'http://timaq4ygg2iegci7.onion/' # txtorcon documentation print("Downloading {}".format(uri)) resp = yield agent.request(b'GET', uri) print("Response has {} bytes".format(resp.length)) body = yield readBody(resp) print("received body ({} bytes)".format(len(body))) print("{}\n[...]\n{}\n".format(body[:200], body[-200:])) txtorcon-0.19.3/examples/minimal_endpoint.py0000755000175000017500000000051713100171552021077 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.19.3/examples/dump_config.py.orig0000644000175000017500000000000013100311040020746 0ustar mikemike00000000000000txtorcon-0.19.3/examples/web_client_custom_circuit.py0000644000175000017500000000576113106645477023025 0ustar mikemike00000000000000# this example shows how to use specific circuits over Tor (with # Twisted's web client or with a custom protocol) # # NOTE WELL: this functionality is for advanced use-cases and if you # do anything "special" to select your circuit hops you risk making it # easy to de-anonymize this (and all other) Tor circuits. from __future__ import print_function from twisted.internet.protocol import Protocol, Factory from twisted.internet.defer import inlineCallbacks, Deferred from twisted.internet.task import react from twisted.internet.endpoints import TCP4ClientEndpoint from twisted.web.client import readBody import txtorcon from txtorcon.util import default_control_port @react @inlineCallbacks def main(reactor): # use port 9051 for system tor instances, or: # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control') ep = TCP4ClientEndpoint(reactor, '127.0.0.1', default_control_port()) tor = yield txtorcon.connect(reactor, ep) print("Connected:", tor) config = yield tor.get_config() state = yield tor.create_state() socks = config.socks_endpoint(reactor) # create a custom circuit; in this case we're just letting Tor # decide the path but you *can* select a path (again: for advanced # use cases that will probably de-anonymize you) circ = yield state.build_circuit() print("Building a circuit:", circ) # at this point, the circuit will be "under way" but may not yet # be in BUILT state -- and hence usable. So, we wait. (Just for # demo purposes: the underlying connect will wait too) yield circ.when_built() print("Circuit is ready:", circ) if True: # create a web.Agent that will use this circuit (or fail) agent = circ.web_agent(reactor, socks) uri = 'https://www.torproject.org' print("Downloading {}".format(uri)) resp = yield agent.request('GET', uri) print("Response has {} bytes".format(resp.length)) body = yield readBody(resp) print("received body ({} bytes)".format(len(body))) print("{}\n[...]\n{}\n".format(body[:200], body[-200:])) if True: # make a plain TCP connection to a thing ep = circ.stream_via(reactor, 'torproject.org', 80, config.socks_endpoint(reactor)) d = Deferred() class ToyWebRequestProtocol(Protocol): def connectionMade(self): print("Connected via {}".format(self.transport.getHost())) self.transport.write( 'GET http://torproject.org/ HTTP/1.1\r\n' 'Host: torproject.org\r\n' '\r\n' ) def dataReceived(self, d): print(" received {} bytes".format(len(d))) def connectionLost(self, reason): print("disconnected: {}".format(reason.value)) d.callback(None) proto = yield ep.connect(Factory.forProtocol(ToyWebRequestProtocol)) yield d print("All done, closing the circuit") yield circ.close() txtorcon-0.19.3/examples/attach_streams_by_country.py.orig0000644000175000017500000000000013100311040023733 0ustar mikemike00000000000000txtorcon-0.19.3/examples/monitor.py0000755000175000017500000000162713106645477017265 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 __future__ import print_function from twisted.internet import task, defer from twisted.internet.endpoints import UNIXClientEndpoint import txtorcon @task.react @defer.inlineCallbacks def main(reactor): ep = UNIXClientEndpoint(reactor, '/var/run/tor/control') tor = yield txtorcon.connect(reactor, ep) def log(msg): print(msg) print("Connected to a Tor version", tor.protocol.version) for event in ['INFO', 'NOTICE', 'WARN', 'ERR']: tor.protocol.add_event_listener(event, log) is_current = yield tor.protocol.get_info('status/version/current') version = yield tor.protocol.get_info('version') print("Version '{}', is_current={}".format(version, is_current['status/version/current'])) yield defer.Deferred() txtorcon-0.19.3/examples/dns_lookups.py0000644000175000017500000000127513106645477020132 0ustar mikemike00000000000000from __future__ import print_function from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks from twisted.internet.endpoints import clientFromString import txtorcon @react @inlineCallbacks def main(reactor): control_ep = clientFromString(reactor, "tcp:localhost:9051") tor = yield txtorcon.connect(reactor, control_ep) for domain in ['torproject.org', 'meejah.ca']: print("Looking up '{}' via Tor".format(domain)) ans = yield tor.dns_resolve(domain) print("...got answer: {}".format(ans)) print("Doing PTR on {}".format(ans)) ans = yield tor.dns_resolve_ptr(ans) print("...got answer: {}".format(ans)) txtorcon-0.19.3/examples/dns_lookups.py.orig0000644000175000017500000000127513100310526021043 0ustar mikemike00000000000000from __future__ import print_function from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks from twisted.internet.endpoints import clientFromString import txtorcon @react @inlineCallbacks def main(reactor): control_ep = clientFromString(reactor, "tcp:localhost:9051") tor = yield txtorcon.connect(reactor, control_ep) for domain in ['torproject.org', 'meejah.ca']: print("Looking up '{}' via Tor".format(domain)) ans = yield tor.dns_resolve(domain) print("...got answer: {}".format(ans)) print("Doing PTR on {}".format(ans)) ans = yield tor.dns_resolve_ptr(ans) print("...got answer: {}".format(ans)) txtorcon-0.19.3/examples/launch_tor2web.py0000644000175000017500000000247313106645477020511 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. from __future__ import print_function 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.19.3/examples/webui_server.py0000755000175000017500000000701613106645477020275 0ustar mikemike00000000000000#!/usr/bin/env python from __future__ import print_function 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.19.3/examples/launch_tor_with_hiddenservice.py.orig0000644000175000017500000000000013100311040024541 0ustar mikemike00000000000000txtorcon-0.19.3/examples/readme3.py0000644000175000017500000000252513106645477017111 0ustar mikemike00000000000000# this is a Python3 version of the code in readme.py from twisted.internet.task import react from twisted.internet.defer import inlineCallbacks, ensureDeferred from twisted.internet.endpoints import UNIXClientEndpoint import treq import txtorcon async def main(reactor): tor = await txtorcon.connect( reactor, UNIXClientEndpoint(reactor, "/var/run/tor/control") ) print("Connected to Tor version {}".format(tor.version)) url = u'https://www.torproject.org:443' print(u"Downloading {}".format(repr(url))) resp = await treq.get(url, agent=tor.web_agent()) print(u" {} bytes".format(resp.length)) data = await resp.text() print(u"Got {} bytes:\n{}\n[...]{}".format( len(data), data[:120], data[-120:], )) print(u"Creating a circuit") state = await tor.create_state() circ = await state.build_circuit() await circ.when_built() print(u" path: {}".format(" -> ".join([r.ip for r in circ.path]))) print(u"Downloading meejah's public key via above circuit...") config = await tor.get_config() resp = await treq.get( u'https://meejah.ca/meejah.asc', agent=circ.web_agent(reactor, config.socks_endpoint(reactor)), ) data = await resp.text() print(data) @react def _main(reactor): return ensureDeferred(main(reactor)) txtorcon-0.19.3/examples/txtorcon.tac0000644000175000017500000000314412752747562017572 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.19.3/examples/web_client.py.orig0000644000175000017500000000303213100310526020607 0ustar mikemike00000000000000# this example shows how to use Twisted's web client with Tor via # txtorcon from __future__ import print_function from twisted.internet.defer import inlineCallbacks from twisted.internet.task import react from twisted.internet.endpoints import TCP4ClientEndpoint from twisted.web.client import readBody import txtorcon from txtorcon.util import default_control_port @react @inlineCallbacks def main(reactor): # use port 9051 for system tor instances, or: # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control') # ep = UNIXClientEndpoint(reactor, '/var/run/tor/control') ep = TCP4ClientEndpoint(reactor, '127.0.0.1', default_control_port()) tor = yield txtorcon.connect(reactor, ep) print("Connected to {tor} via localhost:{port}".format( tor=tor, port=default_control_port(), )) # create a web.Agent that will talk via Tor. If the socks port # given isn't yet configured, this will do so. It may also be # None, which means "the first configured SOCKSPort" # agent = tor.web_agent(u'9999') agent = tor.web_agent() uri = b'http://surely-this-has-not-been-registered-and-is-invalid.com' uri = b'https://www.torproject.org' uri = b'http://timaq4ygg2iegci7.onion/' # txtorcon documentation print("Downloading {}".format(uri)) resp = yield agent.request(b'GET', uri) print("Response has {} bytes".format(resp.length)) body = yield readBody(resp) print("received body ({} bytes)".format(len(body))) print("{}\n[...]\n{}\n".format(body[:200], body[-200:])) txtorcon-0.19.3/docs/0000755000175000017500000000000013111226470014305 5ustar mikemike00000000000000txtorcon-0.19.3/docs/installing.rst0000644000175000017500000001645313106645477017234 0ustar mikemike00000000000000.. _installing: Installing txtorcon =================== Latest Release -------------- txtorcon is on PyPI and in Debian since `jessie `_ (thanks to Lunar and now `irl `_!). So, one of these should work: - install latest release: ``pip install txtorcon`` - Debian or Ubuntu: ``apt-get install python-txtorcon`` - Watch an `asciinema demo `_ for an overview. Rendered documentation for the latest release is at `txtorcon.readthedocs.org `_. What exists for release-notes are in ":ref:`releases`". If you're still using wheezy, ``python-txtorcon`` is also in `wheezy-backports `_. To install, do this as root: .. sourcecode:: shell-session # 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). I am told this package also needs a maintainer; see XXX. **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-*.whl Compatibility ------------- txtorcon runs all tests cleanly under Python2, Python3 and PyPy on: - Debian: "squeeze", "wheezy" and "jessie" - OS X: 10.4 (naif), 10.8 (lukas lueg), 10.9 (kurt neufeld) - Fedora 18 (lukas lueg) - FreeBSD 10 (enrique fynn) (**needed to install "lsof"**) - RHEL6 - **Reports from other OSes appreciated.** .. _configure_tor: Tor Configuration ----------------- Using Tor's cookie authentication is the most convenient way to connect; this proves that your user can read a cookie file written by Tor. To enable this, you'll want to have the following options on in your ``torrc``:: CookieAuthentication 1 CookieAuthFileGroupReadable 1 Note that "Tor BrowserBundle" is configured this way by default, on port 9151. If you want to use unix sockets to speak to tor (highly recommended) add this to your config (Debian is already set up like this):: ControlSocketsGroupWritable 1 ControlSocket /var/run/tor/control Source Code ----------- Most people will use the code from https://github.com/meejah/txtorcon The canonical URI is http://timaq4ygg2iegci7.onion I sign tags with my public key (:download:`meejah.asc <../meejah.asc>`) - ``git clone https://github.com/meejah/txtorcon.git`` - ``torsocks git clone git://timaq4ygg2iegci7.onion/meejah/txtorcon.git`` Rendered documentation for the latest release is at `txtorcon.readthedocs.org `_. See :ref:`hacking` if you wish to contribute back to txtorcon :) Development Environment ----------------------- I like to set up my Python development like this: .. code-block:: shell-session $ git clone https://github.com/meejah/txtorcon.git $ echo "if you later fork it on github, do this:" $ git remote add -f github git+ssh://git@github.com//txtorcon.git $ cd txtorcon $ virtualenv venv $ source venv/bin/activate (venv)$ pip install --editable .[dev] # "dev" adds more deps, like Sphinx (venv)$ make doc (venv)$ make test (venv)$ tox # run all tests, in all supported configs You can now edit code in the repository as normal. To submit a patch, the easiest way is to "clone" the txtorcon project, then "fork" on github and add a remote called "github" with your copy of the code to which you can push (``git remote add -f github git+ssh://git@github.com//txtorcon.git``). The ``-f`` is so you don't have to run ``git fetch`` right after. Now, you can push a new branch you've made to GitHub with ``git push github branch-name`` and then examine it and open a pull-request. This will trigger Travis to run the tests, after which coverage will be produced (and a bot comments on the pull-request). If you require any more changes, the easiest thing to do is just commit them and push them. (If you know how, re-basing/re-arranging/squashing etc is nice to do too). See :ref:`hacking` for more. Integration Tests ----------------- There are a couple of simple integration tests using Docker in the ``integration/`` directory; these make a ``debootstrap``-built base image and then do the test inside containers cloned from this -- no trusting ``https://docker.io`` required. See ``integration/README`` for more information. If you're on Debian, there's a decent chance running ``make txtorcon-tester`` followed by ``make integration`` from the root of the checkout will work (the first commands ultimately runs ``debootstrap`` and some ``apt`` commands besides ``docker`` things). .. _dependencies: Dependencies / Requirements --------------------------- These should have been installed by whichever method you chose above, but are listed here for completeness. You can get all the development requirements with e.g. ``pip install txtorcon[dev]``. - `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). - `automat `_: "a library for concise, idiomatic Python expression of finite-state automata (particularly deterministic finite-state transducers)." - `ipaddress `_: a standard module in Python3, but requires installing the backported package on Python2. - **dev only**: `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. - **dev only**: `coverage `_ to run the code-coverage metrics. - **dev only** `cuv'ner `_ for coverage visualization - **dev only**: `Tox `_ to run different library revisions. - **dev 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 (as root): .. sourcecode:: shell-session # apt-get install -y python-setuptools python-twisted python-ipaddress graphviz tor # echo "for development:" # apt-get install -y python-sphinx python-repoze.sphinx.autointerface python-coverage libgeoip-dev .. END_INSTALL Using pip this would be: .. sourcecode:: shell-session $ pip install --user Twisted ipaddress pygeoip $ echo "for development:" $ pip install --user GeoIP Sphinx repoze.sphinx.autointerface coverage or: .. sourcecode:: shell-session $ pip install -r requirements.txt $ pip install -r dev-requirements.txt txtorcon-0.19.3/docs/txtorcon.rst0000644000175000017500000000072413106645477016742 0ustar mikemike00000000000000.. _api_documentation: API Documentation ================= These are the lowest-level documents, directly from the doc-strings in the code with some minimal organization; if you're just getting started with txtorcon **the** ":ref:`programming_guide`" **is a better place to start**. .. toctree:: txtorcon-controller txtorcon-state txtorcon-config txtorcon-endpoints txtorcon-protocol txtorcon-socks txtorcon-interface txtorcon-util txtorcon-0.19.3/docs/examples.rst0000644000175000017500000001127113106645477016677 0ustar mikemike00000000000000.. _examples: Examples ======== The examples are grouped by functionality and serve as mini-HOWTOs -- if you have a use-case that is missing, it may be useful to add an example, so please file a bug. All files are in the :file:`examples/` sub-directory and are ready to run, usually with defaults designed to work with Tor Browser Bundle (``localhost:9151``). The examples use `default_control_port()` to determine how to connect which you can override with an environment variable: `TX_CONTROL_PORT`. So e.g. `export TX_CONTROL_PORT=9050` to run the examples again a system-wide Tor daemon. .. contents:: :depth: 2 :local: :backlinks: none Web: clients ------------ .. _web_client.py: ``web_client.py`` ~~~~~~~~~~~~~~~~~ :download:`Download the example <../examples/web_client.py>`. Uses `twisted.web.client `_ to download a Web page using a ``twisted.web.client.Agent``, via any circuit Tor chooses. .. literalinclude:: ../examples/web_client.py .. _web_client_treq.py: ``web_client_treq.py`` ~~~~~~~~~~~~~~~~~~~~~~ :download:`Download the example <../examples/web_client_treq.py>`. Uses `treq `_ to download a Web page via Tor. .. literalinclude:: ../examples/web_client_treq.py .. _web_client_custom_circuit.py: ``web_client_custom_circuit.py`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :download:`Download the example <../examples/web_client_custom_circuit.py>`. Builds a custom circuit, and then uses `twisted.web.client `_ to download a Web page using the circuit created. .. literalinclude:: ../examples/web_client_custom_circuit.py Starting Tor ------------ .. _launch_tor.py: :file:`launch_tor.py` ~~~~~~~~~~~~~~~~~~~~~ :download:`Download the example <../examples/launch_tor.py>`. Launch a new Tor instance. This takes care of setting Tor's notion ownership so that when the control connection goes away the running Tor exits. .. 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 `. .. literalinclude:: ../examples/launch_tor_endpoint.py Circuits and Streams -------------------- .. _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. XXX keep this one? .. literalinclude:: ../examples/disallow_streams_by_port.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 Events ------ .. _monitor.py: :file:`monitor.py` ~~~~~~~~~~~~~~~~~~ :download:`Download the example <../examples/monitor.py>`. Use a plain :class:`txtorcon.TorControlProtocol` instance to listen for some simple events -- in this case marginally useful, as it listens for logging at level ``INFO``, ``NOTICE``, ``WARN`` and ``ERR``. .. literalinclude:: ../examples/monitor.py Miscellaneous ------------- .. _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 .. _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.19.3/docs/hacking.rst0000644000175000017500000000473713106645477016476 0ustar mikemike00000000000000.. _hacking: .. _getting help: Contributions ============= You can help contribute to txtorcon by reporting bugs, sending success stories, by adding a feature or fixing a bug. Even asking that "silly" question helps me with documentation writing. .. _contact info: Contact Information ------------------- Discussing txtorcon is welcome in the following places: - IRC: ``#tor-dev`` on `OFTC `_ (please prefix lines with ``meejah:`` to get my attention, and be patient for replies). - email: preferably on the `tor-dev `_ list, or see `meejah.ca `_ for other ways to contact me. - bugs: use `txtorcon's issues `_ tracker on GitHub. - `@txtorcon `_ on Twitter (announcements only) Public Key ---------- You can download my key from a keyserver (`0xC2602803128069A7 `_) or see :download:`meejah.asc <../meejah.asc>` in the repository. The fingerprint is ``9D5A 2BD5 688E CB88 9DEB CD3F C260 2803 1280 69A7``. Also available at `https://meejah.ca/meejah.asc `_. For convenience: ``curl https://meejah.ca/meejah.asc | gpg --import`` Pull Requests ------------- Yes, please! If you have a new feature or a bug fix, the very best way is to submit a pull-request on GitHub. Since we have 100% coverage, all new lines of code should at least be covered by unit-tests. You can also include a note about the change in ``docs/releases.rst`` if you like (or I can make one up after the merge). I prefer if you rebase/squash commits into logical chunks. Discussion of any implementation details can simply occur on the pull-request itself. Force-pushing to the same branch/PR is fine by me if you want to re-order commits etcetera (but, it's also fine if you just want to push new "fix issues" commits instead). Some example pull-requests: * good discussion + more commits: `PR #150 `_; * a simple one that was "ready-to-go": `PR #51 `_. If you want an easy thing to start with, here are `all issues tagged "easy" `_ Making a Release ---------------- Mostly a note-to-self, but here is my release checklist. .. toctree:: :maxdepth: 3 release-checklist txtorcon-0.19.3/docs/txtorcon-socks.rst0000644000175000017500000000253713106645477020066 0ustar mikemike00000000000000.. _socks: :mod:`txtorcon.socks` Module ============================ SOCKS5 Errors ------------- SocksError ~~~~~~~~~~ .. autoclass:: txtorcon.socks.SocksError GeneralServerFailureError ~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: txtorcon.socks.GeneralServerFailureError ConnectionNotAllowedError ~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: txtorcon.socks.ConnectionNotAllowedError NetworkUnreachableError ~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: txtorcon.socks.NetworkUnreachableError HostUnreachableError ~~~~~~~~~~~~~~~~~~~~ .. autoclass:: txtorcon.socks.HostUnreachableError ConnectionRefusedError ~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: txtorcon.socks.ConnectionRefusedError TtlExpiredError ~~~~~~~~~~~~~~~ .. autoclass:: txtorcon.socks.TtlExpiredError CommandNotSupportedError ~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: txtorcon.socks.CommandNotSupportedError AddressTypeNotSupportedError ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: txtorcon.socks.AddressTypeNotSupportedError .. note:: The following sections present low-level APIs. If you are able to work with :class:`txtorcon.Tor`'s corresponding high-level APIs, you should do so. resolve ------- .. autofunction:: txtorcon.socks.resolve resolve_ptr ----------- .. autofunction:: txtorcon.socks.resolve_ptr TorSocksEndpoint ---------------- .. autoclass:: txtorcon.socks.TorSocksEndpoint txtorcon-0.19.3/docs/_themes/0000755000175000017500000000000013111226470015731 5ustar mikemike00000000000000txtorcon-0.19.3/docs/_themes/README0000644000175000017500000000021612752747562016634 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.19.3/docs/_themes/alabaster/0000755000175000017500000000000013111226470017667 5ustar mikemike00000000000000txtorcon-0.19.3/docs/_themes/alabaster/_version.py0000644000175000017500000000012012752747562022102 0ustar mikemike00000000000000__version_info__ = (0, 6, 0) __version__ = '.'.join(map(str, __version_info__)) txtorcon-0.19.3/docs/_themes/alabaster/donate.html0000644000175000017500000000047212752747562022056 0ustar mikemike00000000000000{% if theme_gittip_user %}

Donate

Consider supporting the authors on Gittip:

{% endif %} txtorcon-0.19.3/docs/_themes/alabaster/support.py0000644000175000017500000001146412752747562022007 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.19.3/docs/_themes/alabaster/static/0000755000175000017500000000000013111226470021156 5ustar mikemike00000000000000txtorcon-0.19.3/docs/_themes/alabaster/static/pygments.css0000644000175000017500000000621712752747562023570 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.19.3/docs/_themes/alabaster/static/alabaster.css_t0000644000175000017500000002547513106645477024206 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; } .first-time { font-size: 110%; font-family: 'source sans pro', sans; background-color: #eeeeee; padding: .5em; margin-top: -.25em; margin-bottom: .5em; border: 0.33em solid #ddeedd; border-radius: 0.25em; box-shadow: 0px 1px 4px 1px #ccc; } .caution { font-size: 110%; font-family: 'source sans pro', sans; background-color: #eedddd; padding: .5em; margin-top: -.25em; margin-bottom: -.25em; border: 0.33em solid #ffeeee; border-radius: 0.25em; } 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.19.3/docs/_themes/alabaster/__init__.py0000644000175000017500000000072612752747562022031 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.19.3/docs/_themes/alabaster/layout.html0000644000175000017500000000515412752747562022123 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.19.3/docs/_themes/alabaster/navigation.html0000644000175000017500000000042712752747562022743 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.19.3/docs/_themes/alabaster/about.html0000644000175000017500000000346412752747562021722 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.19.3/docs/_themes/alabaster/theme.conf0000644000175000017500000000163713106645477021667 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.19.3/docs/_static/0000755000175000017500000000000013111226470015733 5ustar mikemike00000000000000txtorcon-0.19.3/docs/_static/avatar.png0000644000175000017500000004675612312757205017750 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.19.3/docs/_static/logo.svg0000644000175000017500000006702112752747562017446 0ustar mikemike00000000000000 image/svg+xml txtorcon-0.19.3/docs/_static/haiku.css0000644000175000017500000001471512312757205017565 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.19.3/docs/_static/logo.png0000644000175000017500000004176612752747562017443 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.19.3/docs/index.rst0000644000175000017500000000601213106645477016165 0ustar mikemike00000000000000.. txtorcon documentation master file, created by sphinx-quickstart on Thu Jan 26 13:04:28 2012. txtorcon ======== - **docs**: https://txtorcon.readthedocs.org or http://timaq4ygg2iegci7.onion - **code**: https://github.com/meejah/txtorcon - ``torsocks git clone git://timaq4ygg2iegci7.onion/txtorcon.git`` - .. 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.svg :target: https://coveralls.io/r/meejah/txtorcon .. image:: http://codecov.io/github/meejah/txtorcon/coverage.svg?branch=master :target: http://codecov.io/github/meejah/txtorcon?branch=master .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=stable :target: https://txtorcon.readthedocs.io/en/stable :alt: ReadTheDocs .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=latest :target: https://txtorcon.readthedocs.io/en/latest :alt: ReadTheDocs .. image:: https://landscape.io/github/meejah/txtorcon/master/landscape.svg?style=flat :target: https://landscape.io/github/meejah/txtorcon/master :alt: Code Health .. container:: first_time If this is your first time exploring txtorcon, please **look at the** :ref:`introduction` **first**. These docs are for version |version|. .. comment:: +---------------+---------+---------+ | Twisted | 15.5.0+ | 16.3.0+ | +===============+=========+=========+ | Python 2.7+ | ✓ | ✓ | +---------------+---------+---------+ | Python 3.5+ | ✓ | ✓ | +---------------+---------+---------+ | PyPy 5.0.0+ | ✓ | ✓ | +---------------+---------+---------+ Supported and tested platforms: Python 2.7+, Python 3.5+, PyPy 5.0.0+ using Twisted 15.5.0+, 16.3.0+, or 17.1.0+ (see `travis `_). Documentation ------------- .. toctree:: :maxdepth: 3 introduction installing guide examples hacking Official Releases: ------------------ All official releases are tagged in Git, and signed by my key. All official releases on PyPI have a corresponding GPG signature of the build. Please be aware that ``pip`` does *not* check GPG signatures by default; please see `this ticket `_ if you care. The most reliable way to verify you got what I intended is to clone the Git repository, ``git checkout`` a tag and verify its signature. The second-best would be to download a release + tag from PyPI and verify that. .. toctree:: :maxdepth: 2 releases API Documentation ----------------- These are the lowest-level documents, directly from the doc-strings in the code with some minimal organization; if you're just getting started with txtorcon **the** ":ref:`programming_guide`" **is a better place to start**. .. toctree:: :maxdepth: 3 txtorcon Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` txtorcon-0.19.3/docs/txtorcon-util.rst0000644000175000017500000000052112312757205017677 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.19.3/docs/Makefile0000644000175000017500000001075512752747562016001 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.19.3/docs/releases.rst0000644000175000017500000006140413111226413016644 0ustar mikemike00000000000000.. _releases: Releases ======== 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 after v0.19.0 versions are following `calendar versioning `_ with the major version being the 2-digit year. The second digit will be "non-trivial" releases and the third will be for bugfix releases. unreleased ---------- `git master `_ *will likely become v17.0.0* v0.19.3 ------- May 24, 2017 * `txtorcon-0.19.3.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * Incorrect parsing of SocksPort options (see `Issue 237 `_) v0.19.2 ------- May 11, 2017 * `txtorcon-0.19.2.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * Work around a bug in `incremental` (see `Issue 233 `_) * Fix for `Issue 190 `_ from Felipe Dau. * add :meth:`txtorcon.Circuit.when_built`. v0.19.1 ------- April 26, 2017 * `txtorcon-0.19.1.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * Fix a regression in ``launch_tor``, see `Issue 227 `_ v0.19.0 ------- April 20, 2017 * `txtorcon-0.19.0.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * Full Python3 support * Drop `txsocksx` and use a custom implementation (this also implements the custom Tor SOCKS5 methods RESOLVE and RESOLVE_PTR * Drop support for older Twisted releases (12, 13 and 14 are no longer supported). * Add a top-level API object, :class:`txtorcon.Tor` that abstracts a running Tor. Instances of this class are created with :meth:`txtorcon.connect` or :meth:`txtorcon.launch`. These instances are intended to be "the" high-level API and most users shouldn't need anything else. * Integrated support for `twisted.web.client.Agent`, baked into :class:`txtorcon.Tor`. This allows simple, straightforward use of treq_ or "raw" `twisted.web.client` for making client-type Web requests via Tor. Automatically handles configuration of SOCKS ports. See :meth:`txtorcon.Tor.web_agent` * new high-level API for putting streams on specific Circuits. This adds :meth:`txtorcon.Circuit.stream_via` and :meth:`txtorcon.Circuit.web_agent` methods that work the same as the "Tor" equivalent methods except they use a specific circuit. This makes :meth:`txtorcon.TorState.set_attacher` the "low-level" / "expert" interface. Most users should only need the new API. * big revamp / re-write of the documentation, including the new `Programming Guide `_ * `Issue 203 `_ * new helper: :meth:`txtorcon.Router.get_onionoo_details`_ * new helper: :func:`txtorcon.util.create_tbb_web_headers`_ * `Issue 72 `_ * `Felipe Dau `_ added specific `SocksError` subclasses for all the available SOCKS5 errors. * (more) Python3 fixes from `rodrigc `_ .. _Automat: https://github.com/glyph/automat .. _treq: https://pypi.python.org/pypi/treq v0.18.0 ------- January 11, 2017 * `txtorcon-0.18.0.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * `issue 200 `_: better feedback if the cookie data can't be read v0.17.0 ------- *October 4, 2016* * `txtorcon-0.17.0.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * `issue 187 `_: fix unix-socket control endpoints * sometimes mapping streams to hostnames wasn't working properly * backwards-compatibility API for `socks_hostname` was incorrectly named v0.16.1 ------- *August 31, 2016* * `txtorcon-0.16.1.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * `issue 172 `_: give `TorProcessProtocol` a `.quit` method * `issue 181 `_: enable SOCKS5-over-unix-sockets for TorClientEndpoint (thanks to `david415 `_ v0.16.0 ------- * there wasn't one, `because reasons `_. v0.15.1 ------- * `txtorcon-0.15.1.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * fix `issue 179 `_ with `Circuit.age`. v0.15.0 ------- *July 26, 2016* * `txtorcon-0.15.0.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) * added support for NULL control-port-authentication which is often appropriate when used with a UNIX domain socket * switched to `ipaddress `_ instead of Google's ``ipaddr``; the API should be the same from a user perspective but **packagers and tutorials** will want to change their instructions slightly (``pip install ipaddress`` or ``apt-get install python-ipaddress`` are the new ways). * support the new ADD_ONION and DEL_ONION "ephemeral hidden services" commands in TorConfig * a first stealth-authentication implementation (for "normal" hidden services, not ephemeral) * bug-fix from `david415 `_ to raise ConnectionRefusedError instead of StopIteration when running out of SOCKS ports. * new feature from `david415 `_ adding a ``build_timeout_circuit`` method which provides a Deferred that callbacks only when the circuit is completely built and errbacks if the provided timeout expires. This is useful because :meth:`txtorcon.TorState.build_circuit` callbacks as soon as a Circuit instance can be provided (and then you'd use :meth:`txtorcon.Circuit.when_built` to find out when it's done building). * new feature from `coffeemakr `_ falling back to password authentication if cookie authentication isn't available (or fails, e.g. because the file isn't readable). * both TorState and TorConfig now have a ``.from_protocol`` class-method. * spec-compliant string-un-escaping from `coffeemakr `_ * a proposed new API: :meth:`txtorcon.connect` * fix `issue 176 `_ v0.14.1 ------- *October 25, 2015* * subtle bug with ``.is_built`` on Circuit; changing the API (but with backwards-compatibility until 0.15.0 at least) 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. * 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 howtos to documentation 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.19.3/docs/introduction.rst0000644000175000017500000001173613106645477017610 0ustar mikemike00000000000000.. _introduction: Introduction ============ txtorcon is an implementation of the `control-spec `_ for `Tor `_ using the `Twisted `_ networking library for `Python `_. With txtorcon you can launch tor; connect to already-running tor instances; use tor as a client (via SOCKS5); set up services over tor; change all aspects of configuration; track live state (active circuits and streams, etc); do DNS via Tor; and query other information from the tor daemon. txtorcon would be of interest to anyone wishing to write event-based software in Python that uses the Tor network as a client or a service (or just wants to display information about a locally running tor). Twisted already provides many robust protocol implementations, deployment, logging and integration with GTK, Qt and other graphics frameworks -- so txtorcon can be used for command-line or GUI applications or integrate with long-lived daemons easily. In fact, due to support for endpoints (adding the ``tor:`` and ``onion:`` plugins), many Twisted applications can now integrate with Tor with **no code changes**. For example, you can use the existing Twisted webserver via ``twistd`` to serve your ``~/public_html`` directory over an onion service: .. code-block:: shell-session $ sudo apt-get install python-txtorcon $ twistd web --port "onion:80" --path ~/public_html txtorcon strives to provide sane and **safe** defaults. txtorcon is `a Tor project `_. .. _features: Features Overview ----------------- Currently, txtorcon is capable of: - making arbitrary client connections to other services over Tor; - configuring `twisted.web.client.Agent `_ instances to do Web requests over Tor; - doing both of the above over specific circuits; - listening as an Onion service; - maintaining up-to-date (live) state information about Tor: Circuits, Streams and Routers (relays); - maintaining current (live) configuration information; - maintaining representation of Tor's address mappings (with expiry); - interrogating initial state of all three of the above; - listening for and altering stream -> circuit mappings; - building custom circuits; - Circuit and Stream state listeners; - listening for any Tor EVENT; - launching and/or controlling a Tor instance (including Tor Browser Bundle); - complete Twisted endpoint support (both "onion"/server side and client-side). This means you may be able to use *existing* Twisted software via Tor with **no code changes**. It also is the preferred way to connect (or listen) in Twisted. Comments (positive or negative) appreciated. Even better if they come with patches 😉 Shell-cast Overview ------------------- A text-only screencast-type overview of some of txtorcon's features, from asciinema.org: .. role:: raw-html(raw) :format: html :raw-html:`` Example Code ------------ `download `_ (also `python3 style `_) .. literalinclude:: ../examples/readme.py .. _known_users: Known Users ----------- - `magic-wormhole `_ "get things from one computer to another, safely" - `Tahoe-LAFS `_ a Free and Open encrypted distributed storage system - txtorcon received a brief mention `at 29C3 `_ starting at 12:20 (or via `youtube `_). - `carml `_ command-line utilities for Tor - `foolscap `_ RPC system inspired by Twisted's built-in "Perspective Broker" package. - `bwscanner `_ next-gen bandwidth scanner for Tor network - `unmessage `_ Privacy enhanced instant messenger - `APAF `_ anonymous Python application framework - `OONI `_ the Open Observatory of Network Interference - `exitaddr `_ scan Tor exit addresses - `txtorhttpproxy `_ simple HTTP proxy in Twisted - `bulb `_ Web-based Tor status monitor - `onionvpn `_ "ipv6 to onion service virtual public network adapter" - `torperf2 `_ new Tor node network performance measurement service - `torweb `_ web-based Tor controller/monitor - `potator `_ "A Tor-based Decentralized Virtual Private Network Application" txtorcon-0.19.3/docs/txtorcon-protocol.rst0000644000175000017500000000101013100171552020544 0ustar mikemike00000000000000.. _protocol: Low-Level Protocol Classes ========================== build_tor_connection -------------------- .. autofunction:: txtorcon.build_tor_connection build_local_tor_connection -------------------------- .. autofunction:: txtorcon.build_local_tor_connection TorControlProtocol ------------------ .. autoclass:: txtorcon.TorControlProtocol TorProtocolFactory ------------------ .. autoclass:: txtorcon.TorProtocolFactory TorProcessProtocol ------------------ .. autoclass:: txtorcon.TorProcessProtocol txtorcon-0.19.3/docs/guide.rst0000644000175000017500000007711413106645477016166 0ustar mikemike00000000000000.. _programming_guide: Programming Guide ================= .. contents:: :depth: 2 :local: :backlinks: none .. _api_stability: API Stability ------------- In general, any method or class prefixed with an underscore (like ``_method`` or ``_ClassName``) is private, and the API may change at any time. You SHOULD NOT use these. Any method in an interface class (which all begin with ``I``, like ``IAnInterface``) are stable, public APIs and will maintain backwards-compatibility between releases. There is **one exception to this** at the moment: the hidden- / onion- services APIs are NOT yet considered stable, and may still change somewhat. Any APIs that will go away will first be deprecated for at least one major release before being removed. There are also some attributes which *don't* have underscores but really should; these will get "deprecated" via an ``@property`` decorator so your code will still work. .. _guide_overview: High Level Overview ------------------- Interacting with Tor via txtorcon should involve *only* calling methods of the :class:`txtorcon.Tor` class. You get an instance of :class:`txtorcon.Tor` in one of two ways: - call :meth:`txtorcon.connect` or; - call :meth:`txtorcon.launch` Once you've got a ``Tor`` instance you can use it to gain access to (or create) instances of the other interesting classes; see "A Tor Instance" below for various use-cases. Note that for historical reasons (namely: ``Tor`` is a relatively new class) there are many other functions and classes exported from txtorcon but you *shouldn't* need to instantiate these directly. If something is missing from this top-level class, please get in touch (file a bug, chat on IRC, etc) because it's probably a missing feature. .. _guide_tor_instance: A Tor Instance -------------- You will need a connection to a Tor instance for txtorcon to control. This can be either an already-running Tor that you're authorized to connect to, or a Tor instance that has been freshly launched by txtorcon. We abstract "a Tor instance" behind the :class:`txtorcon.Tor` class, which provides a very high-level API for all the other things you might want to do: - make client-type connections over tor (see ":ref:`guide_client_use`"); - change its configuration (see ":ref:`guide_configuration`"); - monitor its state (see ":ref:`guide_state`"); - offer hidden-/onion- services via Tor (see ":ref:`guide_onions`"); - create and use custom circuits (see ":ref:`guide_custom_circuits`"); - issue low-level commands (see ":ref:`protocol`") The actual control-protocol connection to tor is abstracted behind :class:`txtorcon.TorControlProtocol`. This can usually be ignored by most users, but can be useful to issue protocol commands directly, listen to raw events, etc. In general, txtorcon tries to never look at Tor's version and instead queries required information directly via the control-protocol (there is only one exception to this). So the names of configuration values and events may change (or, more typically, expand) depending on what version of Tor you're connected to. Connecting to a Running Tor ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tor can listen for control connections on TCP ports or UNIX sockets. See ":ref:`configure_tor`" for information on how to configure Tor to work with txtorcon. By default, "COOKIE" authentication is used; only if that is not available do we try password authentication. To connect, use :meth:`txtorcon.connect` which returns a Deferred that will fire with a :class:`txtorcon.Tor` instance. If you need access to the :class:`txtorcon.TorControlProtocol` instance, it's available via the ``.protocol`` property (there is always exactly one of these per :class:`txtorcon.Tor` instance). Similarly, the current configuration is available via ``.config``. You can change the configuration by updating attributes on this class but it won't take effect until you call :meth:`txtorcon.TorConfig.save`. Launching a New Tor ~~~~~~~~~~~~~~~~~~~ It's also possible to launch your own Tor instance. txtorcon keeps a "global" tor available for use by e.g. the ``.global_tor`` endpoint factory functions (like :func:`txtorcon.TCPHiddenServiceEndpoint.global_tor`). You can access it via :func:`txtorcon.get_global_tor`. There is exactly zero or one of these *per Python process* that uses ``txtorcon``. XXX FIXME the above isn't quite true, as that function existed previously and returned a TorConfig so we need to come up with another name :( To explicitly launch your own Tor instance, use :meth:`txtorcon.launch`. You can pass a couple of minimal options (``data_directory`` being recommended). If you need to set other Tor options, use ``.config`` to retrieve the :class:`txtorcon.TorConfig` instance associated with this tor and change configuration afterwards. Setting ``data_directory`` gives your Tor instance a place to cache its state information which includes the current "consensus" document. If you don't set it, txtorcon creates a temporary directly (which is deleted when this Tor instance exits). Startup time is drammatically improved if Tor already has a recent consensus, so when integrating with Tor by launching your own client it's highly recommended to specify a ``data_directory`` somewhere sensible (e.g. ``~/.config/your_program_name/`` is a popular choice on Linux). See `the Tor manual `_ under the ``DataDirectory`` option for more information. Tor itself will create a missing ``data_directory`` with the correct permissions and Tor will also ``chdir`` into its ``DataDirectory`` when running. For these reasons, txtorcon doesn't try to create the ``data_directory`` nor do any ``chdir``-ing, and neither should you. .. _guide_style: A Note On Style --------------- Most of txtorcon tends towards "attribute-style access". The guiding principle is that "mere data" that is immediately available will be an attribute, whereas things that "take work" or are async (and thus return ``Deferred`` s) will be functions. For example, :meth:`txtorcon.Router.get_location` is a method because it potentially has to ask Tor for the country, whereas :attr:`txtorcon.Router.hex_id` is a plain attribute because it's always available. .. _guide_configuration: Tracking and Changing Tor's Configuration ----------------------------------------- Instances of the :class:`txtorcon.TorConfig` class represent the current, live state of a running Tor. There is a bit of attribute-magic to make it possible to simply get and set things easily: .. sourcecode:: python tor = launch(..) print("SOCKS ports: {}".format(tor.config.SOCKSPort)) tor.config.ControlPort.append(4321) tor.config.save() **Only when** ``.save()`` is called are any ``SETCONF`` commands issued -- and then, all configuration values are sent in a single command. All ``TorConfig`` instances subscribe to configuration updates from Tor, so **"live state" includes actions by any other controllers that may be connected**. For some configuration items, the order they're sent to Tor matters. Sometimes, if you change one config item, you have to set a series of related items. TorConfig handles these cases for you -- you just manipulate the configuration, and wait for ``.save()`` 's ``Deferred`` to fire and the running Tor's configuration is updated. Note there is a tiny window during which the state may appear slightly inconsistent if you have multiple ``TorConfig`` instances: after Tor has acknowledged a ``SETCONF`` command, but before a separate ``TorConfig`` instance has gotten all the ``CONF_CHANGED`` events (because they're hung up in the networking stack for some reason). This shouldn't concern most users. (I'm not even 100% sure this is possible; it may be that Tor doesn't send the OK until after all the CONF_CHANGED events). In normal use, there should only be a single ``TorConfig`` instance for every ``Tor`` instance so this shouldn't affect you unless you've created your own ``TorConfig``. Since :class:`txtorcon.TorConfig` conforms to the Iterator protocol, you can easily find all the config-options that Tor supports: .. sourcecode:: python tor = launch(..) for config_key in tor.config: print("{} has value: {}".format(config_key, getattr(tor.config.config_key))) .. fixme:: why doesn't dir() work; fix it, or mention it here These come from interrogating Tor using ``GETINFO config/names`` and so represent the configuration options of the current connected Tor process. If the value "isn't set" (i.e. is the default), the value from Tor will be ``txtorcon.DEFAULT_VALUE``. When you set values into ``TorConfig``, they are parsed according to control-spec for the different types given to the values, via information from ``GETINFO config/names``. So, for example, setting ``.SOCKSPort`` to a ``"quux"`` won't work. Of course, it would also fail the whole ``SETCONF`` command if txtorcon happens to allow some values that Tor doesn't. Unfortunately, **for any item that's a list**, Tor doesn't tell us anything about each element so they're all strings. This means we can't pre-validate them and so some things may not fail until you call ``.save()``. .. _guide_state: Monitor and Change Tor's State ------------------------------ Instances of :class:`txtorcon.TorState` prepresent a live, interactive version of all the relays/routers (:class:`txtorcon.Router` instances), all circuits (:class:`txtorcon.Circuit` instances) and streams (:class:`txtorcon.Stream` instances) active in the underlying Tor instance. As the ``TorState`` instance has subscribed to various events from Tor, the "live" state represents an "as up-to-date as possible" view. This includes all other controlers, Tor Browser, etcetera that might be interacting with your Tor client. A ``Tor`` instance doesn't have a ``TorState`` instance by default (it can take a few hundred milliseconds to set up) and so one is created via the asynchronous method :meth:`txtorcon.Tor.get_state`. .. note:: If you need to be **absolutely sure** there's nothing stuck in networking buffers and that your instance is "definitely up-to-date" you can issue a do-nothing command to Tor via :meth:`txtorcon.TorControlProtocol.queue_command` (e.g. ``yield queue_command("GETINFO version")``). Most users shouldn't have to worry about this edge-case. In any case, there could be a new update that Tor decides to issue at any moment. You can modify the state of Tor in a few simple ways. For example, you can call :meth:`txtorcon.Stream.close` or :meth:`txtorcon.Circuit.close` to cause a stream or circuit to be closed. You can wait for a circuit to become usable with :meth:`txtorcon.Circuit.when_built`. For a lot of the read-only state, you can simply access interesting attributes. The relays through which a circuit traverses are in ``Circuit.path`` (a list of :class:`txtorcon.Router` instances), ``Circuit.streams`` contains a list of :class:`txtorcon.Stream` instances, ``.state`` and ``.purpose`` are strings. ``.time_created`` returns a `datetime `_ instance. There are also some convenience functions like :meth:`txtorcon.Circuit.age`. For sending streams over a particular circuit, :meth:`txtorcon.Circuit.stream_via` returns an `IStreamClientEndpoint`_ implementation that will cause a subsequent ``.connect()`` on it to go via the given circuit in Tor. A similar method (:meth:`txtorcon.Circuit.web_agent`) exists for Web requests. Listening for certain events to happen can be done by implementing the interfaces :class:`txtorcon.interface.IStreamListener` and :class:`txtorcon.interface.ICircuitListener`. You can request notifications on a Tor-wide basis with :meth:`txtorcon.TorState.add_circuit_listener` or :meth:`txtorcon.TorState.add_stream_listener`. If you are just interested in a single circuit, you can call :meth:`txtorcon.Circuit.listen` directly on a ``Circuit`` instance. The Tor relays are abstracted with :class:`txtorcon.Router` instances. Again, these have read-only attributes for interesting information, e.g.: ``id_hex``, ``ip``, ``flags`` (a list of strings), ``bandwidth``, ``policy``, etc. Note that all information in these objects is from "microdescriptors". If you're doing a long-running iteration over relays, it may be important to remember that the collection of routers can change every hour (when a new "consensus" from the Directory Authorities is published) which may change the underlying collection (e.g. :attr:`txtorcon.TorState.routers_by_hash`) over which you're iterating. Here's a simple sketch that traverses all circuits printing their router IDs, and closing each stream and circuit afterwards: (XXX FIXME test this for realz; can we put it in a "listing"-type file?) .. code-block:: python @inlineCallbacks def main(reactor): tor = yield connect(reactor, UNIXClientEndpoint('/var/run/tor/control')) state = yield tor.get_state() for circuit in state.circuits.values(): path = '->'.join(map(lambda r: r.id_hex, circuit.streams)) print("Circuit {} through {}".format(circuit.id, path)) for stream in circuit.streams: print(" Stream {} to {}".format(stream.id, stream.target_host)) yield stream.close() yield circuit.close() .. _guide_client_use: Making Connections Over Tor --------------------------- SOCKS5 ~~~~~~ Tor exposes a SOCKS5 interface to make client-type connections over the network. There are also a couple of `custom extensions `_ Tor provides to do DNS resolution over a Tor circuit (txtorcon supports these, too). All client-side interactions are via instances that implement `IStreamClientEndpoint`_. There are several factory functions used to create suitable instances. The recommended API is to acquire a :class:`txtorcon.Tor` instance (see ":ref:`guide_tor_instance`") and then call :meth:`txtorcon.Tor.create_client_endpoint`. To do DNS lookups (or reverse lookups) via a Tor circuit, use :meth:`txtorcon.Tor.dns_resolve` and :meth:`txtorcon.Tor.dns_resolve_ptr`. A common use-case is to download a Web resource; you can do so via Twisted's built-in ``twisted.web.client`` package, or using the friendlier `treq`_ library. In both cases, you need a `twisted.web.client.Agent `_ instance which you can acquire with :meth:`txtorcon.Tor.web_agent` or :meth:`txtorcon.Circuit.web_agent`. The latter is used to make the request over a specific circuit. Usually, txtorcon will simply use one of the available SOCKS ports configured in the Tor it is connected to -- if you care which one, you can specify it as the optional ``_socks_endpoint=`` argument (this starts with an underscore on purpose as it's not recommended for "public" use and its semantics might change in the future). .. note:: Tor supports SOCKS over Unix sockets. So does txtorcon. To take advantage of this, simply pass a valid ``SocksPort`` value for unix sockets (e.g. ``unix:/tmp/foo/socks``) as the ``_socks_endpoint`` argument to either ``web_agent()`` call. If this doesn't already exist in the underlying Tor, it will be added. Tor has particular requirements for the directory in which the socket file is (``0700``). We don't have a way (yet?) to auto-discover if the Tor we're connected to can support Unix sockets so the default is to use TCP. You can also use Twisted's `clientFromString`_ API as txtorcon registers a ``tor:`` plugin. This also implies that any Twisted-using program that supports configuring endpoint strings gets Tor support "for free". For example, passing a string like ``tor:timaq4ygg2iegci7.onion:80`` to `clientFromString`_ will return an endpoint that will connect to txtorcon's hidden-service website. Note that these endpoints will use the "global to txtorcon" Tor instance (available from :meth:`txtorcon.get_global_tor`). Thus, if you want to control *which* tor instance your circuit goes over, this is not a suitable API. There are also lower-level APIs to create :class:`txtorcon.TorClientEndpoint` instances directly if you have a :class:`txtorcon.TorConfig` instance. These very APIs are used by the ``Tor`` object mentioned above. If you have a use-case that *requires* using this API, I'd be curious to learn why the :class:`txtorcon.Tor` methods are un-suitable (as those are the suggested API). You should expect these APIs to raise SOCKS5 errors, which can all be handled by catching the :class:`txtorcon.socks.SocksError` class. If you need to work with each specific error (corresponding to the `RFC-specified SOCKS5 replies`_), see the ":ref:`socks`" for a list of them. .. _guide_onions: .. _server_use: Onion (Hidden) Services ----------------------- .. caution:: The Onion service APIs are not stable and will still change; the following is written to what they *will probably* become but **DO NOT** document the current state of the code. An "Onion Service" (also called a "Hidden Service") refers to a feature of Tor allowing servers (e.g. a Web site) to get additional security properties such as: hiding their network location; providing end-to-end encryption; self-certifying domain-names; or offering authentication. For details of how this works, please read `Tor's documentation on Hidden Services `_. For more background, the `RiseUp Onion service best-practices guide `_ is a good read as well. From an API perspective, here are the parts we care about: - each service has a secret, private key (with a corresponding public part): - these keys can be on disk (in the "hidden service directory"); - or, they can be "ephemeral" (only in memory); - the "host name" is a hash of the public-key (e.g. ``timaq4ygg2iegci7.onion``); - a "Descriptor" (which tells clients how to connect) must be published; - a service has a list of port-mappings (public -> local) - e.g. ``"80 127.0.0.1:5432"`` says you can contact the service publically on port 80, which Tor will redirect to a daemon running locally on port ``5432``; - note that "Descriptors" don't show this information - services can be "authenticated", which means they have a list of client names for which Tor creates associated keys (``.auth_token``). - Tor has two flavours of service authentication: ``basic`` and ``stealth`` -- there's no API-level difference, but the ``.hostname`` is unique for each client in the ``stealth`` case. - See :ref:`create_onion` for details on how to choose which (if any) authentication method you'd like To summarize the above in a table format, here are the possible types of Onion Service interfaces classes you may interact with (ephemeral services don't yet support any authentication). +----------------------------------+--------------------------------------+------------------------+ | | Keys on disk | Keys in memory | +==================================+======================================+========================+ | **no authentication** | IFilesystemOnionService | IOnionService | +----------------------------------+--------------------------------------+------------------------+ | **basic/stealth authentication** | IOnionClients | | +----------------------------------+--------------------------------------+------------------------+ Note that it's **up to you to save the private keys** of ephemeral services if you want to re-launch them later; the "ephemeral" refers to the fact that Tor doesn't persist the private keys -- when Tor shuts down, they're gone and there will never be a service at the same URI again. Onion Services Endpoints API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ No matter which kind of service you need, you interact via Twisted's `IStreamServerEndpoint`_ interface. There are various txtorcon methods (see ":ref:`create_onion`") which return some instance implementing that interface. These instances will also implement :class:`txtorcon.IProgressProvider` -- which is a hook to register listeners which get updates about Tor's launching progress (if we started a new Tor) and Descriptor uploading. Fundamentally, "authenticated" services are different from non-authenticated services because they have a list of clients. Therefore, there are two different endpoint types: - :class:`txtorcon.TCPHiddenServiceEndpoint` - :class:`txtorcon.TCPAuthenticatedHiddenServiceEndpoint` In either case, the ``listen`` method will return an instance implementing `IListeningPort`_. In addition to `IListeningPort`_, these instances will implement one of: - :class:`txtorcon.IOnionService` or; - :class:`txtorcon.IOnionClients` The first one corresponds to a non-authenticated service, while the latter is authenticated. The latter manages a collection of instances by (arbitrary) client names, where each of these instances implements :class:`txtorcon.IOnionClient` (and therefore also :class:`txtorcon.IOnionService`). Note that the ``.auth_token`` member is secret, private data which you need to give to **one** client; this information goes in the client's Tor configuration as ``HidServAuth onion-address auth-cookie [service-name]``. See `the Tor manual `_ for more information. Also note that Tor's API for adding "ephemeral" services doesn't yet support any type of authentication (however, it may in the future). .. _create_onion: Creating Onion Endpoints ~~~~~~~~~~~~~~~~~~~~~~~~ The easiest to use API are methods of :class:`txtorcon.Tor`, which allow you to create `IStreamServerEndpoint` instances for the various Onion Service types. Both the main endpoint types have several factory-methods to return instances -- so you first must decide whether to use an "authenticated" service or not. - if you want anyone with e.g. the URL http://timaq4ygg2iegci7.onion to be able to put it in `Tor Browser Bundle `_ and see a Web site, you **do not want** authentication; - if you want only people with the URL *and* a secret authentication token to see the Web site, you want **basic** authentication (these support many more clients than stealth auth); - if you don't even want anyone to be able to decrypt the descriptor without a unique URL *and* a secret authentication token, you want **stealth** authentication (a lot less scalable; for only "a few" clients). Non-Authenticated Services ~~~~~~~~~~~~~~~~~~~~~~~~~~ For non-authenticated services, you want to create a :class:`txtorcon.TCPHiddenServiceEndpoint` instance. You can do this via the :meth:`txtorcon.create_onion_service` factory function or with :meth:`txtorcon.Tor.create_onion_service`. It's also possible to use Twisted's ``serverFromString`` API with the ``onion:`` prefix. (Thus, any program supporting endpoint strings for configuration can use Tor Onion Services with *no code changes*). If you don't want to manage launching or connecting to Tor yourself, you can use one of the three @classmethods on the class, which all return a new endpoint instance: - :meth:`txtorcon.TCPHiddenSeviceEndpoint.global_tor`: uses a Tor instance launched at most once in this Python process (the underlying :class:`txtorcon.Tor` instance for this is available via :meth:`txtorcon.get_global_tor()` if you need to make manual configuration adjustments); - :meth:`txtorcon.TCPHiddenSeviceEndpoint.system_tor`: connects to the control-protocol endpoint you provide (a good choice on Debian would be ``UNIXClientEndpoint('/var/run/tor/control')``); - :meth:`txtorcon.TCPHiddenSeviceEndpoint.private_tor`: causes a fresh, private instance of Tor to be launched for this service alone. This uses a tempdir (honoring ``$TMP``) which is deleted upon reactor shutdown or loss of the control connection. Note that nothing actually "happens" until you call ``.listen()`` on the ``IStreamServerEndpoint`` at which point Tor will possibly be launched, the Onion Service created, and the descriptor published. Authenticated Services ~~~~~~~~~~~~~~~~~~~~~~ To use authenticated services, you want to create a :class:`txtorcon.TCPAuthenticatedHiddenServiceEndpoint` instance. This provides the very same factory methods as for non-authenticatd instances, but adds arguments for a list of clients (strings) and an authentication method (``"basic"`` or ``"stealth"``). For completeness, the methods to create authenticated endpoints are: - :meth:`txtorcon.Tor.create_authenticated_onion_service()`; - :meth:`txtorcon.create_authenticated_onion_service`; - :meth:`txtorcon.TCPAuthenticatedHiddenSeviceEndpoint.global_tor` - :meth:`txtorcon.TCPAuthenticatedHiddenSeviceEndpoint.system_tor` - :meth:`txtorcon.TCPAuthenticatedHiddenSeviceEndpoint.private_tor` Onion Service Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you just want to "look at" the configuration of existing onion services, they are avaialble via :class:`txtorcon.TorConfig` and the ``.HiddenServices`` attribute. This presents a "flattened" version of any authenticated services, so that each element in the list of ``.HiddenServices`` is itself at least a :class:`txtorcon.IOnionService` (it may also implement other interfaces, but every one will implement ``IOnionService``). You can still set any settable attributes on these objects, and Tor's configuration for them will be updated when you call :meth:`txtorcon.TorConfig.save` with an **important exception**: "ephemeral" services cannot be updated after they're created. Note that it's possible for other controllers to create ephemeral services that your controller can't enumerate. .. _guide_custom_circuits: Custom Circuits --------------- Tor provides a way to let controllers like txtorcon decide which streams go on which circuits. Since your Tor client will then be acting differently from a "normal" Tor client, it **may become easier to de-anonymize you**. High Level ~~~~~~~~~~ With that in mind, you may still decide to attach streams to circuits. Most often, this means you simply want to make a client connection over a particluar circuit. The recommended API uses :meth:`txtorcon.Circuit.stream_via` for arbitrary protocols or :meth:`txtorcon.Circuit.web_agent` as a convenience for Web connections. The latter can be used via `Twisted's Web client `_ or via `treq `_ (a "requests"-like library for Twisted). See the following examples: - :ref:`web_client.py` - :ref:`web_client_treq.py` - :ref:`web_client_custom_circuit.py` Note that these APIs mimic :meth:`txtorcon.Tor.stream_via` and :meth:`txtorcon.Tor.web_agent` except they use a particular Circuit. Low Level ~~~~~~~~~ Under the hood of these calls, txtorcon provides a low-level interface directly over top of Tor's circuit-attachment API. This works by: - setting ``__LeaveStreamsUnattached 1`` in the Tor's configuration - listening for ``STREAM`` events - telling Tor (via ``ATTACHSTREAM``) what circuit to put each new stream on - (we can also choose to tell Tor "attach this one however you normally would") This is an asynchronous API (i.e. Tor isn't "asking us" for each stream) so arbitrary work can be done on a per-stream basis before telling Tor which circuit to use. There are two limitations though: - Tor doesn't play nicely with multiple controllers playing the role of attaching circuits. Generally, there's not a good way to know if there's another controller trying to attach streams, but basically the first one to answer "wins". - Tor doesn't currently allow controllers to attach circuits destined for onion-services (even if the circuit is actually suitable and goes to the correct Introduction Point). In order to do custom stream -> circuit mapping, you call :meth:`txtorcon.TorState.set_attacher` with an object implementing :class:`txtorcon.interface.IStreamAttacher`. Then every time a new stream is detected, txtorcon will call :meth:`txtorcon.interface.IStreamAttacher.attach_stream` with the :class:`txtorcon.Stream` instance and a list of all available circuits. You make an appropriate return. There can be either no attacher at all or a single attacher object. You can "un-set" an attacher by calling ``set_attacher(None)`` (in which case ``__LeaveStreamsUnattached`` will be set back to 0). If you really do need multiple attachers, you can use the utility class :class:`txtorcon.attacher.PriorityAttacher` which acts as the "top level" one (so you add your multiple attachers to it). Be aware that txtorcon internally uses this API itself if you've *ever* called the "high level" API (:meth:`txtorcon.Circuit.stream_via` or :meth:`txtorcon.Circuit.web_agent`) and so it is an **error** to set a new attacher if there is already an existing attacher. .. _guide_building_circuits: Building Your Own Circuits -------------------------- To re-iterate the warning above, making your own circuits differently from how Tor normally does **runs a high risk of de-anonymizing you**. That said, you can build custom circuits using txtorcon. Building a Single Circuit ~~~~~~~~~~~~~~~~~~~~~~~~~ If your use-case needs just a single circuit, it is probably easiest to call :meth:`txtorcon.TorState.build_circuit`. This methods takes a list of :class:`txtorcon.Router` instances, which you can get from the :class:`txtorcon.TorState` instance by using one of the attributes: - ``.all_routers`` - ``.routers`` - ``.routers_by_name`` or - ``.routers_by_hash`` The last three are all hash-tables. For relays that have the ``Guard`` flag, you can access the hash-tables ``.guards`` (for **all** of them) or ``.entry_guards`` (for just the entry guards configured on this Tor client). If you don't actually care which relays are used, but simply want a fresh circuit, you can call :meth:`txtorcon.TorState.build_circuit` without any arguments at all which asks Tor to build a new circuit in the way it normally would (i.e. respecting your guard nodes etc). .. _circuit_builder: Building Many Circuits ~~~~~~~~~~~~~~~~~~~~~~ .. caution:: This API doesn't exist yet; this is documenting what **may** become a new API in a future version of txtorcon. Please get in touch if you want this now. If you would like to build many circuits, you'll want an instance that implements :class:`txtorcon.ICircuitBuilder` (which is usually simply an instance of :class:`txtorcon.CircuitBuilder`). Instances of this class can be created by calling one of the factory functions like :func:`txtorcon.circuit_builder_fixed_exit`. XXX what about a "config object" idea, e.g. could have keys: - ``guard_selection``: one of ``entry_only`` (use one of the current entry guards) or ``random_guard`` (use any relay with the Guard flag, selected by XXX). - ``middle_selection``: one of ``uniform`` (selected randomly from all relays), ``weighted`` (selected randomly, but weighted by consensus weight -- basically same way as Tor would select). .. _istreamclientendpoint: http://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IStreamClientEndpoint.html .. _istreamserverendpoint: http://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IStreamServerEndpoint.html .. _clientfromstring: http://twistedmatrix.com/documents/current/api/twisted.internet.endpoints.html#clientFromString .. _serverfromstring: http://twistedmatrix.com/documents/current/api/twisted.internet.endpoints.html#serverFromString .. _ilisteningport: http://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IListeningPort.html .. _treq: https://github.com/twisted/treq .. _`rfc-specified socks5 replies`: https://tools.ietf.org/html/rfc1928#section-6 txtorcon-0.19.3/docs/conf.py0000644000175000017500000002101213100311465015575 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' #keep_warnings = True pygments_style = 'monokai' #pygments_style = 'solarized_dark256' ## 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._metadata 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.19.3/docs/apilinks_sphinxext.py0000644000175000017500000000313612312757205020615 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