[Pkg-privacy-commits] [Git][pkg-privacy-team/txtorcon][upstream] New upstream version 18.3.0
Iain Learmonth
irl at debian.org
Mon Jan 7 12:11:05 GMT 2019
Iain Learmonth pushed to branch upstream at Privacy Maintainers / txtorcon
Commits:
323a8649 by Iain R. Learmonth at 2019-01-07T12:03:46Z
New upstream version 18.3.0
- - - - -
22 changed files:
- Makefile
- PKG-INFO
- docs/guide.rst
- docs/index.rst
- docs/release-checklist.rst
- docs/releases.rst
- + examples/web_onion_service_ephemeral_keyfile.py
- + examples/web_onion_service_ephemeral_nonanon.py
- test/test_endpoints.py
- test/test_onion.py
- test/test_torstate.py
- txtorcon.egg-info/PKG-INFO
- txtorcon.egg-info/SOURCES.txt
- − txtorcon.egg-info/pbr.json
- txtorcon/_metadata.py
- txtorcon/circuit.py
- txtorcon/controller.py
- txtorcon/endpoints.py
- txtorcon/onion.py
- txtorcon/socks.py
- txtorcon/torstate.py
- txtorcon/web.py
Changes:
=====================================
Makefile
=====================================
@@ -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-any.whl:
python setup.py bdist_wheel --universal
dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc: dist/txtorcon-${VERSION}-py2.py3-none-any.whl
- gpg --verify dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc || gpg --no-version --detach-sign --armor --local-user meejah at 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 at 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 at 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 at 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
=====================================
PKG-INFO
=====================================
@@ -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.
=====================================
docs/guide.rst
=====================================
@@ -103,9 +103,10 @@ will fire with a :class:`.Tor` instance. If you need access to the
: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
=====================================
docs/index.rst
=====================================
@@ -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
=====================================
docs/release-checklist.rst
=====================================
@@ -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
=====================================
docs/releases.rst
=====================================
@@ -21,6 +21,41 @@ unreleased
`git master <https://github.com/meejah/txtorcon>`_ *will likely become v19.0.0*
+v18.3.0
+-------
+
+ * `txtorcon-18.3.0.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-18.3.0.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/18.3.0>`_ (:download:`local-sig </../signatues/txtorcon-18.3.0.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-18.3.0.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v18.3.0.tar.gz>`_)
+ * add `singleHop={true,false}` for endpoint-strings as well
+
+
+v18.2.0
+-------
+
+ * `txtorcon-18.2.0.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-18.2.0.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/18.2.0>`_ (:download:`local-sig </../signatues/txtorcon-18.2.0.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-18.2.0.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v18.2.0.tar.gz>`_)
+ * 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 <http://timaq4ygg2iegci7.onion/txtorcon-18.1.0.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/18.1.0>`_ (:download:`local-sig </../signatues/txtorcon-18.1.0.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-18.1.0.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v18.1.0.tar.gz>`_)
+ * better error-reporting (include REASON and REMOTE_REASON if
+ available) when circuit-builds fail (thanks `David Stainton
+ <https://github.com/david415>`_)
+ * more-robust detection of "do we have Python3" (thanks `Balint
+ Reczey <https://github.com/rbalint>`_)
+ * 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
-------
=====================================
examples/web_onion_service_ephemeral_keyfile.py
=====================================
@@ -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"<html>Hello, world! I'm a single-hop hidden service!</html>"
+
+
+ at 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)
=====================================
examples/web_onion_service_ephemeral_nonanon.py
=====================================
@@ -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"<html>Hello, world! I'm a single-hop onion service!</html>"
+
+
+ at 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)
=====================================
test/test_endpoints.py
=====================================
@@ -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(
@@ -1681,6 +1860,26 @@ class TestSocksFactory(unittest.TestCase):
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()
=====================================
test/test_onion.py
=====================================
@@ -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([])
=====================================
test/test_torstate.py
=====================================
@@ -28,8 +28,8 @@ from txtorcon.interface import ICircuitListener
from txtorcon.interface import IStreamListener
from txtorcon.interface import StreamListenerMixin
from txtorcon.interface import CircuitListenerMixin
-from txtorcon.torstate import _extract_reason
from txtorcon.circuit import _get_circuit_attacher
+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
=====================================
txtorcon.egg-info/PKG-INFO
=====================================
@@ -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.
=====================================
txtorcon.egg-info/SOURCES.txt
=====================================
@@ -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
=====================================
txtorcon.egg-info/pbr.json deleted
=====================================
@@ -1 +0,0 @@
-{"is_release": false, "git_version": "0f966c2"}
\ No newline at end of file
=====================================
txtorcon/_metadata.py
=====================================
@@ -1,4 +1,4 @@
-__version__ = '18.0.2'
+__version__ = '18.3.0'
__author__ = 'meejah'
__contact__ = 'meejah at meejah.ca'
__url__ = 'https://github.com/meejah/txtorcon'
=====================================
txtorcon/circuit.py
=====================================
@@ -23,6 +23,24 @@ from txtorcon.util import find_keywords, maybe_ip_addr, SingleObserver
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, reactor, path, timeout, using_guards=False)
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
=====================================
txtorcon/controller.py
=====================================
@@ -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)
=====================================
txtorcon/endpoints.py
=====================================
@@ -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 IStreamClientEndpointStringParserWithRea
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(object):
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(object):
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(object):
local_port=localPort,
ephemeral=ephemeral,
version=version,
+ private_key=privateKey,
+ single_hop=singleHop,
)
return TCPHiddenServiceEndpoint.global_tor(
@@ -888,6 +966,8 @@ class TCPHiddenServiceEndpointParser(object):
control_port=controlPort,
ephemeral=ephemeral,
version=version,
+ private_key=privateKey,
+ single_hop=singleHop,
)
@@ -922,7 +1002,7 @@ def _create_socks_endpoint(reactor, control_protocol, socks_config=None):
# 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, control_protocol, socks_config=None):
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
=====================================
txtorcon/onion.py
=====================================
@@ -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, progress, version, auth=None, await_al
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(object):
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(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).
"""
@@ -721,13 +737,14 @@ class EphemeralAuthenticatedOnionService(object):
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(object):
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 FilesystemAuthenticatedOnionService(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).
"""
# 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)
)
=====================================
txtorcon/socks.py
=====================================
@@ -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
=====================================
txtorcon/torstate.py
=====================================
@@ -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,
+ )
+ )
=====================================
txtorcon/web.py
=====================================
@@ -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'),
View it on GitLab: https://salsa.debian.org/pkg-privacy-team/txtorcon/commit/323a86498bad64bdfe0ee7e01301dc49f748e4dc
--
View it on GitLab: https://salsa.debian.org/pkg-privacy-team/txtorcon/commit/323a86498bad64bdfe0ee7e01301dc49f748e4dc
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-privacy-commits/attachments/20190107/32f5af00/attachment-0001.html>
More information about the Pkg-privacy-commits
mailing list