[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