txtorcon-19.0.0/0000755000175000017500000000000013417536113013361 5ustar mikemike00000000000000txtorcon-19.0.0/scripts/0000755000175000017500000000000013417536113015050 5ustar mikemike00000000000000txtorcon-19.0.0/scripts/asciinema-demo0.py0000755000175000017500000000700712444716153020367 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-19.0.0/scripts/asciinema-demo1.py0000755000175000017500000000450712701107713020362 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-19.0.0/twisted/0000755000175000017500000000000013417536113015044 5ustar mikemike00000000000000txtorcon-19.0.0/twisted/plugins/0000755000175000017500000000000013417536113016525 5ustar mikemike00000000000000txtorcon-19.0.0/twisted/plugins/txtorcon_endpoint_parser.py0000644000175000017500000000024112752747562024245 0ustar mikemike00000000000000import txtorcon tcpHiddenServiceEndpointParser = txtorcon.TCPHiddenServiceEndpointParser() tcpTorClientEndpointParser = txtorcon.TorClientEndpointStringParser() txtorcon-19.0.0/dev-requirements.txt0000644000175000017500000000025613330507756017431 0ustar mikemike00000000000000tox coverage cuvner setuptools>=0.8.0 Sphinx repoze.sphinx.autointerface>=0.4 coveralls codecov wheel twine pyflakes pycodestyle mock ipaddress>=1.0.16 geoip readme_renderer txtorcon-19.0.0/PKG-INFO0000644000175000017500000001543013417536113014461 0ustar mikemike00000000000000Metadata-Version: 2.1 Name: txtorcon Version: 19.0.0 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:: 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 --install-suggests 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 `_). .. _Twisted: https://twistedmatrix.com/trac 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: Programming Language :: Python :: 3 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Internet :: Proxy Servers Classifier: Topic :: Internet Classifier: Topic :: Security Provides-Extra: dev txtorcon-19.0.0/setup.cfg0000644000175000017500000000010313417536113015174 0ustar mikemike00000000000000[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 txtorcon-19.0.0/test/0000755000175000017500000000000013417536113014340 5ustar mikemike00000000000000txtorcon-19.0.0/test/test_circuit.py0000644000175000017500000005143413106646076017427 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-19.0.0/test/test_util.py0000644000175000017500000003623313323436522016734 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 from txtorcon.testutil import FakeControlProtocol 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) ) def test_version_big(self): self.assertTrue( version_at_least("0.3.3.0-alpha-dev", 0, 2, 7, 9) ) 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')) class TestFakeControlProtocol(unittest.TestCase): def test_happens(self): proto = FakeControlProtocol([]) events = [] def event_cb(*args, **kw): events.append((args, kw)) proto.add_event_listener("something", event_cb) proto.event_happened("something", "arg") self.assertEqual( [(("arg",), {})], events ) def test_happened_already(self): proto = FakeControlProtocol([]) events = [] def event_cb(*args, **kw): events.append((args, kw)) proto.event_happened("something", "arg") proto.add_event_listener("something", event_cb) self.assertEqual( [(("arg",), {})], events ) txtorcon-19.0.0/test/test_torstate.py0000644000175000017500000016053113416577210017626 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 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.circuit import _get_circuit_attacher from txtorcon.circuit import _extract_reason try: from .py3_torstate import TorStatePy3Tests # noqa except SyntaxError: pass @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 add_event_listener(self, *args): return defer.succeed(None) 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.add_event_listener = self.add_event_listener 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(2, len(x.all_routers)) self.assertIn('fake', x.routers) self.assertIn('ekaf', x.routers) 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)) attacher = yield _get_circuit_attacher(reactor, self.state) d = ep.connect('foo') 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_remove_routers(self): """ router removed from consensus is removed """ # 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 r Foo ABJJJJUFz1lvQS0jq8nhTdRiXEk /zzzUg1tKMUeyUBoyimzorbQN9E 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(2, len(self.state.all_routers)) self.assertTrue('Unnamed' in self.state.routers) self.assertTrue('Foo' in self.state.routers_by_name) self.assertTrue('$00126582E505CF596F412D23ABC9E14DD4625C49' in self.state.routers) self.assertTrue('$001249249505CF596F412D23ABC9E14DD4625C49' in self.state.routers) # this is a different fingerprint, but same name self.protocol.dataReceived(b'\r\n'.join(b'''650+NEWCONSENSUS r Unnamed ABBBguUFz1lvQS0jq8nhTdRiXEk /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.assertIn('Unnamed', self.state.routers) self.assertIn('$00104182E505CF596F412D23ABC9E14DD4625C49', self.state.routers) self.assertNotIn('$00126582E505CF596F412D23ABC9E14DD4625C49', self.state.routers) self.assertNotIn('$00126582E505CF596F412D23ABC9E14DD4625C49', self.state.routers_by_hash) self.assertNotIn('$001249249505CF596F412D23ABC9E14DD4625C49', self.state.routers) self.assertNotIn('$001249249505CF596F412D23ABC9E14DD4625C49', self.state.routers_by_hash) self.assertNotIn('Foo', self.state.routers_by_name) 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 @defer.inlineCallbacks def test_build_circuit_cancelled(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'] class FakeCircuit: close_called = False def when_built(self): return defer.Deferred() def close(self): self.close_called = True return defer.succeed(None) circ = FakeCircuit() def _build(*args, **kw): return defer.succeed(circ) self.state.build_circuit = _build timeout = 10 clock = task.Clock() # we want this circuit to get to BUILT, but *then* we call # .cancel() on the deferred -- in which case, the circuit must # be closed d = build_timeout_circuit(self.state, clock, path, timeout, using_guards=False) clock.advance(1) d.cancel() with self.assertRaises(CircuitBuildTimedOutError): yield d self.assertTrue(circ.close_called) 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 def test_build_circuit_failure(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', 'FAILED', 'REASON=TIMEOUT']) def check_reason(fail): self.assertEqual(fail.value.reason, 'TIMEOUT') d.addErrback(check_reason) return d def test_build_circuit_with_purpose(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'] d = self.state.build_circuit(path, using_guards=True, purpose="general") d.addCallback(self.circuit_callback) self.assertEqual(self.transport.value(), b'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002 purpose=general\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', 'FAILED', 'REASON=TIMEOUT']) def check_reason(fail): self.assertEqual(fail.value.reason, 'TIMEOUT') d.addErrback(check_reason) return d txtorcon-19.0.0/test/util.py0000644000175000017500000000066313106645477015705 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-19.0.0/test/test_torinfo.py0000644000175000017500000002244013100171552017422 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-19.0.0/test/py3_torstate.py0000644000175000017500000000601313252261646017355 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-19.0.0/test/test_onion.py0000644000175000017500000011013113403761212017063 0ustar mikemike00000000000000from __future__ import print_function import os import sys from mock import Mock from os.path import join from unittest import skipIf from twisted.trial import unittest from twisted.internet import defer from txtorcon import TorConfig from txtorcon import torconfig from txtorcon.onion import FilesystemOnionService from txtorcon.onion import FilesystemAuthenticatedOnionService from txtorcon.onion import EphemeralOnionService from txtorcon.onion import EphemeralAuthenticatedOnionService from txtorcon.onion import AuthStealth, AuthBasic, DISCARD from txtorcon.onion import _validate_ports_low_level from txtorcon.testutil import FakeControlProtocol _test_private_key = ( u'-----BEGIN RSA PRIVATE KEY-----\n' u'MIICXAIBAAKBgQC+bxV7+iEjJCmvQW/2SOYFQBsF06VuAdVKr3xTNMHgqI5mks6O\n' u'D8cizQ1nr0bL/bqtLPA2whUSvaJmDZjkmpC62v90YU1p99tGOv+ILZTzoIIjcWWn\n' u'3muDzA7p+zlN50x55ABuxEwQ3TfRA6nM1JF4HamYuHNae5nzbdwuxXpQ4wIDAQAB\n' u'AoGBAJLjbkf11M+dWkXjjLAE5OAR5YYmDYmAAnycRaKMpCtc+JIoFQlBJFI0pm1e\n' u'ppY8fVyMuDEUnVqaSYS8Yj2a95zD84hr0SzNFf5wSbffEcLIsmw7I18Mxq/YMrmy\n' u'oGwizMnhV/IVPKh40xctPl2cIpg9AdBLYgnc/sO8oBr5k+uRAkEA8B4jeVq4IYv/\n' u'b/kPzWiav/9weFMqKZdDh0O7ashbRe4b6CaHI2+XxX4uop9bFCTXsq73yCL7gqpU\n' u'AkzCPGWvmwJBAMsHqQQjKn7KlPezZsYL4FY2IkqKuq2x6vFWhMPfXl6y66Ya6/uO\n' u'of5kJUlolVcbvAEq4kLAk7nWi9RzWux/DFkCQHk1HX8StkPo4YZqWPm9RfCJRwLW\n' u'KEBaZPIQ1LhwbvJ74YZsfGb828YLjgr1GgqvFlrSS62xSviIdmO6z4mhYuUCQAK9\n' u'E7aOkuAq819z+Arr1hbTnBrNTD9Tiwu+UwQhWzCD0VHoQw6dmenIiAg5dOo74YlS\n' u'fsLPvi5fintPIwbVn+ECQCh6PEvaTP+fsPTyaRPOftCPqgLZbfzGnmt3ZJh1EB60\n' u'6X5Sz7FXRbQ8G5kmBy7opEoT4vsLMWGI+uq5WCXiuqY=\n' u'-----END RSA PRIVATE KEY-----' ) _test_onion_id = u'n7vc7sxqwqrm3vwo' # corresponds to above key # same as above private key, but without the markers + newlines # (e.g. for ADD_ONION etc) _test_private_key_blob = u''.join(_test_private_key.split(u'\n')[1:-1]) class OnionServiceTest(unittest.TestCase): @defer.inlineCallbacks def test_prop224_private_key(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) hsdir = self.mktemp() os.mkdir(hsdir) with open(join(hsdir, 'hs_ed25519_secret_key'), 'wb') as f: f.write(b'\x01\x02\x03\x04') with open(join(hsdir, 'hostname'), 'w') as f: f.write(u'{}.onion'.format(_test_onion_id)) hs_d = FilesystemOnionService.create( Mock(), config, hsdir=hsdir, ports=["80 127.0.0.1:4321"], version=3, ) # arrange HS_DESC callbacks so we get the hs instance back cb = protocol.events['HS_DESC'] for x in range(6): cb('UPLOAD {} UNKNOWN hsdir_{}'.format(_test_onion_id, x)) for x in range(6): cb('UPLOADED {} UNKNOWN hsdir_{}'.format(_test_onion_id, x)) hs = yield hs_d self.assertEqual(b'\x01\x02\x03\x04', hs.private_key) @defer.inlineCallbacks def test_set_ports(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) hsdir = self.mktemp() os.mkdir(hsdir) with open(join(hsdir, 'hs_ed25519_secret_key'), 'wb') as f: f.write(b'\x01\x02\x03\x04') with open(join(hsdir, 'hostname'), 'w') as f: f.write('{}.onion'.format(_test_onion_id)) hs_d = FilesystemOnionService.create( Mock(), config, hsdir=hsdir, ports=["80 127.0.0.1:4321"], version=3, ) # arrange HS_DESC callbacks so we get the hs instance back cb = protocol.events['HS_DESC'] cb('UPLOAD {} UNKNOWN hsdir0'.format(_test_onion_id)) cb('UPLOADED {} UNKNOWN hsdir0'.format(_test_onion_id)) hs = yield hs_d hs.ports = ["443 127.0.0.1:443"] self.assertEqual(1, len(hs.ports)) @defer.inlineCallbacks def test_set_dir(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) hsdir0 = self.mktemp() os.mkdir(hsdir0) hsdir1 = self.mktemp() os.mkdir(hsdir1) with open(join(hsdir0, "hostname"), "w") as f: f.write('{}.onion'.format(_test_onion_id)) hs_d = FilesystemOnionService.create( Mock(), config, hsdir=hsdir0, ports=["80 127.0.0.1:4321"], version=3, ) # arrange HS_DESC callbacks so we get the hs instance back cb = protocol.events['HS_DESC'] cb('UPLOAD {} UNKNOWN hsdir0'.format(_test_onion_id)) cb('UPLOADED {} UNKNOWN hsdir0'.format(_test_onion_id)) hs = yield hs_d hs.dir = hsdir1 self.assertEqual(hs.dir, hsdir1) @defer.inlineCallbacks def test_dir_ioerror(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) hsdir = self.mktemp() os.mkdir(hsdir) with open(join(hsdir, "hostname"), "w") as f: f.write("{}.onion".format(_test_onion_id)) hs_d = FilesystemOnionService.create( Mock(), config, hsdir=hsdir, ports=["80 127.0.0.1:4321"], ) # arrange HS_DESC callbacks so we get the hs instance back cb = protocol.events['HS_DESC'] cb('UPLOAD {} UNKNOWN hsdir0'.format(_test_onion_id)) cb('UPLOADED {} UNKNOWN hsdir0'.format(_test_onion_id)) hs = yield hs_d self.assertIs(None, hs.private_key) @defer.inlineCallbacks def test_dir_ioerror_v3(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) hsdir = self.mktemp() os.mkdir(hsdir) with open(join(hsdir, "hostname"), "w") as f: f.write('{}.onion'.format(_test_onion_id)) hs_d = FilesystemOnionService.create( Mock(), config, hsdir=hsdir, ports=["80 127.0.0.1:4321"], version=3, ) # arrange HS_DESC callbacks so we get the hs instance back cb = protocol.events['HS_DESC'] cb('UPLOAD {} UNKNOWN hsdir0'.format(_test_onion_id)) cb('UPLOADED {} UNKNOWN hsdir0'.format(_test_onion_id)) hs = yield hs_d self.assertIs(None, hs.private_key) @defer.inlineCallbacks def test_unknown_version(self): protocol = FakeControlProtocol([]) protocol.version = "0.1.1.1" config = TorConfig(protocol) hsdir = self.mktemp() os.mkdir(hsdir) hs = yield FilesystemOnionService.create( Mock(), config, hsdir=hsdir, ports=["80 127.0.0.1:4321"], version=99, ) with self.assertRaises(RuntimeError) as ctx: hs.private_key self.assertIn("Don't know how to load", str(ctx.exception)) def test_ephemeral_given_key(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) # returns a Deferred we're ignoring EphemeralOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], private_key=_test_private_key_blob, detach=True, ) cmd, d = protocol.commands[0] self.assertEqual(u"ADD_ONION RSA1024:{} Port=80,127.0.0.1:80 Flags=Detach".format(_test_private_key_blob), cmd) d.callback("PrivateKey={}\nServiceID={}".format(_test_private_key_blob, _test_onion_id)) def test_ephemeral_key_whitespace0(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) d = EphemeralOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], private_key=_test_private_key_blob + '\r', detach=True, ) return self.assertFailure(d, ValueError) def test_ephemeral_key_whitespace1(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) d = EphemeralOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], private_key=_test_private_key_blob + '\n', detach=True, ) return self.assertFailure(d, ValueError) def test_ephemeral_v3_no_key(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) # returns a Deferred we're ignoring EphemeralOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], detach=True, version=3, ) cmd, d = protocol.commands[0] self.assertEqual(u"ADD_ONION NEW:ED25519-V3 Port=80,127.0.0.1:80 Flags=Detach", cmd) d.callback("PrivateKey={}\nServiceID={}".format(_test_private_key_blob, _test_onion_id)) def test_ephemeral_v3_ip_addr_tuple(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) # returns a Deferred we're ignoring EphemeralOnionService.create( Mock(), config, ports=[(80, "192.168.1.2:80")], detach=True, version=3, ) cmd, d = protocol.commands[0] self.assertEqual(u"ADD_ONION NEW:ED25519-V3 Port=80,192.168.1.2:80 Flags=Detach", cmd) d.callback("PrivateKey={}\nServiceID={}".format(_test_private_key_blob, _test_onion_id)) def test_ephemeral_v3_non_anonymous(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) # returns a Deferred we're ignoring EphemeralOnionService.create( Mock(), config, ports=[(80, "192.168.1.2:80")], version=3, detach=True, single_hop=True, ) cmd, d = protocol.commands[0] self.assertEqual(u"ADD_ONION NEW:ED25519-V3 Port=80,192.168.1.2:80 Flags=Detach,NonAnonymous", cmd) d.callback("PrivateKey={}\nServiceID={}".format(_test_private_key_blob, _test_onion_id)) @defer.inlineCallbacks def test_ephemeral_v3_ip_addr_tuple_non_local(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) # returns a Deferred we're ignoring with self.assertRaises(ValueError): yield EphemeralOnionService.create( Mock(), config, ports=[(80, "hostname:80")], detach=True, version=3, ) @defer.inlineCallbacks def test_ephemeral_v3_wrong_key_type(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) privkey = 'RSA1024:{}'.format('a' * 32) with self.assertRaises(ValueError) as ctx: yield EphemeralOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], detach=True, version=3, private_key=privkey, ) self.assertIn( "but private key isn't", str(ctx.exception), ) @defer.inlineCallbacks def test_ephemeral_ports_not_a_list(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) privkey = 'a' * 32 with self.assertRaises(ValueError) as ctx: yield EphemeralOnionService.create( Mock(), config, ports="80 127.0.0.1:80", private_key=privkey, ) self.assertIn( "'ports' must be a list of strings", str(ctx.exception) ) def test_ephemeral_ports_not_strings(self): with self.assertRaises(ValueError) as ctx: _validate_ports_low_level([(80, "127.0.0.1:80")]) self.assertIn( "'ports' must be a list of strings", str(ctx.exception) ) @defer.inlineCallbacks def test_ephemeral_ports_no_spaces(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) privkey = 'a' * 32 with self.assertRaises(ValueError) as ctx: yield EphemeralOnionService.create( Mock(), config, ports=["80:127.0.0.1:80"], private_key=privkey, ) self.assertIn( "exactly one space", str(ctx.exception) ) @defer.inlineCallbacks def test_ephemeral_ports_no_colon(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) privkey = 'a' * 32 with self.assertRaises(ValueError) as ctx: yield EphemeralOnionService.create( Mock(), config, ports=["80 127.0.0.1;80"], private_key=privkey, ) self.assertIn( "local address should be 'IP:port'", str(ctx.exception) ) @defer.inlineCallbacks def test_ephemeral_ports_non_local(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) privkey = 'a' * 32 with self.assertRaises(ValueError) as ctx: yield EphemeralOnionService.create( Mock(), config, ports=["80 8.8.8.8:80"], private_key=privkey, ) self.assertIn( "should be a local address", str(ctx.exception) ) @defer.inlineCallbacks def test_ephemeral_ports_not_an_int(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) privkey = 'a' * 32 with self.assertRaises(ValueError) as ctx: yield EphemeralOnionService.create( Mock(), config, ports=["web 127.0.0.1:80"], private_key=privkey, ) self.assertIn( "external port isn't an int", str(ctx.exception) ) @defer.inlineCallbacks def test_filesystem_wrong_ports(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) with self.assertRaises(ValueError) as ctx: yield FilesystemOnionService.create( Mock(), config, "/dev/null", ports="80 127.0.0.1:80", ) self.assertIn( "'ports' must be a list of strings", str(ctx.exception) ) @defer.inlineCallbacks def test_descriptor_all_uploads_fail(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) progress_messages = [] def progress(*args): progress_messages.append(args) eph_d = EphemeralOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], progress=progress, private_key=DISCARD, ) cmd, d = protocol.commands[0] self.assertEqual(u"ADD_ONION NEW:BEST Port=80,127.0.0.1:80 Flags=DiscardPK", cmd) d.callback("PrivateKey={}\nServiceID={}".format(_test_private_key_blob, _test_onion_id)) # get the event-listener callback that torconfig code added cb = protocol.events['HS_DESC'] for x in range(6): cb('UPLOAD {} UNKNOWN hsdir_{}'.format(_test_onion_id, x)) for x in range(6): cb('FAILED {} UNKNOWN hsdir_{}'.format(_test_onion_id, x)) # now when we wait for our onion, it should already be failed # because all 6 uploads failed. with self.assertRaises(RuntimeError) as ctx: yield eph_d self.assertIn("Failed to upload", str(ctx.exception)) for x in range(6): self.assertIn("hsdir_{}".format(x), str(ctx.exception)) def test_ephemeral_bad_return_value(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) progress_messages = [] def progress(*args): progress_messages.append(args) eph_d = EphemeralOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], progress=progress, private_key=DISCARD, ) cmd, d = protocol.commands[0] self.assertEqual(u"ADD_ONION NEW:BEST Port=80,127.0.0.1:80 Flags=DiscardPK", cmd) d.callback("BadKey=nothing") def check(f): self.assertIn("Expected ADD_ONION to return ServiceID", str(f.value)) return None eph_d.addCallbacks(self.fail, check) return eph_d @defer.inlineCallbacks def test_ephemeral_remove(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) eph_d = EphemeralOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], ) cmd, d = protocol.commands[0] self.assertEqual(u"ADD_ONION NEW:BEST Port=80,127.0.0.1:80", cmd) d.callback("PrivateKey={}\nServiceID={}".format(_test_private_key_blob, _test_onion_id)) cb = protocol.events['HS_DESC'] for x in range(6): cb('UPLOAD {} UNKNOWN hsdir_{}'.format(_test_onion_id, x)) for x in range(6): cb('UPLOADED {} UNKNOWN hsdir_{}'.format(_test_onion_id, x)) hs = yield eph_d remove_d = hs.remove() cmd, d = protocol.commands[-1] self.assertEqual(u"DEL_ONION {}".format(_test_onion_id), cmd) d.callback('OK') yield remove_d @defer.inlineCallbacks def test_ephemeral_remove_not_ok(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) eph_d = EphemeralOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], ) cmd, d = protocol.commands[0] self.assertEqual(u"ADD_ONION NEW:BEST Port=80,127.0.0.1:80", cmd) d.callback("PrivateKey={}\nServiceID={}".format(_test_private_key_blob, _test_onion_id)) cb = protocol.events['HS_DESC'] for x in range(6): cb('UPLOAD {} UNKNOWN hsdir_{}'.format(_test_onion_id, x)) for x in range(6): cb('UPLOADED {} UNKNOWN hsdir_{}'.format(_test_onion_id, x)) hs = yield eph_d remove_d = hs.remove() cmd, d = protocol.commands[-1] self.assertEqual(u"DEL_ONION {}".format(_test_onion_id), cmd) d.callback('bad stuff') with self.assertRaises(RuntimeError): yield remove_d def test_ephemeral_ver_option(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) hs = EphemeralOnionService( config, ports=["80 127.0.0.1:80"], ver=2, ) self.assertEqual(2, hs.version) def test_ephemeral_extra_kwargs(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) with self.assertRaises(ValueError) as ctx: EphemeralOnionService( config, ports=["80 127.0.0.1:80"], ver=2, something_funny="foo", ) self.assertIn( "Unknown kwarg", str(ctx.exception), ) @defer.inlineCallbacks def test_ephemeral_auth_stealth(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) with self.assertRaises(ValueError) as ctx: yield EphemeralAuthenticatedOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], auth=AuthStealth(["steve", "carol"]), ) self.assertIn( "Tor does not yet support", str(ctx.exception), ) @defer.inlineCallbacks def test_old_tor_version(self): protocol = FakeControlProtocol([]) protocol.version = "0.1.2.3" config = TorConfig(protocol) hsdir = self.mktemp() def my_progress(a, b, c): pass eph_d = FilesystemOnionService.create( Mock(), config, hsdir, ports=["80 127.0.0.1:80"], progress=my_progress, ) yield eph_d @defer.inlineCallbacks def test_tor_version_v3_progress(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) hsdir = self.mktemp() os.mkdir(hsdir) with open(join(hsdir, "hostname"), "w") as f: f.write('{}.onion'.format(_test_onion_id)) def my_progress(a, b, c): pass eph_d = FilesystemOnionService.create( Mock(), config, hsdir, ports=["80 127.0.0.1:80"], progress=my_progress, version=3, ) # arrange HS_DESC callbacks so we get the hs instance back cb = protocol.events['HS_DESC'] cb('UPLOAD {} UNKNOWN hsdir0'.format(_test_onion_id)) cb('UPLOADED {} UNKNOWN hsdir0'.format(_test_onion_id)) yield eph_d @defer.inlineCallbacks def test_tor_version_v3_progress_await_all(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) hsdir = self.mktemp() os.mkdir(hsdir) with open(join(hsdir, "hostname"), "w") as f: f.write('{}.onion'.format(_test_onion_id)) class Bad(Exception): pass def my_progress(a, b, c): raise Bad("it's bad") eph_d = FilesystemOnionService.create( Mock(), config, hsdir, ports=["80 127.0.0.1:80"], progress=my_progress, version=3, await_all_uploads=True, ) # arrange HS_DESC callbacks so we get the hs instance back cb = protocol.events['HS_DESC'] cb('UPLOAD {} UNKNOWN hsdir0'.format(_test_onion_id)) cb('UPLOADED {} UNKNOWN hsdir0'.format(_test_onion_id)) yield eph_d errs = self.flushLoggedErrors(Bad) self.assertEqual(3, len(errs)) # because there's a "100%" one too @skipIf('pypy' in sys.version.lower(), "Weird OpenSSL+PyPy problem on Travis") @defer.inlineCallbacks def test_ephemeral_auth_basic(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) eph_d = EphemeralAuthenticatedOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], auth=AuthBasic([ "steve", ("carol", "c4r0ls33kr1t"), ]), ) cmd, d = protocol.commands[0] self.assertTrue( cmd.startswith( u"ADD_ONION NEW:BEST Port=80,127.0.0.1:80 Flags=BasicAuth " ) ) self.assertIn(u"ClientAuth=steve", cmd) self.assertIn(u"ClientAuth=carol:c4r0ls33kr1t", cmd) d.callback("PrivateKey={}\nServiceID={}\nClientAuth=steve:aseekritofsomekind".format(_test_private_key_blob, _test_onion_id)) cb = protocol.events['HS_DESC'] for x in range(6): cb('UPLOAD {} UNKNOWN hsdir_{}'.format(_test_onion_id, x)) for x in range(6): cb('UPLOADED {} UNKNOWN hsdir_{}'.format(_test_onion_id, x)) hs = yield eph_d self.assertEqual( set(["steve", "carol"]), set(hs.client_names()), ) steve = hs.get_client("steve") self.assertEqual( "aseekritofsomekind", steve.auth_token, ) self.assertEqual( "{}.onion".format(_test_onion_id), steve.hostname, ) self.assertEqual( set(["80 127.0.0.1:80"]), steve.ports, ) self.assertTrue(steve.parent is hs) self.assertEqual("steve", steve.name) self.assertEqual(2, steve.version) carol = hs.get_client("carol") self.assertEqual( "c4r0ls33kr1t", carol.auth_token, ) self.assertEqual( "{}.onion".format(_test_onion_id), carol.hostname, ) remove_d = hs.remove() cmd, d = protocol.commands[-1] self.assertEqual(u"DEL_ONION {}".format(_test_onion_id), cmd) d.callback('OK') yield remove_d @skipIf('pypy' in sys.version.lower(), "Weird OpenSSL+PyPy problem on Travis") @defer.inlineCallbacks def test_ephemeral_auth_basic_remove_fails(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) eph_d = EphemeralAuthenticatedOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], auth=AuthBasic([ "steve", ("carol", "c4r0ls33kr1t"), ]), ) cmd, d = protocol.commands[0] self.assertTrue( cmd.startswith( u"ADD_ONION NEW:BEST Port=80,127.0.0.1:80 Flags=BasicAuth " ) ) self.assertIn(u"ClientAuth=steve", cmd) self.assertIn(u"ClientAuth=carol:c4r0ls33kr1t", cmd) d.callback( "PrivateKey={}\nServiceID={}\nClientAuth=steve:aseekritofsomekind".format( _test_private_key_blob, _test_onion_id, ) ) cb = protocol.events['HS_DESC'] for x in range(6): cb('UPLOAD {} UNKNOWN hsdir_{}'.format(_test_onion_id, x)) for x in range(6): cb('UPLOADED {} UNKNOWN hsdir_{}'.format(_test_onion_id, x)) hs = yield eph_d self.assertEqual( set(["steve", "carol"]), set(hs.client_names()), ) steve = hs.get_client("steve") self.assertEqual( "aseekritofsomekind", steve.auth_token, ) self.assertEqual( "{}.onion".format(_test_onion_id), steve.hostname, ) self.assertEqual( set(["80 127.0.0.1:80"]), steve.ports, ) self.assertTrue(steve.parent is hs) self.assertEqual("steve", steve.name) self.assertEqual(2, steve.version) carol = hs.get_client("carol") self.assertEqual( "c4r0ls33kr1t", carol.auth_token, ) self.assertEqual( "{}.onion".format(_test_onion_id), carol.hostname, ) remove_d = hs.remove() cmd, d = protocol.commands[-1] self.assertEqual(u"DEL_ONION {}".format(_test_onion_id), cmd) d.callback('not okay') with self.assertRaises(RuntimeError): yield remove_d def test_ephemeral_auth_basic_bad_name(self): with self.assertRaises(ValueError) as ctx: AuthBasic(["bad name"]) self.assertIn( "Client names can't have spaces", str(ctx.exception), ) @defer.inlineCallbacks def test_ephemeral_auth_unknown(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) with self.assertRaises(ValueError) as ctx: yield EphemeralAuthenticatedOnionService.create( Mock(), config, ports=["80 127.0.0.1:80"], auth=["carol", "steve"], ) self.assertIn( "'auth' should be an AuthBasic or AuthStealth instance", str(ctx.exception), ) @defer.inlineCallbacks def test_ephemeral_ports_bad0(self): protocol = FakeControlProtocol([]) config = TorConfig(protocol) with self.assertRaises(ValueError) as ctx: yield EphemeralAuthenticatedOnionService.create( Mock(), config, ports="80 127.0.0.1:80", auth=AuthBasic(["xavier"]), ) self.assertIn( "'ports' must be a list of strings", str(ctx.exception), ) def test_ephemeral_ports_bad1(self): with self.assertRaises(ValueError) as ctx: _validate_ports_low_level([80]) self.assertIn( "'ports' must be a list of strings", str(ctx.exception), ) def test_ephemeral_ports_bad2(self): with self.assertRaises(ValueError) as ctx: _validate_ports_low_level("not even a list") self.assertIn( "'ports' must be a list of strings", str(ctx.exception), ) 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 first call [0] was to add_event_listener; we want the # [1] arg of that cb = proto.method_calls[0][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 first call [0] was to add_event_listener; we want the # [1] arg of that cb = proto.method_calls[0][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 first call [0] was to add_event_listener; we want the # [1] arg of that cb = proto.method_calls[0][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) class AuthenticatedFilesystemHiddenServiceTest(unittest.TestCase): def setUp(self): self.thedir = self.mktemp() os.mkdir(self.thedir) protocol = FakeControlProtocol([]) self.config = TorConfig(protocol) self.hs = FilesystemAuthenticatedOnionService( config=self.config, thedir=self.thedir, ports=["80 127.0.0.1:1234"], auth=AuthBasic(['foo', 'bar']) ) def test_create_progress_old_tor(self): hsdir = "/dev/null" ports = ["80 127.0.0.1:1234"] def progress(pct, tag, msg): pass # print(pct, tag, msg) self.config.tor_protocol.version = "0.2.0.0" FilesystemAuthenticatedOnionService.create( Mock(), self.config, hsdir, ports, auth=AuthBasic(['alice']), progress=progress, ) def test_unknown_auth_type(self): with self.assertRaises(ValueError) as ctx: FilesystemAuthenticatedOnionService( self.config, self.thedir, ["80 127.0.0.1:1234"], auth=object(), ) self.assertIn( "must be one of AuthBasic or AuthStealth", str(ctx.exception), ) def test_bad_client_name(self): with self.assertRaises(ValueError) as ctx: FilesystemAuthenticatedOnionService( self.config, self.thedir, ["80 127.0.0.1:1234"], auth=AuthBasic(["bob can't have spaces"]), ) self.assertIn( "can't have spaces", str(ctx.exception), ) def test_get_client_missing(self): with open(join(self.thedir, "hostname"), "w") as f: f.write( "foo.onion fooauthtoken # client: foo\n" "bar.onion barauthtoken # client: bar\n" ) with self.assertRaises(KeyError) as ctx: self.hs.get_client("quux") self.assertIn( "No such client", str(ctx.exception), ) def test_get_client(self): with open(join(self.thedir, "hostname"), "w") as f: f.write( "foo.onion fooauthtoken # client: foo\n" "bar.onion barauthtoken # client: bar\n" ) client = self.hs.get_client("foo") with self.assertRaises(KeyError): client.private_key client.group_readable def test_get_client_private_key_error(self): with open(join(self.thedir, "hostname"), "w") as f: f.write( "foo.onion fooauthtoken # client: foo\n" "bar.onion barauthtoken # client: bar\n" ) with open(join(self.thedir, "client_keys"), "w") as f: f.write("foo blargly baz baz\n") client = self.hs.get_client("foo") with self.assertRaises(RuntimeError) as ctx: client.private_key self.assertIn( "Parse error at", str(ctx.exception), ) def test_get_client_expected_not_found(self): self.hs = FilesystemAuthenticatedOnionService( self.config, self.thedir, ["80 127.0.0.1:1234"], auth=AuthBasic(["foo", "bar", "baz"]), ) with open(join(self.thedir, "hostname"), "w") as f: f.write( "foo.onion fooauthtoken # client: foo\n" "bar.onion barauthtoken # client: bar\n" ) with self.assertRaises(RuntimeError) as ctx: self.hs.get_client("baz") self.assertIn( "Didn't find expected client", str(ctx.exception), ) txtorcon-19.0.0/test/profile_startup.py0000644000175000017500000000076613073067516020151 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-19.0.0/test/test_microdesc.py0000644000175000017500000000700213106645477017731 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-19.0.0/test/test_log.py0000644000175000017500000000023412752747562016546 0ustar mikemike00000000000000from twisted.trial import unittest from txtorcon import log class LoggingTests(unittest.TestCase): def test_debug(self): log.debug_logging() txtorcon-19.0.0/test/test_endpoints.py0000644000175000017500000021044613403761212017756 0ustar mikemike00000000000000from __future__ import print_function import os import sys from mock import patch from mock import Mock, MagicMock from unittest import skipIf from binascii import b2a_base64 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 cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization 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.onion import IAuthenticatedOnionClients from txtorcon.onion import IOnionService from txtorcon.onion import _compute_permanent_id from txtorcon import AuthStealth from txtorcon import AuthBasic 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_onion import _test_private_key # put in testutil? from .test_onion import _test_private_key_blob # put in testutil? from .test_onion import _test_onion_id # put in testutil? from txtorcon.testutil import FakeControlProtocol @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.protocol.answers.append( 'config/names=\nHiddenServiceOptions Virtual\nControlPort LineList\nSOCKSPort LineList' ) self.protocol.answers.append('config/defaults=') 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.protocol.answers.append({'SOCKSPort': '9050'}) self.protocol.answers.append({'onions/detached': ''}) self.protocol.answers.append({'onions/current': ''}) self.patcher = patch( 'txtorcon.controller.find_tor_binary', return_value='/not/tor' ) self.patcher.start() self.config = TorConfig(self.protocol) d = defer.Deferred() self.config.post_bootstrap.addCallback(lambda _: d.callback(self.config)) return d 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): fake_tor = Mock() fake_tor.get_config = Mock(return_value=self.config) config = yield get_global_tor( Mock(), _tor_launcher=lambda x, progress_updates=None: fake_tor ) # XXX this was asserting SOCKSPort == 0 before; why? self.assertEqual(['9050'], config.SOCKSPort) @defer.inlineCallbacks def test_global_tor_error(self, ftb): yield get_global_tor( reactor=FakeReactorTcp(self), _tor_launcher=lambda x, y, progress_updates=None: 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=FakeReactorTcp(self), control_port=111, _tor_launcher=lambda x, y, progress_updates=None: 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): with patch('txtorcon.endpoints._global_tor_config'): with patch('txtorcon.controller.launch') as launch: m = Mock() directlyProvides(m, IReactorCore) yield TCPHiddenServiceEndpoint.private_tor( reactor=m, public_port=80, control_port=1234, ) self.assertTrue(launch.called) @defer.inlineCallbacks def test_private_tor_no_control_port(self, ftb): with patch('txtorcon.endpoints._global_tor_config'): with patch('txtorcon.controller.launch') as launch: yield TCPHiddenServiceEndpoint.private_tor(self.reactor, 80) self.assertTrue(len(launch.mock_calls) > 1) @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.controller.launch') 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_d = ep.listen(NoOpProtocolFactory()) self.protocol.commands[0][1].callback("ServiceID=service\nPrivateKey=blob") self.protocol.events['HS_DESC']('UPLOAD service x x x x') self.protocol.events['HS_DESC']('UPLOADED service x x x x') port = yield port_d toa = port.getHost() self.assertTrue(hasattr(toa, 'onion_uri')) self.assertTrue(hasattr(toa, 'onion_port')) port.startListening() str(port) port.tor_config port.local_address with self.assertRaises(ValueError) as ctx: port.hidden_service_dir self.assertIn( "our _service doesn't provide IFilesystemOnionService", str(ctx.exception) ) # system_tor should be connecting to a running one, # *not* launching a new one. self.assertFalse(launch_mock.called) @defer.inlineCallbacks def test_system_tor_explit_dir_not_exist(self, ftb): # same as above, but we pass an explicit (but non-existent) # hsdir and then simulate Tor creating it... 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 hsdir = self.mktemp() # not creating it with patch('txtorcon.controller.launch') 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, hidden_service_dir=hsdir) port_d = ep.listen(NoOpProtocolFactory()) # Tor would create the hsdir "approximately now" os.mkdir(hsdir) with open(os.path.join(hsdir, "hostname"), "w") as f: f.write("service.onion\n") self.protocol.events['HS_DESC']('UPLOAD service x x x x') self.protocol.events['HS_DESC']('UPLOADED service x x x x') port = yield port_d toa = port.getHost() self.assertTrue(hasattr(toa, 'onion_uri')) self.assertTrue(hasattr(toa, 'onion_port')) port.startListening() str(port) port.tor_config port.hidden_service_dir # system_tor should be connecting to a running one, # *not* launching a new one. self.assertFalse(launch_mock.called) @defer.inlineCallbacks def test_system_tor_explit_dir_not_readable0(self, ftb): # same as above, but we pass an explicit (but non-existent) # hsdir and then simulate Tor creating it... 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 hsdir = self.mktemp() os.mkdir(hsdir) with patch('txtorcon.controller.launch') 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, hidden_service_dir=hsdir) port_d = ep.listen(NoOpProtocolFactory()) fname = os.path.join(hsdir, "hostname") with open(fname, 'w') as f: f.write("service.onion") self.protocol.events['HS_DESC']('UPLOAD service x x x x') self.protocol.events['HS_DESC']('UPLOADED service x x x x') port = yield port_d 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) # make it re-read the hostname information ep.hiddenservice._hostname = None # make an IOError happen when we try to read the hostname os.chmod(fname, 0x0) # ...but this eats it and returns None self.assertIs(None, ep.onion_uri) @defer.inlineCallbacks def test_system_tor_explit_dir_not_readable_version3(self, ftb): # same as above, but we pass an explicit (but non-existent) # hsdir and then simulate Tor creating it... 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 hsdir = self.mktemp() os.mkdir(hsdir) with patch('txtorcon.controller.launch') 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, hidden_service_dir=hsdir) port_d = ep.listen(NoOpProtocolFactory()) fname = os.path.join(hsdir, "hostname") with open(fname, 'w') as f: f.write("service.onion") self.protocol.events['HS_DESC']('UPLOAD service x x x x') self.protocol.events['HS_DESC']('UPLOADED service x x x x') # make it re-read the hostname information ep.hiddenservice._hostname = None # make an IOError happen when we try to read the hostname os.chmod(fname, 0x0) port = yield port_d 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_explit_dir_not_readable_version2(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 hsdir = self.mktemp() os.mkdir(hsdir) with patch('txtorcon.controller.launch') 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, hidden_service_dir=hsdir, version=2, ) port_d = ep.listen(NoOpProtocolFactory()) fname = os.path.join(hsdir, "hostname") with open(fname, 'w') as f: f.write("service.onion") self.protocol.events['HS_DESC']('UPLOAD service x x x x') self.protocol.events['HS_DESC']('UPLOADED service x x x x') # make it re-read the hostname information ep.hiddenservice._hostname = None # make an IOError happen when we try to read the hostname os.chmod(fname, 0x0) fname = os.path.join(hsdir, "private_key") with open(fname, 'w') as f: f.write("privkey") os.chmod(fname, 0x0) port = yield port_d 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) self.assertIs(None, port.onion_service.private_key) @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) assert self.config.post_bootstrap.called 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) @defer.inlineCallbacks def test_basic_auth(self, ftb): reactor = proto_helpers.MemoryReactor() reactor.addSystemEventTrigger = Mock() privkey = ( b'-----BEGIN RSA PRIVATE KEY-----\n' b'MIICXAIBAAKBgQC+bxV7+iEjJCmvQW/2SOYFQBsF06VuAdVKr3xTNMHgqI5mks6O\n' b'D8cizQ1nr0bL/bqtLPA2whUSvaJmDZjkmpC62v90YU1p99tGOv+ILZTzoIIjcWWn\n' b'3muDzA7p+zlN50x55ABuxEwQ3TfRA6nM1JF4HamYuHNae5nzbdwuxXpQ4wIDAQAB\n' b'AoGBAJLjbkf11M+dWkXjjLAE5OAR5YYmDYmAAnycRaKMpCtc+JIoFQlBJFI0pm1e\n' b'ppY8fVyMuDEUnVqaSYS8Yj2a95zD84hr0SzNFf5wSbffEcLIsmw7I18Mxq/YMrmy\n' b'oGwizMnhV/IVPKh40xctPl2cIpg9AdBLYgnc/sO8oBr5k+uRAkEA8B4jeVq4IYv/\n' b'b/kPzWiav/9weFMqKZdDh0O7ashbRe4b6CaHI2+XxX4uop9bFCTXsq73yCL7gqpU\n' b'AkzCPGWvmwJBAMsHqQQjKn7KlPezZsYL4FY2IkqKuq2x6vFWhMPfXl6y66Ya6/uO\n' b'of5kJUlolVcbvAEq4kLAk7nWi9RzWux/DFkCQHk1HX8StkPo4YZqWPm9RfCJRwLW\n' b'KEBaZPIQ1LhwbvJ74YZsfGb828YLjgr1GgqvFlrSS62xSviIdmO6z4mhYuUCQAK9\n' b'E7aOkuAq819z+Arr1hbTnBrNTD9Tiwu+UwQhWzCD0VHoQw6dmenIiAg5dOo74YlS\n' b'fsLPvi5fintPIwbVn+ECQCh6PEvaTP+fsPTyaRPOftCPqgLZbfzGnmt3ZJh1EB60\n' b'6X5Sz7FXRbQ8G5kmBy7opEoT4vsLMWGI+uq5WCXiuqY=\n' b'-----END RSA PRIVATE KEY-----\n' ) perm_id = _compute_permanent_id( serialization.load_pem_private_key( privkey, password=None, backend=default_backend(), ) ) hsdir = self.mktemp() os.mkdir(hsdir) with open(os.path.join(hsdir, 'private_key'), 'wb') as f: f.write(privkey) ep = TCPHiddenServiceEndpoint( reactor, self.config, 123, ephemeral=False, hidden_service_dir=hsdir, auth=AuthBasic(['alice', 'bob']), ) assert self.config.post_bootstrap.called yield self.config.post_bootstrap self.assertTrue(IProgressProvider.providedBy(ep)) port_d = ep.listen(NoOpProtocolFactory()) self.assertEqual(4, len(self.protocol.sets)) hsdesc = self.protocol.events['HS_DESC'] hsdesc("UPLOAD {} x x x x".format(perm_id)) hsdesc("UPLOADED {} x x x x".format(perm_id)) yield port_d @defer.inlineCallbacks def test_not_ephemeral_no_hsdir(self, ftb): listen = RuntimeError("listen") connect = RuntimeError("connect") reactor = proto_helpers.RaisingMemoryReactor(listen, connect) reactor.addSystemEventTrigger = Mock() ep = TCPHiddenServiceEndpoint(reactor, self.config, 123, ephemeral=False) assert self.config.post_bootstrap.called 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_error(self, ftb): config = TorConfig() ep = TCPHiddenServiceEndpoint(self.reactor, config, 123) self.assertTrue(IProgressProvider.providedBy(ep)) prog = IProgressProvider(ep) class CustomBadness(Exception): pass def boom(*args, **kw): raise CustomBadness("the bad stuff") prog.add_progress_listener(boom) args = (50, "blarg", "Doing that thing we talked about.") # kind-of cheating, test-wise? ep._tor_progress_update(*args) ep._descriptor_progress_update(*args) # if we ignore the progress-listener error: success errs = self.flushLoggedErrors(CustomBadness) self.assertEqual(2, len(errs)) def test_progress_updates_private_tor(self, ftb): with patch('txtorcon.controller.launch') as tor: with patch('txtorcon.endpoints._global_tor_config'): ep = TCPHiddenServiceEndpoint.private_tor(self.reactor, 1234) self.assertTrue(len(tor.mock_calls) > 1) tor.mock_calls[0][2]['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_single_hop_non_ephemeral(self, ftb): control_ep = Mock() control_ep.connect = Mock(return_value=defer.succeed(None)) directlyProvides(control_ep, IStreamClientEndpoint) with self.assertRaises(ValueError) as ctx: TCPHiddenServiceEndpoint.system_tor( self.reactor, control_ep, 1234, ephemeral=False, single_hop=True, ) self.assertIn("single_hop=", str(ctx.exception)) def test_progress_updates_global_tor(self, ftb): with patch('txtorcon.endpoints.get_global_tor_instance') 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): return None 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() ep.listen(NoOpProtocolFactory()) defer.returnValue(arg) return d0.addBoth(more_listen) self.protocol.commands[0][1].callback( 'ServiceID=blarglyfoo\nPrivateKey=bigbadkeyblob' ) self.protocol.events['HS_DESC']( 'UPLOAD blarglyfoo x x x x' ) self.protocol.events['HS_DESC']( 'UPLOADED blarglyfoo x x x x' ) def check(port): self.assertEqual('blarglyfoo.onion', port.getHost().onion_uri) self.assertEqual('127.0.0.1', ep.tcp_endpoint._interface) self.assertEqual(len(self.config.EphemeralOnionServices), 1) d0.addCallback(check).addErrback(self.fail) return d0 def test_multiple_disk(self, ftb): tmp = self.mktemp() os.mkdir(tmp) with open(os.path.join(tmp, 'hostname'), 'w') as f: f.write('blarglyfoo.onion') with open(os.path.join(tmp, 'private_key'), 'w') as f: f.write('some hex or something') ep = TCPHiddenServiceEndpoint(self.reactor, self.config, 123, hidden_service_dir=tmp) d0 = ep.listen(NoOpProtocolFactory()) @defer.inlineCallbacks def more_listen(arg): yield arg.stopListening() ep.listen(NoOpProtocolFactory()) defer.returnValue(arg) return d0.addBoth(more_listen) if True: self.protocol.events['HS_DESC']( 'UPLOAD blarglyfoo x x x x' ) self.protocol.events['HS_DESC']( 'UPLOADED blarglyfoo x x x x' ) def check(port): self.assertEqual('blarglyfoo.onion', port.getHost().onion_uri) self.assertEqual('some hex or something', port.getHost().onion_key) self.assertEqual('127.0.0.1', ep.tcp_endpoint._interface) self.assertEqual(len(self.config.HiddenServices), 1) self.assertIs(self.config.HiddenServices[0], port.onion_service) self.assertTrue(IOnionService.providedBy(port.getHost().onion_service)) d0.addCallback(check).addErrback(self.fail) return d0 # XXX what is this even supposed to test? def _test_already_bootstrapped(self, ftb): self.config.bootstrap() ep = TCPHiddenServiceEndpoint(self.reactor, self.config, 123) d = ep.listen(NoOpProtocolFactory()) self.protocol.commands[0][1].callback("ServiceID=gobbledegook\nPrivateKey=seekrit") self.protocol.events['HS_DESC']( "UPLOAD gobbledegook basic somedirauth REASON=testing" ) self.protocol.events['HS_DESC']( "UPLOADED gobbledegook basic somedirauth REASON=testing" ) 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.onion') ep = TCPHiddenServiceEndpoint(self.reactor, self.config, 123, d) # make sure listen() correctly configures our hidden-serivce # with the explicit directory we passed in above listen_d = ep.listen(NoOpProtocolFactory()) self.protocol.events['HS_DESC']( "UPLOAD public basic somedirauth REASON=testing" ) self.protocol.events['HS_DESC']( "UPLOADED public basic somedirauth REASON=testing" ) yield listen_d self.assertEqual(1, len(self.config.HiddenServices)) self.assertEqual(self.config.HiddenServices[0].dir, d) self.assertEqual(self.config.HiddenServices[0].hostname, 'public.onion') 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, progress_updates=None: 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_via_plugin_key_from_file(self, ftb): tmp = self.mktemp() os.mkdir(tmp) with open(os.path.join(tmp, 'some_data'), 'wb') as f: f.write(b'ED25519-V3:deadbeefdeadbeef\n') # 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, progress_updates=None: defer.succeed(config) ) ep = serverFromString( self.reactor, 'onion:88:localPort=1234:privateKeyFile={}'.format(os.path.join(tmp, 'some_data')), ) self.assertEqual(ep.public_port, 88) self.assertEqual(ep.local_port, 1234) self.assertEqual(ep.private_key, "ED25519-V3:deadbeefdeadbeef") def test_parse_via_plugin_key_from_v3_private_file(self, ftb): tmp = self.mktemp() os.mkdir(tmp) with open(os.path.join(tmp, 'some_data'), 'wb') as f: f.write(b'== ed25519v1-secret: type0 ==\x00\x00\x00H\x9e\xa6j\x0e\x98\x85\xa9\xec\xee@\x9d&\xe2\xbfe\xc9\x90\xb9\xcb\xb2g\xb0\xab\xe4\xd0\x14c\xb0\xb2\x9dX\xfa\xaa\xf8,di8\xec\xc6\x82t\xd0A\x16>u\xde\xc6&\x82\x03\x1app\x18c`T\xc3\xdc\x1a\xca') # 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, progress_updates=None: defer.succeed(config) ) ep = serverFromString( self.reactor, 'onion:88:localPort=1234:privateKeyFile={}'.format(os.path.join(tmp, 'some_data')), ) self.assertEqual(ep.public_port, 88) self.assertEqual(ep.local_port, 1234) self.assertTrue("\n" not in ep.private_key) self.assertEqual( ep.private_key, u"ED25519-V3:" + b2a_base64(b"H\x9e\xa6j\x0e\x98\x85\xa9\xec\xee@\x9d&\xe2\xbfe\xc9\x90\xb9\xcb\xb2g\xb0\xab\xe4\xd0\x14c\xb0\xb2\x9dX\xfa\xaa\xf8,di8\xec\xc6\x82t\xd0A\x16>u\xde\xc6&\x82\x03\x1app\x18c`T\xc3\xdc\x1a\xca").decode('ascii').strip(), ) def test_parse_via_plugin_key_from_v2_private_file(self, ftb): tmp = self.mktemp() os.mkdir(tmp) with open(os.path.join(tmp, 'some_data'), 'w') as f: f.write('-----BEGIN RSA PRIVATE KEY-----\nthekeyblob\n-----END RSA PRIVATE KEY-----\n') # 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, progress_updates=None: defer.succeed(config) ) ep = serverFromString( self.reactor, 'onion:88:localPort=1234:privateKeyFile={}'.format(os.path.join(tmp, 'some_data')), ) self.assertEqual(ep.public_port, 88) self.assertEqual(ep.local_port, 1234) self.assertEqual( ep.private_key, u"RSA1024:thekeyblob", ) def test_parse_via_plugin_key_from_invalid_private_file(self, ftb): tmp = self.mktemp() os.mkdir(tmp) with open(os.path.join(tmp, 'some_data'), 'w') as f: f.write('nothing to see here\n') # 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, progress_updates=None: defer.succeed(config) ) with self.assertRaises(ValueError): serverFromString( self.reactor, 'onion:88:localPort=1234:privateKeyFile={}'.format(os.path.join(tmp, 'some_data')), ) def test_parse_via_plugin_single_hop(self, ftb): tmp = self.mktemp() os.mkdir(tmp) with open(os.path.join(tmp, 'some_data'), 'wb') as f: f.write(b'ED25519-V3:deadbeefdeadbeef\n') # 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, progress_updates=None: defer.succeed(config) ) ep = serverFromString( self.reactor, 'onion:88:localPort=1234:singleHop=True:privateKeyFile={}'.format(os.path.join(tmp, 'some_data')), ) self.assertEqual(ep.public_port, 88) self.assertEqual(ep.local_port, 1234) self.assertEqual(ep.private_key, "ED25519-V3:deadbeefdeadbeef") self.assertTrue(ep.single_hop) def test_parse_via_plugin_single_hop_explicit_false(self, ftb): tmp = self.mktemp() os.mkdir(tmp) with open(os.path.join(tmp, 'some_data'), 'wb') as f: f.write(b'ED25519-V3:deadbeefdeadbeef\n') # 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, progress_updates=None: defer.succeed(config) ) ep = serverFromString( self.reactor, 'onion:88:localPort=1234:singleHop=false:privateKeyFile={}'.format(os.path.join(tmp, 'some_data')), ) self.assertEqual(ep.public_port, 88) self.assertEqual(ep.local_port, 1234) self.assertEqual(ep.private_key, "ED25519-V3:deadbeefdeadbeef") self.assertFalse(ep.single_hop) def test_parse_via_plugin_single_hop_bogus(self, ftb): with self.assertRaises(ValueError): serverFromString( self.reactor, 'onion:88:singleHop=yes_please', ) def test_parse_via_plugin_key_and_keyfile(self, ftb): with self.assertRaises(ValueError): serverFromString( self.reactor, 'onion:88:privateKeyFile=foo:privateKey=blarg' ) def test_parse_via_plugin_key_and_dir(self, ftb): with self.assertRaises(ValueError): serverFromString( self.reactor, 'onion:88:localPort=1234:hiddenServiceDir=/foo/bar:privateKey=blarg' ) def test_parse_illegal_version_foo(self, ftb): with self.assertRaises(ValueError) as ctx: serverFromString( self.reactor, 'onion:88:version=foo:localPort=1234:hiddenServiceDir=~/blam/blarg' ) self.assertIn( "version must be an int", str(ctx.exception), ) def test_parse_illegal_version_1(self, ftb): with self.assertRaises(ValueError) as ctx: serverFromString( self.reactor, 'onion:88:version=1:localPort=1234:hiddenServiceDir=~/blam/blarg' ) self.assertIn( "Invalid version '1'", str(ctx.exception), ) def test_parse_version_3(self, ftb): ep = serverFromString( self.reactor, 'onion:88:version=3:localPort=1234:hiddenServiceDir=~/blam/blarg' ) self.assertFalse(ep.ephemeral) 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, progress_updates=None: 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, progress_updates=None: 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) def test_illegal_arg_ephemeral_auth(self, ftb): # XXX I think Tor does actually support this now? reactor = Mock() config = Mock() with self.assertRaises(ValueError) as ctx: TCPHiddenServiceEndpoint( reactor, config, 80, stealth_auth=['alice', 'bob'], ephemeral=True, ) self.assertIn( "onion services don't support 'stealth' auth", str(ctx.exception), ) def test_illegal_arg_ephemeral_hsdir(self, ftb): reactor = Mock() config = Mock() with self.assertRaises(ValueError) as ctx: TCPHiddenServiceEndpoint( reactor, config, 80, ephemeral=True, hidden_service_dir='/tmp/foo', ) self.assertIn( "Specifying 'hidden_service_dir' is incompatible", str(ctx.exception), ) def test_illegal_arg_disk_privkey(self, ftb): reactor = Mock() config = Mock() with self.assertRaises(ValueError) as ctx: TCPHiddenServiceEndpoint( reactor, config, 80, ephemeral=False, private_key=b'something', ) self.assertIn( "only understood for ephemeral services", str(ctx.exception), ) @defer.inlineCallbacks def test_illegal_arg_torconfig(self, ftb): class Foo(object): pass config = Foo() ep = TCPHiddenServiceEndpoint(self.reactor, config, 123) factory = Mock() with self.assertRaises(ValueError) as ctx: yield ep.listen(factory) self.assertIn( "Expected a TorConfig instance but", str(ctx.exception) ) @skipIf('pypy' in sys.version.lower(), "Weird OpenSSL+PyPy problem on Travis") @defer.inlineCallbacks def test_basic_auth_ephemeral(self, ftb): ''' ''' ep = TCPHiddenServiceEndpoint( self.reactor, self.config, 123, ephemeral=True, auth=AuthBasic(['alice', 'bob']), private_key=_test_private_key_blob, ) # make sure listen() correctly configures our hidden-serivce # with the explicit directory we passed in above d = ep.listen(NoOpProtocolFactory()) self.assertEqual(1, len(self.protocol.commands)) cmd, cmd_d = self.protocol.commands[0] self.assertTrue( cmd.startswith(u"ADD_ONION RSA1024:{} ".format(_test_private_key_blob)) ) cmd_d.callback("ServiceID={}\nPrivateKey={}\nClientAuth=bob:asdf\nClientAuth=alice:fdsa\n".format(_test_onion_id, _test_private_key_blob)) self.protocol.events['HS_DESC']( "UPLOAD {} basic somedirauth REASON=testing".format(_test_onion_id) ) self.protocol.events['HS_DESC']( "UPLOADED {} basic somedirauth REASON=testing".format(_test_onion_id) ) yield d # returns 'port' self.assertEqual(1, len(self.config.EphemeralOnionServices)) service = self.config.EphemeralOnionServices[0] self.assertTrue(IAuthenticatedOnionClients.providedBy(service)) self.assertEqual( set(["alice", "bob"]), set(service.client_names()), ) self.assertEqual( "asdf", service.get_client("bob").auth_token, ) self.assertEqual( "fdsa", service.get_client("alice").auth_token, ) @defer.inlineCallbacks def test_basic_ephemeral_v3(self, ftb): ''' ''' ep = TCPHiddenServiceEndpoint( self.reactor, self.config, 123, ephemeral=True, version=3, private_key='f' * 32, ) # make sure listen() correctly configures our hidden-serivce # with the explicit directory we passed in above d = ep.listen(NoOpProtocolFactory()) self.assertEqual(1, len(self.protocol.commands)) cmd, cmd_d = self.protocol.commands[0] self.assertTrue( cmd.startswith(u"ADD_ONION ED25519-V3:ffffffffffffffffffffffffffffffff ") ) cmd_d.callback("ServiceID=service\nPrivateKey=deadbeef") self.protocol.events['HS_DESC']( "UPLOAD service basic somedirauth REASON=testing" ) self.protocol.events['HS_DESC']( "UPLOADED service basic somedirauth REASON=testing" ) yield d # returns 'port' self.assertEqual(1, len(self.config.EphemeralOnionServices)) service = self.config.EphemeralOnionServices[0] self.assertTrue(IOnionService.providedBy(service)) @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. ''' tmp = self.mktemp() os.mkdir(tmp) with open(os.path.join(tmp, 'hostname'), 'w') as f: f.write('public0.onion token0 # client: alice\n') f.write('public1.onion token1 # client: bob\n') with open(os.path.join(tmp, 'private_key'), 'w') as f: f.write(_test_private_key) ep = TCPHiddenServiceEndpoint( self.reactor, self.config, 123, tmp, 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): return fail d.addErrback(foo) self.protocol.events['HS_DESC']( "UPLOAD {} basic somedirauth REASON=testing".format(_test_onion_id) ) self.protocol.events['HS_DESC']( "UPLOADED {} basic somedirauth REASON=testing".format(_test_onion_id) ) port = yield d # returns 'port' self.assertEqual(1, len(self.config.HiddenServices)) hs = self.config.HiddenServices[0] # hs will be IAuthenticatedOnionService self.assertEqual(2, len(hs.client_names())) self.assertIn("alice", hs.client_names()) self.assertIn("bob", hs.client_names()) alice = hs.get_client("alice") self.assertEqual(alice.hidden_service_directory, os.path.abspath(tmp)) self.assertEqual("token0", alice.auth_token) with self.assertRaises(ValueError): ep.onion_uri hs = port.onion_service self.assertEqual('public0.onion', hs.get_client("alice").hostname) self.assertEqual('public1.onion', hs.get_client("bob").hostname) def test_stealth_auth_deprecated(self, ftb): ''' make sure we produce a HiddenService instance with stealth-auth lines if we had authentication specified in the first place. ''' tmp = self.mktemp() os.mkdir(tmp) with open(os.path.join(tmp, 'hostname'), 'w') as f: f.write('public.onion\n') with self.assertRaises(ValueError) as ctx: TCPHiddenServiceEndpoint( self.reactor, self.config, 123, tmp, stealth_auth=['alice', 'bob'], auth=AuthStealth(['alice', 'bob']), ) self.assertIn( "use auth= only for new code", str(ctx.exception), ) @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): hs = Mock() hs.hostname = "foo.onion" addr = TorOnionAddress(80, hs) # 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() ports_attempted = [] class FakeSocks5(object): def __init__(self, tor_ep, *args, **kw): self.tor_port = tor_ep._port def connect(self, *args, **kw): ports_attempted.append(self.tor_port) if self.tor_port != 9150: return Failure(error.ConnectError("foo")) else: 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) self.assertEqual( ports_attempted, [9050, 9150] ) # now, if we re-use the endpoint, we should again attempt the # two ports p3 = yield endpoint.connect(None) self.assertTrue(proto is p3) self.assertEqual( ports_attempted, [9050, 9150, 9050, 9150] ) @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() directlyProvides(reactor, IReactorCore) 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 = FakeReactorTcp(self) 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_unix_socket_bad(self): reactor = Mock() cp = Mock() cp.get_conf = Mock( return_value=defer.succeed({ 'SocksPort': ['unix:bad worse wosrt'] }) ) the_error = Exception("a bad thing") def boom(*args, **kw): raise the_error with patch('txtorcon.endpoints.available_tcp_port', lambda r: 1234): with patch('txtorcon.torconfig.UNIXClientEndpoint', boom): yield _create_socks_endpoint(reactor, cp) errs = self.flushLoggedErrors() self.assertEqual(errs[0].value, the_error) @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-19.0.0/test/test_socks.py0000644000175000017500000007177313245162674017120 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.internet.interfaces import IStreamClientEndpoint from twisted.test import proto_helpers from twisted.test.iosim import connect, FakeTransport from zope.interface import directlyProvides 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() directlyProvides(socks_ep, IStreamClientEndpoint) 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_deferred_proxy_wrong_return(self): class NotAnEndpoint(object): "definitely doesn't implement IStreamClientEndpoint" not_an_endpoint = NotAnEndpoint() factory = Mock() ep = socks.TorSocksEndpoint( socks_endpoint=defer.succeed(not_an_endpoint), host=u'meejah.ca', port=443, ) with self.assertRaises(ValueError) as ctx: yield ep.connect(factory) self.assertIn( "must resolve to an IStreamClientEndpoint provider", str(ctx.exception), ) @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_tls_context(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) from OpenSSL import SSL class CertificateOptions(object): def getContext(self, *args, **kw): return SSL.Context(SSL.TLSv1_METHOD) ep = socks.TorSocksEndpoint(socks_ep, u'meejah.ca', 443, tls=CertificateOptions()) 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-19.0.0/test/test_torcontrolprotocol.py0000644000175000017500000012362113403761212021740 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_when_disconnect(self): """ see that we get our callback for when_disconnected if the transport goes away """ def it_was_called(arg): it_was_called.yes = True return None it_was_called.yes = False d = self.protocol.when_disconnected() d.addCallback(it_was_called) f = failure.Failure(error.ConnectionDone("It's all over")) self.protocol.connectionLost(f) self.assertTrue(it_was_called.yes) def test_when_disconnect_error(self): """ see that we get our errback for when_disconnected if the transport goes away """ def it_was_called(arg): it_was_called.yes = True return None it_was_called.yes = False d = self.protocol.when_disconnected() d.addErrback(it_was_called) f = failure.Failure(RuntimeError("sadness")) 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) def test_disconnect_outstanding_commands(self): """ outstanding commands should errback on disconnect """ def it_was_called(f): str(f) it_was_called.count += 1 return None it_was_called.count = 0 # we want to make sure outstanding commands get errbacks d0 = self.protocol.queue_command("some command0") d1 = self.protocol.queue_command("some command1") d0.addErrback(it_was_called) d1.addErrback(it_was_called) self.protocol.on_disconnect.addErrback(lambda _: None) f = failure.Failure(RuntimeError("The thing didn't do the stuff.")) self.protocol.connectionLost(f) self.assertEqual(it_was_called.count, 2) 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 test_getconf_single(self): d = self.protocol.get_conf_single("SOCKSPORT") d.addCallback(CallbackChecker('9050')) self.send(b"250 SocksPort=9050") 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_single(self): d = self.protocol.get_info_single("version") d.addCallback(CallbackChecker('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 Exception: 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-19.0.0/test/test_addrmap.py0000644000175000017500000001611613100171552017355 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-19.0.0/test/test_util_imports.py0000644000175000017500000000262513106645477020521 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-19.0.0/test/test_stream.py0000644000175000017500000003551713106645477017270 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-19.0.0/test/__init__.py0000644000175000017500000000000012777316067016453 0ustar mikemike00000000000000txtorcon-19.0.0/test/verify-release.py0000644000175000017500000000350513076556022017641 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 <