diff -pruN 18.0.2-2/debian/changelog 18.3.0-1/debian/changelog --- 18.0.2-2/debian/changelog 2018-08-16 11:50:11.000000000 +0000 +++ 18.3.0-1/debian/changelog 2019-01-07 12:09:47.000000000 +0000 @@ -1,3 +1,10 @@ +txtorcon (18.3.0-1) unstable; urgency=medium + + * New upstream version 18.3.0 + * Refreshed patches + + -- Iain R. Learmonth Mon, 07 Jan 2019 12:09:47 +0000 + txtorcon (18.0.2-2) unstable; urgency=medium * No longer builds a Python 2 package (Closes: #905253) diff -pruN 18.0.2-2/debian/patches/remove-privacy-infringing-buttons.patch 18.3.0-1/debian/patches/remove-privacy-infringing-buttons.patch --- 18.0.2-2/debian/patches/remove-privacy-infringing-buttons.patch 2018-07-31 22:28:43.000000000 +0000 +++ 18.3.0-1/debian/patches/remove-privacy-infringing-buttons.patch 2019-01-07 12:05:02.000000000 +0000 @@ -72,8 +72,8 @@ consults local documentation, which shou - .. image:: https://coveralls.io/repos/meejah/txtorcon/badge.svg - :target: https://coveralls.io/r/meejah/txtorcon - -- .. image:: http://codecov.io/github/meejah/txtorcon/coverage.svg?branch=master -- :target: http://codecov.io/github/meejah/txtorcon?branch=master +- .. image:: https://codecov.io/gh/meejah/txtorcon/branch/master/graphs/badge.svg?branch=master +- :target: https://codecov.io/github/meejah/txtorcon?branch=master - - .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=stable - :target: https://txtorcon.readthedocs.io/en/stable diff -pruN 18.0.2-2/docs/guide.rst 18.3.0-1/docs/guide.rst --- 18.0.2-2/docs/guide.rst 2018-04-29 18:42:10.000000000 +0000 +++ 18.3.0-1/docs/guide.rst 2018-09-29 15:29:49.000000000 +0000 @@ -103,9 +103,10 @@ will fire with a :class:`.Tor` instance. :class:`.TorControlProtocol` instance, it's available via the ``.protocol`` property (there is always exactly one of these per :class:`.Tor` instance). Similarly, the current configuration is -available via ``.config``. You can change the configuration by -updating attributes on this class but it won't take effect until you -call :meth:`.TorConfig.save`. +available via ``.get_config`` (which returns a Deferred firing a +:class:`.TorConfig`). You can change the configuration by updating +attributes on this class but it won't take effect until you call +:meth:`.TorConfig.save`. Launching a New Tor diff -pruN 18.0.2-2/docs/index.rst 18.3.0-1/docs/index.rst --- 18.0.2-2/docs/index.rst 2018-05-30 06:26:50.000000000 +0000 +++ 18.3.0-1/docs/index.rst 2018-09-27 02:36:38.000000000 +0000 @@ -16,8 +16,8 @@ txtorcon .. image:: https://coveralls.io/repos/meejah/txtorcon/badge.svg :target: https://coveralls.io/r/meejah/txtorcon - .. image:: http://codecov.io/github/meejah/txtorcon/coverage.svg?branch=master - :target: http://codecov.io/github/meejah/txtorcon?branch=master + .. image:: https://codecov.io/gh/meejah/txtorcon/branch/master/graphs/badge.svg?branch=master + :target: https://codecov.io/github/meejah/txtorcon?branch=master .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=stable :target: https://txtorcon.readthedocs.io/en/stable diff -pruN 18.0.2-2/docs/release-checklist.rst 18.3.0-1/docs/release-checklist.rst --- 18.0.2-2/docs/release-checklist.rst 2018-06-26 03:43:24.000000000 +0000 +++ 18.3.0-1/docs/release-checklist.rst 2018-10-05 00:27:55.000000000 +0000 @@ -10,7 +10,7 @@ Release Checklist * txtorcon/_metadata.py * run all tests, on all configurations - * "tox" + * "detox" * ensure long_description will render properly: * python setup.py check -r -s diff -pruN 18.0.2-2/docs/releases.rst 18.3.0-1/docs/releases.rst --- 18.0.2-2/docs/releases.rst 2018-07-02 18:36:27.000000000 +0000 +++ 18.3.0-1/docs/releases.rst 2018-10-05 23:02:38.000000000 +0000 @@ -21,6 +21,41 @@ unreleased `git master `_ *will likely become v19.0.0* +v18.3.0 +------- + + * `txtorcon-18.3.0.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) + * add `singleHop={true,false}` for endpoint-strings as well + + +v18.2.0 +------- + + * `txtorcon-18.2.0.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) + * add `privateKeyFile=` option to endpoint parser (ticket 313) + * use `privateKey=` option properly in endpoint parser + * support `NonAnonymous` mode for `ADD_ONION` via `single_hop=` kwarg + + +v18.1.0 +------- + +September 26, 2018 + + * `txtorcon-18.1.0.tar.gz `_ (`PyPI `_ (:download:`local-sig ` or `github-sig `_) (`source `_) + * better error-reporting (include REASON and REMOTE_REASON if + available) when circuit-builds fail (thanks `David Stainton + `_) + * more-robust detection of "do we have Python3" (thanks `Balint + Reczey `_) + * fix parsing of Unix-sockets for SOCKS + * better handling of concurrent Web agent requests before SOCKS ports + are known + * allow fowarding to ip:port pairs for Onion services when using the + "list of 2-tuples" method of specifying the remote vs local + connections. + + v18.0.2 ------- diff -pruN 18.0.2-2/examples/web_onion_service_ephemeral_keyfile.py 18.3.0-1/examples/web_onion_service_ephemeral_keyfile.py --- 18.0.2-2/examples/web_onion_service_ephemeral_keyfile.py 1970-01-01 00:00:00.000000000 +0000 +++ 18.3.0-1/examples/web_onion_service_ephemeral_keyfile.py 2018-10-02 21:19:23.000000000 +0000 @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +# This shows how to leverage the endpoints API to get a new hidden +# service up and running quickly. You can pass along this API to your +# users by accepting endpoint strings as per Twisted recommendations. +# +# http://twistedmatrix.com/documents/current/core/howto/endpoints.html#maximizing-the-return-on-your-endpoint-investment +# +# note that only the progress-updates needs the "import txtorcon" -- +# you do still need it installed so that Twisted finds the endpoint +# parser plugin but code without knowledge of txtorcon can still +# launch a Tor instance using it. cool! + +from __future__ import print_function +from twisted.internet import defer, task, endpoints +from twisted.web import server, resource + +import txtorcon +from txtorcon.util import default_control_port +from txtorcon.onion import AuthBasic + + +class Simple(resource.Resource): + """ + A really simple Web site. + """ + isLeaf = True + + def render_GET(self, request): + return b"Hello, world! I'm a single-hop hidden service!" + + +@defer.inlineCallbacks +def main(reactor): + tor = yield txtorcon.connect( + reactor, + endpoints.TCP4ClientEndpoint(reactor, "localhost", 9251), + ) + ep = endpoints.serverFromString( + reactor, + "onion:80:version=3:privateKeyFile=/home/mike/src/txtorcon/foodir/hs_ed25519_secret_key" + ) + + def on_progress(percent, tag, msg): + print('%03d: %s' % (percent, msg)) + txtorcon.IProgressProvider(ep).add_progress_listener(on_progress) + print("Note: descriptor upload can take several minutes") + + port = yield ep.listen(server.Site(Simple())) + print("Private key:\n{}".format(port.getHost().onion_key)) + hs = port.onion_service + print("hs {}".format(hs)) + print("{}".format(hs.hostname)) + yield defer.Deferred() # wait forever + + +task.react(main) diff -pruN 18.0.2-2/examples/web_onion_service_ephemeral_nonanon.py 18.3.0-1/examples/web_onion_service_ephemeral_nonanon.py --- 18.0.2-2/examples/web_onion_service_ephemeral_nonanon.py 1970-01-01 00:00:00.000000000 +0000 +++ 18.3.0-1/examples/web_onion_service_ephemeral_nonanon.py 2018-10-05 23:02:12.000000000 +0000 @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +# Here we use some very new Tor configuration options to set up a +# "single-hop" or "non-anonymous" onion service. These do NOT give the +# server location-privacy, so may be appropriate for certain kinds of +# services. Once you publish a service like this, there's no going +# back to location-hidden. + +from __future__ import print_function +from twisted.internet import defer, task, endpoints +from twisted.web import server, resource + +import txtorcon +from txtorcon.util import default_control_port +from txtorcon.onion import AuthBasic + + +class Simple(resource.Resource): + """ + A really simple Web site. + """ + isLeaf = True + + def render_GET(self, request): + return b"Hello, world! I'm a single-hop onion service!" + + +@defer.inlineCallbacks +def main(reactor): + # For the "single_hop=True" below to work, the Tor we're + # connecting to must have the following options set: + # SocksPort 0 + # HiddenServiceSingleHopMode 1 + # HiddenServiceNonAnonymousMode 1 + + tor = yield txtorcon.connect( + reactor, + endpoints.TCP4ClientEndpoint(reactor, "localhost", 9351), + ) + if False: + ep = tor.create_onion_endpoint( + 80, + version=3, + single_hop=True, + ) + else: + ep = endpoints.serverFromString(reactor, "onion:80:version=3:singleHop=true") + + def on_progress(percent, tag, msg): + print('%03d: %s' % (percent, msg)) + txtorcon.IProgressProvider(ep).add_progress_listener(on_progress) + + port = yield ep.listen(server.Site(Simple())) + print("Private key:\n{}".format(port.getHost().onion_key)) + hs = port.onion_service + print("Site on http://{}".format(hs.hostname)) + yield defer.Deferred() # wait forever + + +task.react(main) diff -pruN 18.0.2-2/Makefile 18.3.0-1/Makefile --- 18.0.2-2/Makefile 2018-07-02 18:20:05.000000000 +0000 +++ 18.3.0-1/Makefile 2018-10-05 23:02:53.000000000 +0000 @@ -1,6 +1,6 @@ .PHONY: test html counts coverage sdist clean install doc integration diagrams default: test -VERSION = 18.0.2 +VERSION = 18.3.0 test: PYTHONPATH=. trial --reporter=text test @@ -112,11 +112,11 @@ dist/txtorcon-${VERSION}-py2.py3-none-an python setup.py bdist_wheel --universal dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc: dist/txtorcon-${VERSION}-py2.py3-none-any.whl - gpg --verify dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc || gpg --no-version --detach-sign --armor --local-user meejah@meejah.ca dist/txtorcon-${VERSION}-py2.py3-none-any.whl + gpg --verify dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc || gpg --pinentry loopback --no-version --detach-sign --armor --local-user meejah@meejah.ca dist/txtorcon-${VERSION}-py2.py3-none-any.whl dist/txtorcon-${VERSION}.tar.gz: sdist dist/txtorcon-${VERSION}.tar.gz.asc: dist/txtorcon-${VERSION}.tar.gz - gpg --verify dist/txtorcon-${VERSION}.tar.gz.asc || gpg --no-version --detach-sign --armor --local-user meejah@meejah.ca dist/txtorcon-${VERSION}.tar.gz + gpg --verify dist/txtorcon-${VERSION}.tar.gz.asc || gpg --pinentry loopback --no-version --detach-sign --armor --local-user meejah@meejah.ca dist/txtorcon-${VERSION}.tar.gz release: twine upload -r pypi -c "txtorcon v${VERSION} tarball" dist/txtorcon-${VERSION}.tar.gz dist/txtorcon-${VERSION}.tar.gz.asc diff -pruN 18.0.2-2/PKG-INFO 18.3.0-1/PKG-INFO --- 18.0.2-2/PKG-INFO 2018-07-02 18:37:44.000000000 +0000 +++ 18.3.0-1/PKG-INFO 2018-10-05 23:05:41.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: txtorcon -Version: 18.0.2 +Version: 18.3.0 Summary: Twisted-based Tor controller client, with state-tracking and configuration abstractions. diff -pruN 18.0.2-2/test/test_endpoints.py 18.3.0-1/test/test_endpoints.py --- 18.0.2-2/test/test_endpoints.py 2018-05-12 06:50:09.000000000 +0000 +++ 18.3.0-1/test/test_endpoints.py 2018-10-05 23:02:12.000000000 +0000 @@ -5,6 +5,7 @@ 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 @@ -547,6 +548,18 @@ class EndpointTests(unittest.TestCase): 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) @@ -704,6 +717,172 @@ class EndpointTests(unittest.TestCase): 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( @@ -1682,6 +1861,26 @@ class TestSocksFactory(unittest.TestCase 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() diff -pruN 18.0.2-2/test/test_onion.py 18.3.0-1/test/test_onion.py --- 18.0.2-2/test/test_onion.py 2018-05-12 06:50:09.000000000 +0000 +++ 18.3.0-1/test/test_onion.py 2018-10-04 23:03:21.000000000 +0000 @@ -264,6 +264,56 @@ class OnionServiceTest(unittest.TestCase 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([]) diff -pruN 18.0.2-2/test/test_torstate.py 18.3.0-1/test/test_torstate.py --- 18.0.2-2/test/test_torstate.py 2018-07-02 17:57:02.000000000 +0000 +++ 18.3.0-1/test/test_torstate.py 2018-07-28 07:05:52.000000000 +0000 @@ -28,8 +28,8 @@ from txtorcon.interface import ICircuitL from txtorcon.interface import IStreamListener from txtorcon.interface import StreamListenerMixin from txtorcon.interface import CircuitListenerMixin -from txtorcon.torstate import _extract_reason from txtorcon.circuit import _get_circuit_attacher +from txtorcon.circuit import _extract_reason try: from .py3_torstate import TorStatePy3Tests # noqa @@ -1372,6 +1372,52 @@ s Fast Guard Running Stable Valid 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): + print("DING {} {}".format(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) + print("DING {}".format(self.state)) + 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 @@ -1434,3 +1480,32 @@ s Fast Guard Running Stable Valid # 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 diff -pruN 18.0.2-2/txtorcon/circuit.py 18.3.0-1/txtorcon/circuit.py --- 18.0.2-2/txtorcon/circuit.py 2018-05-07 01:06:50.000000000 +0000 +++ 18.3.0-1/txtorcon/circuit.py 2018-09-27 01:34:37.000000000 +0000 @@ -23,6 +23,24 @@ from txtorcon.util import find_keywords, TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' +def _extract_reason(kw): + """ + Internal helper. Extracts a reason (possibly both reasons!) from + the kwargs for a circuit failed or closed event. + """ + try: + # we "often" have a REASON + reason = kw['REASON'] + try: + # ...and sometimes even have a REMOTE_REASON + reason = '{}, {}'.format(reason, kw['REMOTE_REASON']) + except KeyError: + pass # should still be the 'REASON' error if we had it + except KeyError: + reason = "unknown" + return reason + + @implementer(IStreamAttacher) class _CircuitAttacher(object): """ @@ -500,7 +518,7 @@ class Circuit(object): class CircuitBuildTimedOutError(Exception): - """ + """ This exception is thrown when using `timed_circuit_build` and the circuit build times-out. """ @@ -530,11 +548,12 @@ def build_timeout_circuit(tor_state, rea d2 = timed_circuit[0].close() else: d2 = defer.succeed(None) - d2.addCallback(lambda ign: Failure(CircuitBuildTimedOutError("circuit build timed out"))) + d2.addCallback(lambda _: Failure(CircuitBuildTimedOutError("circuit build timed out"))) return d2 d.addCallback(get_circuit) d.addCallback(lambda circ: circ.when_built()) d.addErrback(trap_cancel) + reactor.callLater(timeout, d.cancel) return d diff -pruN 18.0.2-2/txtorcon/controller.py 18.3.0-1/txtorcon/controller.py --- 18.0.2-2/txtorcon/controller.py 2018-07-02 17:49:38.000000000 +0000 +++ 18.3.0-1/txtorcon/controller.py 2018-10-05 18:07:56.000000000 +0000 @@ -42,7 +42,7 @@ from .interface import ITor try: from .controller_py3 import _AsyncOnionAuthContext HAVE_ASYNC = True -except SyntaxError: +except Exception: HAVE_ASYNC = False if sys.platform in ('linux', 'linux2', 'darwin'): @@ -755,7 +755,7 @@ class Tor(object): auth=auth, ) - def create_onion_endpoint(self, port, private_key=None, version=None): + def create_onion_endpoint(self, port, private_key=None, version=None, single_hop=None): """ WARNING: API subject to change @@ -778,6 +778,11 @@ class Tor(object): :param version: if not None, a specific version of service to use; version=3 is Proposition 224 and version=2 is the older 1024-bit key based implementation. + + :param single_hop: if True, pass the `NonAnonymous` flag. Note + that Tor options `HiddenServiceSingleHopMode`, + `HiddenServiceNonAnonymousMode` must be set to `1` and there + must be no `SOCKSPort` configured for this to actually work. """ # note, we're just depending on this being The Ultimate # Everything endpoint. Which seems fine, because "normal" @@ -791,6 +796,7 @@ class Tor(object): private_key=private_key, version=version, auth=None, + single_hop=single_hop, ) def create_filesystem_onion_endpoint(self, port, hs_dir, group_readable=False, version=None): @@ -939,7 +945,7 @@ class Tor(object): # method names are kind of long (not-ideal) @inlineCallbacks - def create_onion_service(self, ports, private_key=None, version=3, progress=None, await_all_uploads=False): + def create_onion_service(self, ports, private_key=None, version=3, progress=None, await_all_uploads=False, single_hop=None): """ Create a new Onion service @@ -975,6 +981,11 @@ class Tor(object): until at least one upload of our Descriptor to a Directory Authority has completed; if True we wait until all have completed. + + :param single_hop: if True, pass the `NonAnonymous` flag. Note + that Tor options `HiddenServiceSingleHopMode`, + `HiddenServiceNonAnonymousMode` must be set to `1` and there + must be no `SOCKSPort` configured for this to actually work. """ if version not in (2, 3): raise ValueError( @@ -993,6 +1004,7 @@ class Tor(object): version=version, progress=progress, await_all_uploads=await_all_uploads, + single_hop=single_hop, ) returnValue(service) diff -pruN 18.0.2-2/txtorcon/endpoints.py 18.3.0-1/txtorcon/endpoints.py --- 18.0.2-2/txtorcon/endpoints.py 2018-06-19 03:54:58.000000000 +0000 +++ 18.3.0-1/txtorcon/endpoints.py 2018-10-05 23:02:12.000000000 +0000 @@ -9,6 +9,7 @@ import shutil import weakref import tempfile import functools +from binascii import b2a_base64 from txtorcon.util import available_tcp_port from txtorcon.socks import TorSocksEndpoint @@ -17,6 +18,7 @@ from twisted.internet.interfaces import from twisted.internet import defer, error from twisted.python import log from twisted.python.deprecate import deprecated +from twisted.python.failure import Failure from twisted.internet.interfaces import IStreamServerEndpointStringParser from twisted.internet.interfaces import IStreamServerEndpoint from twisted.internet.interfaces import IStreamClientEndpoint @@ -222,7 +224,8 @@ class TCPHiddenServiceEndpoint(object): ephemeral=None, auth=None, private_key=None, - version=None): + version=None, + single_hop=None): """ This returns a TCPHiddenServiceEndpoint connected to the endpoint you specify in `control_endpoint`. After connecting, a @@ -248,6 +251,7 @@ class TCPHiddenServiceEndpoint(object): private_key=private_key, auth=auth, version=version, + single_hop=single_hop, ) @classmethod @@ -259,7 +263,8 @@ class TCPHiddenServiceEndpoint(object): auth=None, ephemeral=None, private_key=None, - version=None): + version=None, + single_hop=None): """ This returns a TCPHiddenServiceEndpoint connected to a txtorcon global Tor instance. The first time you call this, a @@ -306,6 +311,7 @@ class TCPHiddenServiceEndpoint(object): ephemeral=ephemeral, private_key=private_key, version=version, + single_hop=single_hop, ) progress.target = r._tor_progress_update return r @@ -318,7 +324,8 @@ class TCPHiddenServiceEndpoint(object): ephemeral=None, private_key=None, auth=None, - version=None): + version=None, + single_hop=None): """ This returns a TCPHiddenServiceEndpoint that's always connected to its own freshly-launched Tor instance. All @@ -344,6 +351,7 @@ class TCPHiddenServiceEndpoint(object): private_key=private_key, auth=auth, version=version, + single_hop=single_hop, ) progress.target = r._tor_progress_update return r @@ -356,7 +364,8 @@ class TCPHiddenServiceEndpoint(object): ephemeral=None, # will be set to True, unless hsdir spec'd private_key=None, group_readable=False, - version=None): + version=None, + single_hop=None): """ :param reactor: :api:`twisted.internet.interfaces.IReactorTCP` provider @@ -395,6 +404,11 @@ class TCPHiddenServiceEndpoint(object): :param version: Either None, 2 or 3 to specify a version 2 service or Proposition 224 (version 3) service. + + :param single_hop: if True, pass the `NonAnonymous` flag. Note + that Tor options `HiddenServiceSingleHopMode`, + `HiddenServiceNonAnonymousMode` must be set to `1` and there + must be no `SOCKSPort` configured for this to actually work. """ # this supports API backwards-compatibility -- if you didn't @@ -431,6 +445,11 @@ class TCPHiddenServiceEndpoint(object): "'private_key' only understood for ephemeral services" ) + if single_hop and not ephemeral: + raise ValueError( + "'single_hop=' flag only makes sense for ephemeral onions" + ) + self._reactor = reactor self._config = defer.maybeDeferred(lambda: config) self.public_port = public_port @@ -446,6 +465,7 @@ class TCPHiddenServiceEndpoint(object): self.hiddenservice = None self.group_readable = group_readable self.version = version + self.single_hop = single_hop self.retries = 0 if self.version is None: @@ -592,6 +612,7 @@ class TCPHiddenServiceEndpoint(object): progress=self._descriptor_progress_update, version=self.version, auth=self.auth, + single_hop=self.single_hop, ) else: @@ -603,6 +624,7 @@ class TCPHiddenServiceEndpoint(object): detach=False, progress=self._descriptor_progress_update, version=self.version, + single_hop=self.single_hop, ) else: if self.auth is not None: @@ -798,6 +820,35 @@ class TorOnionListeningPort(object): return self._service.directory +def _load_private_key_file(fname): + """ + Loads an onion-service private-key from the given file. This can + be either a 'key blog' as returned from a previous ADD_ONION call, + or a v3 or v2 file as created by Tor when using the + HiddenServiceDir directive. + + In any case, a key-blob suitable for ADD_ONION use is returned. + """ + with open(fname, "rb") as f: + data = f.read() + if b"\x00\x00\x00" in data: # v3 private key file + blob = data[data.find(b"\x00\x00\x00") + 3:] + return u"ED25519-V3:{}".format(b2a_base64(blob.strip()).decode('ascii').strip()) + if b"-----BEGIN RSA PRIVATE KEY-----" in data: # v2 RSA key + blob = "".join(data.decode('ascii').split('\n')[1:-2]) + return u"RSA1024:{}".format(blob) + blob = data.decode('ascii').strip() + if ':' in blob: + kind, key = blob.split(':', 1) + if kind in ['ED25519-V3', 'RSA1024']: + return blob + raise ValueError( + "'{}' does not appear to contain v2 or v3 private key data".format( + fname, + ) + ) + + @implementer(IStreamServerEndpointStringParser, IPlugin) class TCPHiddenServiceEndpointParser(object): """ @@ -821,6 +872,10 @@ class TCPHiddenServiceEndpointParser(obj If ``hiddenServiceDir`` is not specified, one is created with ``tempfile.mkdtemp()``. The IStreamServerEndpoint returned will be an instance of :class:`txtorcon.TCPHiddenServiceEndpoint` + + If ``privateKey`` or ``privateKeyFile`` is specified, the service + will be "ephemeral" and Tor will receive the private key via the + ADD_ONION control-port command. """ prefix = "onion" @@ -830,16 +885,37 @@ class TCPHiddenServiceEndpointParser(obj def parseStreamServer(self, reactor, public_port, localPort=None, controlPort=None, hiddenServiceDir=None, - privateKey=None, version=None): + privateKey=None, privateKeyFile=None, + version=None, singleHop=None): """ :api:`twisted.internet.interfaces.IStreamServerEndpointStringParser` """ + if privateKeyFile is not None: + if privateKey is not None: + raise ValueError( + "Can't specify both privateKey= and privateKeyFile=" + ) + privateKey = _load_private_key_file(privateKeyFile) + privateKeyFile = None + if hiddenServiceDir is not None and privateKey is not None: raise ValueError( - "Only one of hiddenServiceDir and privateKey accepted" + "Only one of hiddenServiceDir and privateKey/privateKeyFile accepted" ) + if singleHop is not None: + if singleHop.strip().lower() in ['0', 'false']: + singleHop = False + elif singleHop.strip().lower() in ['1', 'true']: + singleHop = True + else: + raise ValueError( + "singleHop= param must be 'true' or 'false'" + ) + else: + singleHop = False + if version is not None: try: version = int(version) @@ -879,6 +955,8 @@ class TCPHiddenServiceEndpointParser(obj local_port=localPort, ephemeral=ephemeral, version=version, + private_key=privateKey, + single_hop=singleHop, ) return TCPHiddenServiceEndpoint.global_tor( @@ -888,6 +966,8 @@ class TCPHiddenServiceEndpointParser(obj control_port=controlPort, ephemeral=ephemeral, version=version, + private_key=privateKey, + single_hop=singleHop, ) @@ -922,7 +1002,7 @@ def _create_socks_endpoint(reactor, cont # could check platform? but why would you have unix ports on a # platform that doesn't? - unix_ports = set([p.startswith('unix:') for p in socks_ports]) + unix_ports = set([p for p in socks_ports if p.startswith('unix:')]) tcp_ports = set(socks_ports) - unix_ports socks_endpoint = None @@ -932,7 +1012,10 @@ def _create_socks_endpoint(reactor, cont try: socks_endpoint = _endpoint_from_socksport_line(reactor, p) except Exception as e: - log.msg("clientFromString('{}') failed: {}".format(p, e)) + log.err( + Failure(), + "failed to process SOCKS port '{}': {}".format(p, e) + ) # if we still don't have an endpoint, nothing worked (or there # were no SOCKSPort lines at all) so we add config to tor diff -pruN 18.0.2-2/txtorcon/_metadata.py 18.3.0-1/txtorcon/_metadata.py --- 18.0.2-2/txtorcon/_metadata.py 2018-07-02 18:20:13.000000000 +0000 +++ 18.3.0-1/txtorcon/_metadata.py 2018-10-05 23:02:59.000000000 +0000 @@ -1,4 +1,4 @@ -__version__ = '18.0.2' +__version__ = '18.3.0' __author__ = 'meejah' __contact__ = 'meejah@meejah.ca' __url__ = 'https://github.com/meejah/txtorcon' diff -pruN 18.0.2-2/txtorcon/onion.py 18.3.0-1/txtorcon/onion.py --- 18.0.2-2/txtorcon/onion.py 2018-06-30 04:51:46.000000000 +0000 +++ 18.3.0-1/txtorcon/onion.py 2018-10-05 18:07:56.000000000 +0000 @@ -193,6 +193,10 @@ class FilesystemOnionService(object): :param progress: a callable taking (percent, tag, description) that is called periodically to report progress. + :param await_all_uploads: if True, the Deferred only fires + after ALL descriptor uploads have completed (otherwise, it + fires when at least one has completed). + See also :meth:`txtorcon.Tor.create_onion_service` (which ultimately calls this). """ @@ -576,6 +580,8 @@ def _add_ephemeral_service(config, onion assert isinstance(auth, AuthBasic) # don't support AuthStealth yet if isinstance(auth, AuthBasic): flags.append('BasicAuth') + if onion._single_hop: + flags.append('NonAnonymous') # depends on some Tor options, too if flags: cmd += ' Flags={}'.format(','.join(flags)) @@ -674,7 +680,8 @@ class EphemeralAuthenticatedOnionService version=None, progress=None, auth=None, - await_all_uploads=None): # AuthBasic, or AuthStealth instance + await_all_uploads=None, # AuthBasic, or AuthStealth instance + single_hop=False): """ returns a new EphemeralAuthenticatedOnionService after adding it @@ -698,6 +705,15 @@ class EphemeralAuthenticatedOnionService :param progress: a callable taking (percent, tag, description) that is called periodically to report progress. + :param await_all_uploads: if True, the Deferred only fires + after ALL descriptor uploads have completed (otherwise, it + fires when at least one has completed). + + :param single_hop: if True, pass the `NonAnonymous` flag. Note + that Tor options `HiddenServiceSingleHopMode`, + `HiddenServiceNonAnonymousMode` must be set to `1` and there + must be no `SOCKSPort` configured for this to actually work. + See also :meth:`txtorcon.Tor.create_onion_service` (which ultimately calls this). """ @@ -721,13 +737,14 @@ class EphemeralAuthenticatedOnionService private_key=private_key, detach=detach, version=version, + single_hop=single_hop, ) yield _add_ephemeral_service(config, onion, progress, version, auth, await_all_uploads) defer.returnValue(onion) def __init__(self, config, ports, hostname=None, private_key=None, auth=[], version=3, - detach=False): + detach=False, single_hop=None): """ Users should create instances of this class by using the async method :meth:`txtorcon.EphemeralAuthenticatedOnionService.create` @@ -742,6 +759,7 @@ class EphemeralAuthenticatedOnionService self._version = version self._detach = detach self._clients = dict() + self._single_hop = single_hop def get_permanent_id(self): """ @@ -824,7 +842,8 @@ class EphemeralOnionService(object): private_key=None, # or DISCARD version=None, progress=None, - await_all_uploads=None): + await_all_uploads=None, + single_hop=False): """ returns a new EphemeralOnionService after adding it to the provided config and ensuring at least one of its descriptors @@ -847,6 +866,15 @@ class EphemeralOnionService(object): :param progress: a callable taking (percent, tag, description) that is called periodically to report progress. + :param await_all_uploads: if True, the Deferred only fires + after ALL descriptor uploads have completed (otherwise, it + fires when at least one has completed). + + :param single_hop: if True, pass the `NonAnonymous` flag. Note + that Tor options `HiddenServiceSingleHopMode`, + `HiddenServiceNonAnonymousMode` must be set to `1` and there + must be no `SOCKSPort` configured for this to actually work. + See also :meth:`txtorcon.Tor.create_onion_service` (which ultimately calls this). """ @@ -862,6 +890,7 @@ class EphemeralOnionService(object): detach=detach, version=version, await_all_uploads=await_all_uploads, + single_hop=single_hop, ) yield _add_ephemeral_service(config, onion, progress, version, None, await_all_uploads) @@ -869,7 +898,7 @@ class EphemeralOnionService(object): defer.returnValue(onion) def __init__(self, config, ports, hostname=None, private_key=None, version=3, - detach=False, await_all_uploads=None, **kwarg): + detach=False, await_all_uploads=None, single_hop=None, **kwarg): """ Users should create instances of this class by using the async method :meth:`txtorcon.EphemeralOnionService.create` @@ -893,6 +922,7 @@ class EphemeralOnionService(object): self._private_key = private_key self._version = version self._detach = detach + self._single_hop = single_hop # not putting an "add_to_tor" method here; that class is now # deprecated and you add one of these by using .create() @@ -1069,6 +1099,10 @@ class FilesystemAuthenticatedOnionServic :param progress: a callable taking (percent, tag, description) that is called periodically to report progress. + + :param await_all_uploads: if True, the Deferred only fires + after ALL descriptor uploads have completed (otherwise, it + fires when at least one has completed). """ # if hsdir is relative, it's "least surprising" (IMO) to make # it into a relative path here -- otherwise, it's relative to @@ -1311,11 +1345,20 @@ def _validate_ports(reactor, ports): try: local = int(local) except ValueError: - if not local.startswith('unix:/'): - raise ValueError( - "local port must be either an integer" - " or start with unix:/" - ) + if local.startswith('unix:/'): + pass + else: + if ':' not in local: + raise ValueError( + "local port must be either an integer" + " or start with unix:/ or be an IP:port" + ) + ip, port = local.split(':') + if not _is_non_public_numeric_address(ip): + log.msg( + "'{}' used as onion port doesn't appear to be a " + "local, numeric address".format(ip) + ) processed_ports.append( "{} {}".format(remote, local) ) diff -pruN 18.0.2-2/txtorcon/socks.py 18.3.0-1/txtorcon/socks.py --- 18.0.2-2/txtorcon/socks.py 2018-04-29 18:42:10.000000000 +0000 +++ 18.3.0-1/txtorcon/socks.py 2018-09-27 01:34:37.000000000 +0000 @@ -741,7 +741,8 @@ class TorSocksEndpoint(object): if not IStreamClientEndpoint.providedBy(proxy_ep): raise ValueError( "The Deferred provided as 'socks_endpoint' must " - "resolve to an IStreamClientEndpoint provider" + "resolve to an IStreamClientEndpoint provider (got " + "{})".format(type(proxy_ep).__name__) ) else: proxy_ep = self._proxy_ep diff -pruN 18.0.2-2/txtorcon/torstate.py 18.3.0-1/txtorcon/torstate.py --- 18.0.2-2/txtorcon/torstate.py 2018-07-02 18:11:49.000000000 +0000 +++ 18.3.0-1/txtorcon/torstate.py 2018-09-27 01:34:37.000000000 +0000 @@ -19,7 +19,7 @@ from zope.interface import implementer from txtorcon.torcontrolprotocol import TorProtocolFactory from txtorcon.stream import Stream -from txtorcon.circuit import Circuit +from txtorcon.circuit import Circuit, _extract_reason from txtorcon.router import Router, hashFromHexId from txtorcon.addrmap import AddrMap from txtorcon.torcontrolprotocol import parse_keywords @@ -167,24 +167,6 @@ def flags_from_dict(kw): return flags -def _extract_reason(kw): - """ - Internal helper. Extracts a reason (possibly both reasons!) from - the kwargs for a circuit failed or closed event. - """ - try: - # we "often" have a REASON - reason = kw['REASON'] - try: - # ...and sometimes even have a REMOTE_REASON - reason = '{}, {}'.format(reason, kw['REMOTE_REASON']) - except KeyError: - pass # should still be the 'REASON' error if we had it - except KeyError: - reason = "unknown" - return reason - - @implementer(ICircuitListener) @implementer(ICircuitContainer) @implementer(IRouterContainer) @@ -941,7 +923,9 @@ class TorState(object): "ICircuitListener API" txtorlog.msg("circuit_closed", circuit) circuit._when_built.fire( - Failure(Exception("Circuit closed ('{}')".format(_extract_reason(kw)))) + Failure( + CircuitBuildClosedError(_extract_reason(kw)) + ) ) self.circuit_destroy(circuit) @@ -949,6 +933,34 @@ class TorState(object): "ICircuitListener API" txtorlog.msg("circuit_failed", circuit, str(kw)) circuit._when_built.fire( - Failure(Exception("Circuit failed ('{}')".format(_extract_reason(kw)))) + Failure( + CircuitBuildFailedError(_extract_reason(kw)) + ) ) self.circuit_destroy(circuit) + + +class CircuitBuildFailedError(Exception): + """ + This exception is thrown when a circuit we're building fails + """ + def __init__(self, reason): + self.reason = reason + super(CircuitBuildFailedError, self).__init__( + "Circuit failed: {}".format( + self.reason, + ) + ) + + +class CircuitBuildClosedError(Exception): + """ + This exception is thrown when a circuit we're building is closed + """ + def __init__(self, reason): + self.reason = reason + super(CircuitBuildClosedError, self).__init__( + "Circuit closed: {}".format( + self.reason, + ) + ) diff -pruN 18.0.2-2/txtorcon/web.py 18.3.0-1/txtorcon/web.py --- 18.0.2-2/txtorcon/web.py 2018-02-27 04:59:40.000000000 +0000 +++ 18.3.0-1/txtorcon/web.py 2018-09-27 01:34:37.000000000 +0000 @@ -13,25 +13,28 @@ from zope.interface import implementer from txtorcon.socks import TorSocksEndpoint from txtorcon.log import txtorlog +from txtorcon.util import SingleObserver @implementer(IAgentEndpointFactory) class _AgentEndpointFactoryUsingTor(object): def __init__(self, reactor, tor_socks_endpoint): self._reactor = reactor - self._proxy_ep = tor_socks_endpoint + self._proxy_ep = SingleObserver() # if _proxy_ep is Deferred, but we get called twice, we must # remember the resolved object here if isinstance(tor_socks_endpoint, Deferred): - self._proxy_ep.addCallback(self._set_proxy) + tor_socks_endpoint.addCallback(self._set_proxy) + else: + self._proxy_ep.fire(tor_socks_endpoint) def _set_proxy(self, p): - self._proxy_ep = p + self._proxy_ep.fire(p) return p def endpointForURI(self, uri): return TorSocksEndpoint( - self._proxy_ep, + self._proxy_ep.when_fired(), uri.host, uri.port, tls=(uri.scheme == b'https'), diff -pruN 18.0.2-2/txtorcon.egg-info/pbr.json 18.3.0-1/txtorcon.egg-info/pbr.json --- 18.0.2-2/txtorcon.egg-info/pbr.json 2016-06-13 06:36:09.000000000 +0000 +++ 18.3.0-1/txtorcon.egg-info/pbr.json 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -{"is_release": false, "git_version": "0f966c2"} \ No newline at end of file diff -pruN 18.0.2-2/txtorcon.egg-info/PKG-INFO 18.3.0-1/txtorcon.egg-info/PKG-INFO --- 18.0.2-2/txtorcon.egg-info/PKG-INFO 2018-07-02 18:37:44.000000000 +0000 +++ 18.3.0-1/txtorcon.egg-info/PKG-INFO 2018-10-05 23:05:41.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: txtorcon -Version: 18.0.2 +Version: 18.3.0 Summary: Twisted-based Tor controller client, with state-tracking and configuration abstractions. diff -pruN 18.0.2-2/txtorcon.egg-info/SOURCES.txt 18.3.0-1/txtorcon.egg-info/SOURCES.txt --- 18.0.2-2/txtorcon.egg-info/SOURCES.txt 2018-07-02 18:37:44.000000000 +0000 +++ 18.3.0-1/txtorcon.egg-info/SOURCES.txt 2018-10-05 23:05:41.000000000 +0000 @@ -76,6 +76,8 @@ examples/web_onion_service_aiohttp.py examples/web_onion_service_endpoints.py examples/web_onion_service_ephemeral.py examples/web_onion_service_ephemeral_auth.py +examples/web_onion_service_ephemeral_keyfile.py +examples/web_onion_service_ephemeral_nonanon.py examples/web_onion_service_ephemeral_unix.py examples/web_onion_service_filesystem.py examples/web_onion_service_prop224.py @@ -137,6 +139,5 @@ txtorcon/web.py txtorcon.egg-info/PKG-INFO txtorcon.egg-info/SOURCES.txt txtorcon.egg-info/dependency_links.txt -txtorcon.egg-info/pbr.json txtorcon.egg-info/requires.txt txtorcon.egg-info/top_level.txt \ No newline at end of file