diff --git a/docs/releases.rst b/docs/releases.rst index fbd7b38d..05dff653 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -15,6 +15,9 @@ unreleased commands in TorConfig * a first stealth-authentication implementation (for "normal" hidden services, not ephemeral) + * bug-fix from `david415 `_ to raise + ConnectionRefusedError instead of StopIteration when running out of + SOCKS ports. v0.14.1 ------- diff --git a/test/test_endpoints.py b/test/test_endpoints.py index c7e30167..c00f0d54 100644 --- a/test/test_endpoints.py +++ b/test/test_endpoints.py @@ -3,7 +3,7 @@ import tempfile from mock import patch -from mock import Mock +from mock import Mock, MagicMock from zope.interface import implements @@ -39,9 +39,6 @@ import util -connectionRefusedFailure = Failure(ConnectionRefusedError()) - - class EndpointTests(unittest.TestCase): def setUp(self): @@ -571,12 +568,12 @@ def __init__(self, *args, **kw): self.transport = None self.failure = kw.get('failure', None) - self.acceptPort = kw.get('acceptPort', None) + self.accept_port = kw.get('accept_port', None) def connect(self, fac): self.factory = fac - if self.acceptPort: - if self.port != self.acceptPort: + if self.accept_port: + if self.port != self.accept_port: return defer.fail(self.failure) else: if self.failure: @@ -595,10 +592,10 @@ def test_client_connection_failed(self): This test is equivalent to txsocksx's TestSOCKS4ClientEndpoint.test_clientConnectionFailed """ - def FailTorSocksEndpointGenerator(*args, **kw): - kw['failure'] = connectionRefusedFailure + def fail_tor_socks_endpoint_generator(*args, **kw): + kw['failure'] = Failure(ConnectionRefusedError()) return FakeTorSocksEndpoint(*args, **kw) - endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=FailTorSocksEndpointGenerator) + endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=fail_tor_socks_endpoint_generator) d = endpoint.connect(None) return self.assertFailure(d, ConnectionRefusedError) @@ -606,18 +603,18 @@ def test_client_connection_failed_user_password(self): """ Same as above, but with a username/password. """ - def FailTorSocksEndpointGenerator(*args, **kw): - kw['failure'] = connectionRefusedFailure + def fail_tor_socks_endpoint_generator(*args, **kw): + kw['failure'] = Failure(ConnectionRefusedError()) return FakeTorSocksEndpoint(*args, **kw) endpoint = TorClientEndpoint( 'invalid host', 0, socks_username='billy', socks_password='s333cure', - _proxy_endpoint_generator=FailTorSocksEndpointGenerator) + _proxy_endpoint_generator=fail_tor_socks_endpoint_generator) d = endpoint.connect(None) return self.assertFailure(d, ConnectionRefusedError) def test_default_generator(self): - # just ensuring the default generator doesn't blow updoesn't blow up + # just ensuring the default generator doesn't blow up default_tcp4_endpoint_generator(None, 'foo.bar', 1234) def test_no_host(self): @@ -647,28 +644,48 @@ def test_default_factory(self): """ This test is equivalent to txsocksx's TestSOCKS5ClientEndpoint.test_defaultFactory """ - def TorSocksEndpointGenerator(*args, **kw): + endpoints = [] + def tor_socks_endpoint_generator(*args, **kw): + endpoints.append(FakeTorSocksEndpoint(*args, **kw)) + return endpoints[-1] + endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=tor_socks_endpoint_generator) + endpoint.connect(Mock) + self.assertEqual(1, len(endpoints)) + self.assertEqual(endpoints[0].transport.value(), '\x05\x01\x00') + + @patch('txtorcon.endpoints.SOCKS5ClientEndpoint') + @defer.inlineCallbacks + def test_success(self, socks5_factory): + ep = MagicMock() + gold_proto = object() + ep.connect = MagicMock(return_value=gold_proto) + socks5_factory.return_value = ep + + def tor_socks_endpoint_generator(*args, **kw): return FakeTorSocksEndpoint(*args, **kw) - endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator) - endpoint.connect(None) - self.assertEqual(endpoint.tor_socks_endpoint.transport.value(), '\x05\x01\x00') + + endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=tor_socks_endpoint_generator) + other_proto = yield endpoint.connect(MagicMock()) + self.assertEqual(other_proto, gold_proto) def test_good_port_retry(self): """ This tests that our Tor client endpoint retry logic works correctly. - We create a proxy endpoint that fires a connectionRefusedFailure + We create a proxy endpoint that fires a ConnectionRefusedError unless the connecting port matches. We attempt to connect with the proxy endpoint for each port that the Tor client endpoint will try. """ success_ports = TorClientEndpoint.socks_ports_to_try + endpoints = [] for port in success_ports: - def TorSocksEndpointGenerator(*args, **kw): - kw['acceptPort'] = port - kw['failure'] = connectionRefusedFailure - return FakeTorSocksEndpoint(*args, **kw) - endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator) + def tor_socks_endpoint_generator(*args, **kw): + kw['accept_port'] = port + kw['failure'] = Failure(ConnectionRefusedError()) + endpoints.append(FakeTorSocksEndpoint(*args, **kw)) + return endpoints[-1] + endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=tor_socks_endpoint_generator) endpoint.connect(None) - self.assertEqual(endpoint.tor_socks_endpoint.transport.value(), '\x05\x01\x00') + self.assertEqual(endpoints[-1].transport.value(), '\x05\x01\x00') def test_bad_port_retry(self): """ @@ -676,11 +693,11 @@ def test_bad_port_retry(self): """ fail_ports = [1984, 666] for port in fail_ports: - def TorSocksEndpointGenerator(*args, **kw): - kw['acceptPort'] = port - kw['failure'] = connectionRefusedFailure + def tor_socks_endpoint_generator(*args, **kw): + kw['accept_port'] = port + kw['failure'] = Failure(ConnectionRefusedError()) return FakeTorSocksEndpoint(*args, **kw) - endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator) + endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=tor_socks_endpoint_generator) d = endpoint.connect(None) return self.assertFailure(d, ConnectionRefusedError) @@ -689,13 +706,16 @@ def test_good_no_guess_socks_port(self): This tests that if a SOCKS port is specified, we *only* attempt to connect to that SOCKS port. """ - def TorSocksEndpointGenerator(*args, **kw): - kw['acceptPort'] = 6669 - kw['failure'] = connectionRefusedFailure - return FakeTorSocksEndpoint(*args, **kw) - endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator, socks_port=6669) + endpoints = [] + def tor_socks_endpoint_generator(*args, **kw): + kw['accept_port'] = 6669 + kw['failure'] = Failure(ConnectionRefusedError()) + endpoints.append(FakeTorSocksEndpoint(*args, **kw)) + return endpoints[-1] + endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=tor_socks_endpoint_generator, socks_port=6669) endpoint.connect(None) - self.assertEqual(endpoint.tor_socks_endpoint.transport.value(), '\x05\x01\x00') + self.assertEqual(1, len(endpoints)) + self.assertEqual(endpoints[-1].transport.value(), '\x05\x01\x00') def test_bad_no_guess_socks_port(self): """ @@ -703,10 +723,10 @@ def test_bad_no_guess_socks_port(self): specified SOCKS port... even if there is a valid SOCKS port listening on the socks_ports_to_try list. """ - def TorSocksEndpointGenerator(*args, **kw): - kw['acceptPort'] = 9050 - kw['failure'] = connectionRefusedFailure + def tor_socks_endpoint_generator(*args, **kw): + kw['accept_port'] = 9050 + kw['failure'] = Failure(ConnectionRefusedError()) return FakeTorSocksEndpoint(*args, **kw) - endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator, socks_port=6669) + endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=tor_socks_endpoint_generator, socks_port=6669) d = endpoint.connect(None) self.assertFailure(d, ConnectionRefusedError) diff --git a/txtorcon/endpoints.py b/txtorcon/endpoints.py index c857eb5e..843cf43a 100644 --- a/txtorcon/endpoints.py +++ b/txtorcon/endpoints.py @@ -23,10 +23,11 @@ from twisted.internet.endpoints import serverFromString from twisted.internet.endpoints import clientFromString from twisted.internet.endpoints import TCP4ClientEndpoint +from twisted.internet.error import ConnectionRefusedError from twisted.internet import error from twisted.plugin import IPlugin from twisted.python.util import FancyEqMixin -from twisted.internet.error import ConnectionRefusedError +from twisted.python.failure import Failure from zope.interface import implementer from zope.interface import Interface, Attribute @@ -669,7 +670,7 @@ def __init__(self, host, port, self.port = int(port) self._proxy_endpoint_generator = _proxy_endpoint_generator self.socks_hostname = socks_hostname - self.socks_port = int(socks_port) if socks_port else None + self.socks_port = int(socks_port) if socks_port is not None else None self.socks_username = socks_username self.socks_password = socks_password @@ -677,54 +678,37 @@ def __init__(self, host, port, self._socks_port_iter = iter(self.socks_ports_to_try) self._socks_guessing_enabled = True else: + self._socks_port_iter = [socks_port] self._socks_guessing_enabled = False + @defer.inlineCallbacks def connect(self, protocolfactory): - self.protocolfactory = protocolfactory - - if self._socks_guessing_enabled: - self.socks_port = self._socks_port_iter.next() + last_error = None + for socks_port in self._socks_port_iter: + self.socks_port = socks_port + tor_ep = self._proxy_endpoint_generator( + reactor, + self.socks_hostname, + self.socks_port, + ) - d = self._try_connect() - return d + args = (self.host, self.port, tor_ep) + kwargs = dict() + if self.socks_username is not None and self.socks_password is not None: + kwargs['methods'] = dict( + login=(self.socks_username, self.socks_password), + ) - def _try_connect(self): - self.tor_socks_endpoint = self._proxy_endpoint_generator( - reactor, - self.socks_hostname, - self.socks_port - ) + socks_ep = SOCKS5ClientEndpoint(*args, **kwargs) - if self.socks_username is None or self.socks_password is None: - ep = SOCKS5ClientEndpoint( - self.host, - self.port, - self.tor_socks_endpoint - ) - else: - ep = SOCKS5ClientEndpoint( - self.host, - self.port, - self.tor_socks_endpoint, - methods=dict(login=(self.socks_username, self.socks_password)) - ) - - d = ep.connect(self.protocolfactory) - if self._socks_guessing_enabled: - d.addErrback(self._retry_socks_port) - return d + try: + proto = yield socks_ep.connect(protocolfactory) + defer.returnValue(proto) - def _retry_socks_port(self, failure): - failure.trap(error.ConnectError) - try: - self.socks_port = self._socks_port_iter.next() - except StopIteration: - return defer.fail( - ConnectionRefusedError('tor socks port retry failed') - ) - d = self._try_connect() - d.addErrback(self._retry_socks_port) - return d + except error.ConnectError as e0: + last_error = e0 + if last_error is not None: + raise last_error @implementer(IPlugin, IStreamClientEndpointStringParser) diff --git a/txtorcon/torconfig.py b/txtorcon/torconfig.py index 4d2ff0a6..c0667452 100644 --- a/txtorcon/torconfig.py +++ b/txtorcon/torconfig.py @@ -846,7 +846,8 @@ def __init__(self, ports, key_blob_or_type='NEW:BEST', auth=[], ver=2): # FIXME nicer than assert, plz assert ' ' not in self._key_blob assert isinstance(ports, types.ListType) - if not key_blob_or_type.startswith('NEW:') and len(key_blob_or_type) != (812 + 8): + if not key_blob_or_type.startswith('NEW:') \ + and (len(key_blob_or_type) > 825 or len(key_blob_or_type) < 820): raise RuntimeError('Wrong size key-blob') @defer.inlineCallbacks @@ -861,7 +862,8 @@ def add_to_tor(self, protocol): ans = yield protocol.queue_command(cmd) ans = find_keywords(ans.split('\n')) self.hostname = ans['ServiceID'] + '.onion' - self.private_key = ans['PrivateKey'] + if self._key_blob == 'NEW:BEST': + self.private_key = ans['PrivateKey'] log.msg('Created hidden-service at', self.hostname)