From 805e07cd97d8457e37238b8767b3690ad33575b6 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 23 May 2017 00:16:05 -0700 Subject: [PATCH 1/6] setup.py: extras=dev requires txtorcon on both py2 and py3 We used to avoid this on py3, but now that txtorcon (0.19.2) is compatible with it, we can use it on both. This also means tests can rely on having txtorcon available. --- setup.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/setup.py b/setup.py index 24101ac..e759d74 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,9 @@ -import sys from setuptools import setup import versioneer commands = versioneer.get_cmdclass() -DEV_REQUIREMENTS = [ - "mock", - "tox", - "pyflakes", -] -if sys.version_info[0] < 3: - # txtorcon is not yet compatible with py3, so we include "txtorcon" in - # DEV_REQUIREMENTS under py2 but not under py3. The test suite will skip - # the tor tests when txtorcon is not importable. This results in - # different wheels when built under py2 vs py3 (with different - # extras_require[dev] dependencies), but I think this is ok, since nobody - # should be installing with [dev] from a wheel. - DEV_REQUIREMENTS.append("txtorcon") - setup(name="magic-wormhole", version=versioneer.get_version(), description="Securely transfer data between computers", @@ -53,8 +38,8 @@ setup(name="magic-wormhole", ], extras_require={ ':sys_platform=="win32"': ["pypiwin32"], - "tor": ["txtorcon"], - "dev": DEV_REQUIREMENTS, # includes txtorcon on py2, but not py3 + "tor": ["txtorcon >= 0.19.2"], + "dev": ["mock", "tox", "pyflakes", "txtorcon >= 0.19.2"], }, test_suite="wormhole.test", cmdclass=commands, From 46a9c9eeb9729cf5b79f32b7b59762b1c2047b5a Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 23 May 2017 00:45:02 -0700 Subject: [PATCH 2/6] rewrite tor support This shifts most reponsibility to the new txtorcon "Controller" object, where it belongs. We no longer need a list of likely control-port locations, nor do we need to keep track of the SOCKS port ourselves. The one downside is that if a control-port is not reachable, then this does not fall back to using a plain SOCKS port (usually tcp:localhost:9050). txtorcon no longer uses txsocksx, so it no longer advertises a simple way to use Tor without the control port. This shouldn't affect users who run the TorBrowserBundle, or who are running a tor daemon which they can control directly, but it may break for users who want to use a pre-existing tor daemon that they don't have permissions to speak control-port to. --- src/wormhole/_boss.py | 6 +- src/wormhole/_rendezvous.py | 9 +- src/wormhole/cli/cmd_receive.py | 25 +- src/wormhole/cli/cmd_send.py | 24 +- src/wormhole/test/test_cli.py | 21 +- src/wormhole/test/test_tor_manager.py | 375 ++++---------------------- src/wormhole/test/test_transit.py | 10 +- src/wormhole/tor_manager.py | 228 +++++----------- src/wormhole/transit.py | 17 +- src/wormhole/wormhole.py | 6 +- src/wormhole/xfer_util.py | 25 +- 11 files changed, 194 insertions(+), 552 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 257bd8d..171397f 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -3,7 +3,7 @@ import re import six from zope.interface import implementer from attr import attrs, attrib -from attr.validators import provides, instance_of +from attr.validators import provides, instance_of, optional from twisted.python import log from automat import MethodicalMachine from . import _interfaces @@ -35,7 +35,7 @@ class Boss(object): _versions = attrib(validator=instance_of(dict)) _reactor = attrib() _journal = attrib(validator=provides(_interfaces.IJournal)) - _tor_manager = attrib() # TODO: ITorManager or None + _tor = attrib(validator=optional(provides(_interfaces.ITorManager))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() set_trace = getattr(m, "_setTrace", lambda self, f: None) @@ -53,7 +53,7 @@ class Boss(object): self._R = Receive(self._side, self._timing) self._RC = RendezvousConnector(self._url, self._appid, self._side, self._reactor, self._journal, - self._tor_manager, self._timing) + self._tor, self._timing) self._L = Lister(self._timing) self._A = Allocator(self._timing) self._I = Input(self._timing) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index bb1ece4..269ce17 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -2,7 +2,7 @@ from __future__ import print_function, absolute_import, unicode_literals import os from six.moves.urllib_parse import urlparse from attr import attrs, attrib -from attr.validators import provides, instance_of +from attr.validators import provides, instance_of, optional from zope.interface import implementer from twisted.python import log from twisted.internet import defer, endpoints @@ -65,7 +65,7 @@ class RendezvousConnector(object): _side = attrib(validator=instance_of(type(u""))) _reactor = attrib() _journal = attrib(validator=provides(_interfaces.IJournal)) - _tor_manager = attrib() # TODO: ITorManager or None + _tor = attrib(validator=optional(provides(_interfaces.ITorManager))) _timing = attrib(validator=provides(_interfaces.ITiming)) def __attrs_post_init__(self): @@ -86,8 +86,9 @@ class RendezvousConnector(object): self._trace(old_state="", input=what, new_state="") def _make_endpoint(self, hostname, port): - if self._tor_manager: - return self._tor_manager.get_endpoint_for(hostname, port) + if self._tor: + # TODO: when we enable TLS, maybe add tls=True here + return self._tor.stream_via(hostname, port) return endpoints.HostnameEndpoint(self._reactor, hostname, port) def wire(self, boss, nameplate, mailbox, allocator, lister, terminator): diff --git a/src/wormhole/cli/cmd_receive.py b/src/wormhole/cli/cmd_receive.py index 7608c04..fe44411 100644 --- a/src/wormhole/cli/cmd_receive.py +++ b/src/wormhole/cli/cmd_receive.py @@ -7,7 +7,7 @@ from twisted.internet.defer import inlineCallbacks, returnValue from twisted.python import log from wormhole import create, input_with_completion, __version__ from ..transit import TransitReceiver -from ..errors import TransferError, WormholeClosedError, NoTorError +from ..errors import TransferError, WormholeClosedError from ..util import (dict_to_bytes, bytes_to_dict, bytes_to_hexstr, estimate_free_space) from .welcome import handle_welcome @@ -45,7 +45,7 @@ class Receiver: assert isinstance(args.relay_url, type(u"")) self.args = args self._reactor = reactor - self._tor_manager = None + self._tor = None self._transit_receiver = None def _msg(self, *args, **kwargs): @@ -55,29 +55,26 @@ class Receiver: def go(self): if self.args.tor: with self.args.timing.add("import", which="tor_manager"): - from ..tor_manager import TorManager - self._tor_manager = TorManager(self._reactor, - self.args.launch_tor, - self.args.tor_control_port, - timing=self.args.timing) - if not self._tor_manager.tor_available(): - raise NoTorError() + from ..tor_manager import get_tor # For now, block everything until Tor has started. Soon: launch - # tor in parallel with everything else, make sure the TorManager + # tor in parallel with everything else, make sure the Tor object # can lazy-provide an endpoint, and overlap the startup process # with the user handing off the wormhole code - yield self._tor_manager.start() + self._tor = yield get_tor(self._reactor, + self.args.launch_tor, + self.args.tor_control_port, + timing=self.args.timing) w = create(self.args.appid or APPID, self.args.relay_url, self._reactor, - tor_manager=self._tor_manager, + tor=self._tor, timing=self.args.timing) self._w = w # so tests can wait on events too # I wanted to do this instead: # # try: - # yield self._go(w, tor_manager) + # yield self._go(w, tor) # finally: # yield w.close() # @@ -230,7 +227,7 @@ class Receiver: def _build_transit(self, w, sender_transit): tr = TransitReceiver(self.args.transit_helper, no_listen=(not self.args.listen), - tor_manager=self._tor_manager, + tor=self._tor, reactor=self._reactor, timing=self.args.timing) self._transit_receiver = tr diff --git a/src/wormhole/cli/cmd_send.py b/src/wormhole/cli/cmd_send.py index aaa274a..12d925e 100644 --- a/src/wormhole/cli/cmd_send.py +++ b/src/wormhole/cli/cmd_send.py @@ -6,8 +6,7 @@ from twisted.python import log from twisted.protocols import basic from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, returnValue -from ..errors import (TransferError, WormholeClosedError, NoTorError, - UnsendableFileError) +from ..errors import (TransferError, WormholeClosedError, UnsendableFileError) from wormhole import create, __version__ from ..transit import TransitSender from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr @@ -31,7 +30,7 @@ class Sender: def __init__(self, args, reactor): self._args = args self._reactor = reactor - self._tor_manager = None + self._tor = None self._timing = args.timing self._fd_to_send = None self._transit_sender = None @@ -41,22 +40,19 @@ class Sender: assert isinstance(self._args.relay_url, type(u"")) if self._args.tor: with self._timing.add("import", which="tor_manager"): - from ..tor_manager import TorManager - self._tor_manager = TorManager(reactor, - self._args.launch_tor, - self._args.tor_control_port, - timing=self._timing) - if not self._tor_manager.tor_available(): - raise NoTorError() + from ..tor_manager import get_tor # For now, block everything until Tor has started. Soon: launch - # tor in parallel with everything else, make sure the TorManager + # tor in parallel with everything else, make sure the Tor object # can lazy-provide an endpoint, and overlap the startup process # with the user handing off the wormhole code - yield self._tor_manager.start() + self._tor = yield get_tor(reactor, + self._args.launch_tor, + self._args.tor_control_port, + timing=self._timing) w = create(self._args.appid or APPID, self._args.relay_url, self._reactor, - tor_manager=self._tor_manager, + tor=self._tor, timing=self._timing) d = self._go(w) @@ -151,7 +147,7 @@ class Sender: if self._fd_to_send: ts = TransitSender(args.transit_helper, no_listen=(not args.listen), - tor_manager=self._tor_manager, + tor=self._tor, reactor=self._reactor, timing=self._timing) self._transit_sender = ts diff --git a/src/wormhole/test/test_cli.py b/src/wormhole/test/test_cli.py index ba9cfc1..44d8cdf 100644 --- a/src/wormhole/test/test_cli.py +++ b/src/wormhole/test/test_cli.py @@ -4,9 +4,10 @@ from textwrap import fill, dedent from humanize import naturalsize import mock import click.testing +from zope.interface import implementer from twisted.trial import unittest from twisted.python import procutils, log -from twisted.internet import defer, endpoints, reactor +from twisted.internet import endpoints, reactor from twisted.internet.utils import getProcessOutputAndValue from twisted.internet.defer import gatherResults, inlineCallbacks, returnValue from .. import __version__ @@ -14,6 +15,7 @@ from .common import ServerBase, config from ..cli import cmd_send, cmd_receive, welcome, cli from ..errors import (TransferError, WrongPasswordError, WelcomeError, UnsendableFileError) +from .._interfaces import ITorManager from wormhole.server.cmd_server import MyPlugin from wormhole.server.cli import server @@ -297,15 +299,12 @@ class ScriptVersion(ServerBase, ScriptsBase, unittest.TestCase): self.failUnlessEqual(ver.strip(), "magic-wormhole {}".format(__version__)) self.failUnlessEqual(rc, 0) -class FakeTorManager: +@implementer(ITorManager) +class FakeTor: # use normal endpoints, but record the fact that we were asked def __init__(self): self.endpoints = [] - def tor_available(self): - return True - def start(self): - return defer.succeed(None) - def get_endpoint_for(self, host, port): + def stream_via(self, host, port): self.endpoints.append((host, port)) return endpoints.HostnameEndpoint(reactor, host, port) @@ -456,16 +455,16 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): if fake_tor: send_cfg.tor = True send_cfg.transit_helper = self.transit - tx_tm = FakeTorManager() - with mock.patch("wormhole.tor_manager.TorManager", + tx_tm = FakeTor() + with mock.patch("wormhole.tor_manager.get_tor", return_value=tx_tm, ) as mtx_tm: send_d = cmd_send.send(send_cfg) recv_cfg.tor = True recv_cfg.transit_helper = self.transit - rx_tm = FakeTorManager() - with mock.patch("wormhole.tor_manager.TorManager", + rx_tm = FakeTor() + with mock.patch("wormhole.tor_manager.get_tor", return_value=rx_tm, ) as mrx_tm: receive_d = cmd_receive.receive(recv_cfg) diff --git a/src/wormhole/test/test_tor_manager.py b/src/wormhole/test/test_tor_manager.py index c658d66..dea5e5d 100644 --- a/src/wormhole/test/test_tor_manager.py +++ b/src/wormhole/test/test_tor_manager.py @@ -3,335 +3,78 @@ import mock, io from twisted.trial import unittest from twisted.internet import defer from twisted.internet.error import ConnectError -from six import next -from ..tor_manager import TorManager, DEFAULT_VALUE +from ..tor_manager import get_tor +from ..errors import NoTorError +from .._interfaces import ITorManager + +class X(): + pass class Tor(unittest.TestCase): - def test_create(self): - tm = TorManager(None) - del tm + def test_no_txtorcon(self): + with mock.patch("wormhole.tor_manager.txtorcon", None): + self.failureResultOf(get_tor(None), NoTorError) def test_bad_args(self): - e = self.assertRaises(TypeError, - TorManager, None, launch_tor="not boolean") - self.assertEqual(str(e), "launch_tor= must be boolean") - e = self.assertRaises(TypeError, - TorManager, None, tor_control_port=1234) - self.assertEqual(str(e), "tor_control_port= must be str or None") - e = self.assertRaises(ValueError, - TorManager, None, launch_tor=True, - tor_control_port="tcp:127.0.0.1:1234") - self.assertEqual(str(e), + f = self.failureResultOf(get_tor(None, launch_tor="not boolean"), + TypeError) + self.assertEqual(str(f.value), "launch_tor= must be boolean") + + f = self.failureResultOf(get_tor(None, tor_control_port=1234), + TypeError) + self.assertEqual(str(f.value), "tor_control_port= must be str or None") + f = self.failureResultOf(get_tor(None, launch_tor=True, + tor_control_port="tcp:127.0.0.1:1234"), + ValueError) + self.assertEqual(str(f.value), "cannot combine --launch-tor and --tor-control-port=") - def test_start_launch_tor(self): - reactor = object() - stderr = io.StringIO() - tm = TorManager(reactor, launch_tor=True, stderr=stderr) - dlt_d = defer.Deferred() - tm._do_launch_tor = mock.Mock(return_value=dlt_d) - tm._try_control_port = mock.Mock() - d = tm.start() - self.assertNoResult(d) - tsep = object() - with mock.patch("wormhole.tor_manager.clientFromString", - return_value=tsep) as cfs: - dlt_d.callback(("tproto", "tconfig", "socks_desc")) - res = self.successResultOf(d) - self.assertEqual(res, None) - self.assertEqual(tm._tor_protocol, "tproto") - self.assertEqual(tm._tor_config, "tconfig") - self.assertEqual(tm._tor_socks_endpoint, tsep) - self.assertEqual(tm._do_launch_tor.mock_calls, [mock.call()]) - self.assertEqual(tm._try_control_port.mock_calls, []) - self.assertEqual(cfs.mock_calls, [mock.call(reactor, "socks_desc")]) - - def test_start_control_port_default_failure(self): - reactor = object() - stderr = io.StringIO() - tm = TorManager(reactor, stderr=stderr) - tm._do_launch_tor = mock.Mock() - tcp_ds = [defer.Deferred() for i in range(5)] - tcp_ds_iter = iter(tcp_ds) - attempted_control_ports = [] - def next_d(control_port): - attempted_control_ports.append(control_port) - return next(tcp_ds_iter) - tm._try_control_port = mock.Mock(side_effect=next_d) - d = tm.start() - tsep = object() - with mock.patch("wormhole.tor_manager.clientFromString", - return_value=tsep) as cfs: - self.assertNoResult(d) - self.assertEqual(attempted_control_ports, - ["unix:/var/run/tor/control"]) - self.assertEqual(tm._try_control_port.mock_calls, - [mock.call("unix:/var/run/tor/control")]) - tcp_ds[0].callback((None, None, None)) - - self.assertNoResult(d) - self.assertEqual(attempted_control_ports, - ["unix:/var/run/tor/control", - "tcp:127.0.0.1:9051", - ]) - self.assertEqual(tm._try_control_port.mock_calls, - [mock.call("unix:/var/run/tor/control"), - mock.call("tcp:127.0.0.1:9051"), - ]) - tcp_ds[1].callback((None, None, None)) - - self.assertNoResult(d) - self.assertEqual(attempted_control_ports, - ["unix:/var/run/tor/control", - "tcp:127.0.0.1:9051", - "tcp:127.0.0.1:9151", - ]) - self.assertEqual(tm._try_control_port.mock_calls, - [mock.call("unix:/var/run/tor/control"), - mock.call("tcp:127.0.0.1:9051"), - mock.call("tcp:127.0.0.1:9151"), - ]) - tcp_ds[2].callback((None, None, None)) - - res = self.successResultOf(d) - self.assertEqual(res, None) - self.assertEqual(tm._tor_protocol, None) - self.assertEqual(tm._tor_config, None) - self.assertEqual(tm._tor_socks_endpoint, tsep) - self.assertEqual(tm._do_launch_tor.mock_calls, []) - self.assertEqual(cfs.mock_calls, - [mock.call(reactor, "tcp:127.0.0.1:9050")]) - - def test_start_control_port_default(self): - reactor = object() - stderr = io.StringIO() - tm = TorManager(reactor, stderr=stderr) - tm._do_launch_tor = mock.Mock() - tcp_d = defer.Deferred() - # let it succeed on the first try - tm._try_control_port = mock.Mock(return_value=tcp_d) - d = tm.start() - self.assertNoResult(d) - tsep = object() - with mock.patch("wormhole.tor_manager.clientFromString", - return_value=tsep) as cfs: - tcp_d.callback(("tproto", "tconfig", "socks_desc")) - res = self.successResultOf(d) - self.assertEqual(res, None) - self.assertEqual(tm._tor_protocol, "tproto") - self.assertEqual(tm._tor_config, "tconfig") - self.assertEqual(tm._tor_socks_endpoint, tsep) - self.assertEqual(tm._do_launch_tor.mock_calls, []) - self.assertEqual(tm._try_control_port.mock_calls, - [mock.call("unix:/var/run/tor/control")]) - self.assertEqual(cfs.mock_calls, [mock.call(reactor, "socks_desc")]) - - def test_start_control_port_non_default_failure(self): - reactor = object() - my_port = "my_port" - stderr = io.StringIO() - tm = TorManager(reactor, tor_control_port=my_port, stderr=stderr) - tm._do_launch_tor = mock.Mock() - tcp_ds = [defer.Deferred() for i in range(5)] - tcp_ds_iter = iter(tcp_ds) - attempted_control_ports = [] - def next_d(control_port): - attempted_control_ports.append(control_port) - return next(tcp_ds_iter) - tm._try_control_port = mock.Mock(side_effect=next_d) - d = tm.start() - tsep = object() - with mock.patch("wormhole.tor_manager.clientFromString", - return_value=tsep) as cfs: - self.assertNoResult(d) - self.assertEqual(attempted_control_ports, [my_port]) - self.assertEqual(tm._try_control_port.mock_calls, - [mock.call(my_port)]) - tcp_ds[0].callback((None, None, None)) - - res = self.successResultOf(d) - self.assertEqual(res, None) - self.assertEqual(tm._tor_protocol, None) - self.assertEqual(tm._tor_config, None) - self.assertEqual(tm._tor_socks_endpoint, tsep) - self.assertEqual(tm._do_launch_tor.mock_calls, []) - self.assertEqual(cfs.mock_calls, - [mock.call(reactor, "tcp:127.0.0.1:9050")]) - - def test_start_control_port_non_default(self): - reactor = object() - my_port = "my_port" - stderr = io.StringIO() - tm = TorManager(reactor, tor_control_port=my_port, stderr=stderr) - tm._do_launch_tor = mock.Mock() - tcp_d = defer.Deferred() - tm._try_control_port = mock.Mock(return_value=tcp_d) - d = tm.start() - self.assertNoResult(d) - tsep = object() - with mock.patch("wormhole.tor_manager.clientFromString", - return_value=tsep) as cfs: - tcp_d.callback(("tproto", "tconfig", "socks_desc")) - res = self.successResultOf(d) - self.assertEqual(res, None) - self.assertEqual(tm._tor_protocol, "tproto") - self.assertEqual(tm._tor_config, "tconfig") - self.assertEqual(tm._tor_socks_endpoint, tsep) - self.assertEqual(tm._do_launch_tor.mock_calls, []) - self.assertEqual(tm._try_control_port.mock_calls, - [mock.call(my_port)]) - self.assertEqual(cfs.mock_calls, [mock.call(reactor, "socks_desc")]) - def test_launch(self): reactor = object() + my_tor = X() # object() didn't like providedBy() + launch_d = defer.Deferred() stderr = io.StringIO() - tc = mock.Mock() - mock_TorConfig = mock.patch("wormhole.tor_manager.TorConfig", - return_value=tc) - lt_d = defer.Deferred() - mock_launch_tor = mock.patch("wormhole.tor_manager.launch_tor", - return_value=lt_d) - mock_allocate_tcp_port = mock.patch("wormhole.tor_manager.allocate_tcp_port", - return_value=12345) - mock_clientFromString = mock.patch("wormhole.tor_manager.clientFromString") - with mock_TorConfig as mtc: - with mock_launch_tor as mlt: - with mock_allocate_tcp_port as matp: - with mock_clientFromString as mcfs: - tm = TorManager(reactor, launch_tor=True, stderr=stderr) - d = tm.start() - self.assertNoResult(d) - tp = mock.Mock() - lt_d.callback(tp) - res = self.successResultOf(d) - self.assertEqual(res, None) - self.assertIs(tm._tor_protocol, tp) - self.assertIs(tm._tor_config, tc) - self.assertEqual(mtc.mock_calls, [mock.call()]) - self.assertEqual(mlt.mock_calls, [mock.call(tc, reactor)]) - self.assertEqual(matp.mock_calls, [mock.call()]) - self.assertEqual(mcfs.mock_calls, - [mock.call(reactor, "tcp:127.0.0.1:12345")]) + with mock.patch("wormhole.tor_manager.txtorcon.launch", + side_effect=launch_d) as launch: + d = get_tor(reactor, launch_tor=True, stderr=stderr) + self.assertNoResult(d) + self.assertEqual(launch.mock_calls, [mock.call(reactor)]) + launch_d.callback(my_tor) + tor = self.successResultOf(d) + self.assertIs(tor, my_tor) + self.assert_(ITorManager.providedBy(tor)) + self.assertEqual(stderr.getvalue(), + " launching a new Tor process, this may take a while..\n") - def _do_test_try_control_port(self, socks_ports, exp_socks_desc, - btc_exception=None, tcfp_exception=None): + def test_connect(self): reactor = object() + my_tor = X() # object() didn't like providedBy() + tcp = "port" + connect_d = defer.Deferred() stderr = io.StringIO() - ep = object() - mock_clientFromString = mock.patch("wormhole.tor_manager.clientFromString", - return_value=ep) - tproto = mock.Mock() - btc_d = defer.Deferred() - mock_build_tor_connection = mock.patch("wormhole.tor_manager.build_tor_connection", return_value=btc_d) - torconfig = mock.Mock() - tc = mock.Mock() - tc.SocksPort = iter(socks_ports) - tc_d = defer.Deferred() - torconfig.from_protocol = mock.Mock(return_value=tc_d) - mock_torconfig = mock.patch("wormhole.tor_manager.TorConfig", torconfig) + with mock.patch("wormhole.tor_manager.txtorcon.connect", + side_effect=connect_d) as connect: + d = get_tor(reactor, tor_control_port=tcp, stderr=stderr) + self.assertNoResult(d) + self.assertEqual(connect.mock_calls, [mock.call(reactor, tcp)]) + connect_d.callback(my_tor) + tor = self.successResultOf(d) + self.assertIs(tor, my_tor) + self.assert_(ITorManager.providedBy(tor)) + self.assertEqual(stderr.getvalue(), " using Tor\n") - control_port = object() - - with mock_clientFromString as cfs: - with mock_build_tor_connection as btc: - with mock_torconfig: - tm = TorManager(reactor, stderr=stderr) - d = tm._try_control_port(control_port) - # waiting in 'tproto = yield build_tor_connection(..)' - self.assertNoResult(d) - self.assertEqual(cfs.mock_calls, - [mock.call(reactor, control_port)]) - self.assertEqual(btc.mock_calls, - [mock.call(ep, build_state=False)]) - self.assertEqual(torconfig.from_protocol.mock_calls, []) - - btc_d.callback(tproto) - # waiting in 'tconfig = yield TorConfig.from_protocol(..)' - self.assertNoResult(d) - self.assertEqual(torconfig.from_protocol.mock_calls, - [mock.call(tproto)]) - - tc_d.callback(tc) - res = self.successResultOf(d) - self.assertEqual(res, (tproto, tc, exp_socks_desc)) - - def test_try_control_port(self): - self._do_test_try_control_port(["1234 ignorestuff", - "unix:/foo WorldWritable"], - "tcp:127.0.0.1:1234") - self._do_test_try_control_port(["unix:/foo WorldWritable", - "1234 ignorestuff"], - "unix:/foo") - self._do_test_try_control_port([DEFAULT_VALUE, - "1234"], - "tcp:127.0.0.1:9050") - - def _do_test_try_control_port_exception(self, btc_exc=None, tcfp_exc=None): + def test_connect_fails(self): reactor = object() + tcp = "port" + connect_d = defer.Deferred() stderr = io.StringIO() - ep = object() - mock_clientFromString = mock.patch("wormhole.tor_manager.clientFromString", - return_value=ep) - tproto = mock.Mock() - btc_d = defer.Deferred() - mock_build_tor_connection = mock.patch("wormhole.tor_manager.build_tor_connection", return_value=btc_d) - torconfig = mock.Mock() - tcfp_d = defer.Deferred() - torconfig.from_protocol = mock.Mock(return_value=tcfp_d) - mock_torconfig = mock.patch("wormhole.tor_manager.TorConfig", torconfig) - - control_port = object() - - with mock_clientFromString: - with mock_build_tor_connection: - with mock_torconfig: - tm = TorManager(reactor, stderr=stderr) - d = tm._try_control_port(control_port) - # waiting in 'tproto = yield build_tor_connection(..)' - self.assertNoResult(d) - - if btc_exc: - btc_d.errback(btc_exc) - else: - btc_d.callback(tproto) - assert tcfp_exc - tcfp_d.errback(tcfp_exc) - - res = self.successResultOf(d) - self.assertEqual(res, (None, None, None)) - - def test_try_control_port_error(self): - self._do_test_try_control_port_exception(btc_exc=ValueError()) - self._do_test_try_control_port_exception(btc_exc=ConnectError()) - self._do_test_try_control_port_exception(tcfp_exc=ValueError()) - self._do_test_try_control_port_exception(tcfp_exc=ConnectError()) - - def test_badaddr(self): - tm = TorManager(None) - isnon = tm.is_non_public_numeric_address - self.assertTrue(isnon("10.0.0.1")) - self.assertTrue(isnon("127.0.0.1")) - self.assertTrue(isnon("192.168.78.254")) - self.assertTrue(isnon("::1")) - self.assertFalse(isnon("8.8.8.8")) - self.assertFalse(isnon("example.org")) - - def test_endpoint(self): - reactor = object() - stderr = io.StringIO() - tm = TorManager(reactor, stderr=stderr) - tm._tor_socks_endpoint = tse = object() - exp_ep = object() - with mock.patch("wormhole.tor_manager.TorClientEndpoint", - return_value=exp_ep) as tce: - ep = tm.get_endpoint_for("example.com", 1234) - self.assertIs(ep, exp_ep) - self.assertEqual(tce.mock_calls, - [mock.call(b"example.com", 1234, - socks_endpoint=tse)]) - with mock.patch("wormhole.tor_manager.TorClientEndpoint", - return_value=exp_ep) as tce: - ep = tm.get_endpoint_for("127.0.0.1", 1234) - self.assertEqual(ep, None) - self.assertEqual(tce.mock_calls, []) + with mock.patch("wormhole.tor_manager.txtorcon.connect", + side_effect=connect_d) as connect: + d = get_tor(reactor, tor_control_port=tcp, stderr=stderr) + self.assertNoResult(d) + self.assertEqual(connect.mock_calls, [mock.call(reactor, tcp)]) + connect_d.errback(ConnectError()) + self.failureResultOf(d, ConnectError) + self.assertEqual(stderr.getvalue(), + " unable to find control port, bailing\n") diff --git a/src/wormhole/test/test_transit.py b/src/wormhole/test/test_transit.py index 5c79a63..a710dce 100644 --- a/src/wormhole/test/test_transit.py +++ b/src/wormhole/test/test_transit.py @@ -149,14 +149,14 @@ class Hints(unittest.TestCase): self.assertIsInstance(efho(transit.DirectTCPV1Hint("host", 1234, 0.0)), endpoints.HostnameEndpoint) self.assertEqual(efho("unknown:stuff:yowza:pivlor"), None) - # c._tor_manager is currently None + # c._tor is currently None self.assertEqual(efho(transit.TorTCPV1Hint("host", "port", 0)), None) - c._tor_manager = mock.Mock() + c._tor = mock.Mock() def tor_ep(hostname, port): if hostname == "non-public": return None return ("tor_ep", hostname, port) - c._tor_manager.get_endpoint_for = mock.Mock(side_effect=tor_ep) + c._tor.stream_via = mock.Mock(side_effect=tor_ep) self.assertEqual(efho(transit.DirectTCPV1Hint("host", 1234, 0.0)), ("tor_ep", "host", 1234)) self.assertEqual(efho(transit.TorTCPV1Hint("host2.onion", 1234, 0.0)), @@ -1470,7 +1470,7 @@ class Transit(unittest.TestCase): @inlineCallbacks def test_success_direct_tor(self): clock = task.Clock() - s = transit.TransitSender("", tor_manager=mock.Mock(), reactor=clock) + s = transit.TransitSender("", tor=mock.Mock(), reactor=clock) s.set_transit_key(b"key") hints = yield s.get_connection_hints() # start the listener del hints @@ -1491,7 +1491,7 @@ class Transit(unittest.TestCase): @inlineCallbacks def test_success_direct_tor_relay(self): clock = task.Clock() - s = transit.TransitSender("", tor_manager=mock.Mock(), reactor=clock) + s = transit.TransitSender("", tor=mock.Mock(), reactor=clock) s.set_transit_key(b"key") hints = yield s.get_connection_hints() # start the listener del hints diff --git a/src/wormhole/tor_manager.py b/src/wormhole/tor_manager.py index b74b207..fc3beaf 100644 --- a/src/wormhole/tor_manager.py +++ b/src/wormhole/tor_manager.py @@ -1,171 +1,85 @@ from __future__ import print_function, unicode_literals -import sys, re -import six -from zope.interface import implementer +import sys +from zope.interface.declarations import directlyProvides from twisted.internet.defer import inlineCallbacks, returnValue -from twisted.internet.error import ConnectError -from twisted.internet.endpoints import clientFromString try: - from txtorcon import (TorConfig, launch_tor, build_tor_connection, - DEFAULT_VALUE, TorClientEndpoint) + import txtorcon except ImportError: - TorConfig = None - launch_tor = None - build_tor_connection = None - TorClientEndpoint = None - DEFAULT_VALUE = "DEFAULT_VALUE" -import ipaddress -from . import _interfaces + txtorcon = None +from . import _interfaces, errors from .timing import DebugTiming -from .transit import allocate_tcp_port +@inlineCallbacks +def get_tor(reactor, launch_tor=False, tor_control_port=None, + timing=None, stderr=sys.stderr): + """ + If launch_tor=True, I will try to launch a new Tor process, ask it + for its SOCKS and control ports, and use those for outbound + connections (and inbound onion-service listeners, if necessary). -@implementer(_interfaces.ITorManager) -class TorManager: - def __init__(self, reactor, launch_tor=False, tor_control_port=None, - timing=None, stderr=sys.stderr): - """ - If launch_tor=True, I will try to launch a new Tor process, ask it - for its SOCKS and control ports, and use those for outbound - connections (and inbound onion-service listeners, if necessary). + Otherwise if tor_control_port is provided, I will attempt to connect + to an existing Tor's control port at the endpoint it specifies. I'll + ask that Tor for its SOCKS port. - Otherwise if tor_control_port is provided, I will attempt to connect - to an existing Tor's control port at the endpoint it specifies. I'll - ask that Tor for its SOCKS port. + With no arguments, I will try to connect to an existing Tor's control + port at the usual places: [unix:/var/run/tor/control, + tcp:127.0.0.1:9051, tcp:127.0.0.1:9151]. If any are successful, I'll + ask that Tor for its SOCKS port. If none are successful, I'll attempt + to do SOCKS to tcp:127.0.0.1:9050. - With no arguments, I will try to connect to an existing Tor's control - port at the usual places: [unix:/var/run/tor/control, - tcp:127.0.0.1:9051, tcp:127.0.0.1:9151]. If any are successful, I'll - ask that Tor for its SOCKS port. If none are successful, I'll attempt - to do SOCKS to tcp:127.0.0.1:9050. + If I am unable to make a SOCKS connection, the initial connection to + the Rendezvous Server will fail, and the program will terminate. - If I am unable to make a SOCKS connection, the initial connection to - the Rendezvous Server will fail, and the program will terminate. + Control-port connections can only succeed if I can authenticate (by + reading a cookie file named by the Tor process), so the current user + must have permission to read that file (either they started Tor, e.g. + TorBrowser, or they are in a unix group that's been given access, + e.g. debian-tor). + """ + # rationale: launching a new Tor takes a long time, so only do it if + # the user specifically asks for it with --launch-tor. Using an + # existing Tor should be much faster, but still requires general + # permission via --tor. - Control-port connections can only succeed if I can authenticate (by - reading a cookie file named by the Tor process), so the current user - must have permission to read that file (either they started Tor, e.g. - TorBrowser, or they are in a unix group that's been given access, - e.g. debian-tor). - """ - # rationale: launching a new Tor takes a long time, so only do it if - # the user specifically asks for it with --launch-tor. Using an - # existing Tor should be much faster, but still requires general - # permission via --tor. + if not txtorcon: + raise errors.NoTorError() - self._reactor = reactor - if not isinstance(launch_tor, bool): # note: False is int - raise TypeError("launch_tor= must be boolean") - if not isinstance(tor_control_port, (type(""), type(None))): - raise TypeError("tor_control_port= must be str or None") - if launch_tor and tor_control_port is not None: - raise ValueError("cannot combine --launch-tor and --tor-control-port=") - self._launch_tor = launch_tor - self._tor_control_port = tor_control_port - self._timing = timing or DebugTiming() - self._stderr = stderr + if not isinstance(launch_tor, bool): # note: False is int + raise TypeError("launch_tor= must be boolean") + if not isinstance(tor_control_port, (type(""), type(None))): + raise TypeError("tor_control_port= must be str or None") + assert tor_control_port != "" + if launch_tor and tor_control_port is not None: + raise ValueError("cannot combine --launch-tor and --tor-control-port=") + timing = timing or DebugTiming() - def tor_available(self): - # unit tests mock out everything we get from txtorcon, so we can test - # this class under py3 even if txtorcon isn't installed. But the real - # commands need to know if they have Tor or not. - return bool(TorConfig) + # Connect to an existing Tor, or create a new one. If we need to + # launch an onion service, then we need a working control port (and + # authentication cookie). If we're only acting as a client, we don't + # need the control port. - @inlineCallbacks - def start(self): - # Connect to an existing Tor, or create a new one. If we need to - # launch an onion service, then we need a working control port (and - # authentication cookie). If we're only acting as a client, we don't - # need the control port. - - if self._launch_tor: - print(" launching a new Tor process, this may take a while..", - file=self._stderr) - with self._timing.add("launch tor"): - (tproto, tconfig, socks_desc) = yield self._do_launch_tor() - else: - control_ports = ["unix:/var/run/tor/control", # debian tor package - "tcp:127.0.0.1:9051", # standard Tor - "tcp:127.0.0.1:9151", # TorBrowser - ] - if self._tor_control_port: - control_ports = [self._tor_control_port] - with self._timing.add("find tor"): - for control_port in control_ports: - (tproto, tconfig, - socks_desc) = yield self._try_control_port(control_port) - if tproto: - print(" using Tor (control port %s) (SOCKS port %s)" - % (control_port, socks_desc), - file=self._stderr) - break - else: - tproto = None - tconfig = None - socks_desc = "tcp:127.0.0.1:9050" # fallback - print(" using Tor (SOCKS port %s)" % socks_desc, - file=self._stderr) - - self._tor_protocol = tproto - self._tor_config = tconfig - self._tor_socks_endpoint = clientFromString(self._reactor, socks_desc) - - @inlineCallbacks - def _do_launch_tor(self): - tconfig = TorConfig() - #tconfig.ControlPort = allocate_tcp_port() # defaults to 9052 - tconfig.SocksPort = allocate_tcp_port() - socks_desc = "tcp:127.0.0.1:%d" % tconfig.SocksPort - # this could take tor_binary= - tproto = yield launch_tor(tconfig, self._reactor) - returnValue((tproto, tconfig, socks_desc)) - - @inlineCallbacks - def _try_control_port(self, control_port): - NOPE = (None, None, None) - ep = clientFromString(self._reactor, control_port) - try: - tproto = yield build_tor_connection(ep, build_state=False) - # now wait for bootstrap - tconfig = yield TorConfig.from_protocol(tproto) - except (ValueError, ConnectError): - returnValue(NOPE) - socks_ports = list(tconfig.SocksPort) - socks_port = socks_ports[0] # TODO: when might there be multiple? - # I've seen "9050", and "unix:/var/run/tor/socks WorldWritable" - pieces = socks_port.split() - p = pieces[0] - if p == DEFAULT_VALUE: - socks_desc = "tcp:127.0.0.1:9050" - elif re.search('^\d+$', p): - socks_desc = "tcp:127.0.0.1:%s" % p - else: - socks_desc = p - returnValue((tproto, tconfig, socks_desc)) - - def is_non_public_numeric_address(self, host): - # for numeric hostnames, skip RFC1918 addresses, since no Tor exit - # node will be able to reach those. Likewise ignore IPv6 addresses. - try: - a = ipaddress.ip_address(host) - except ValueError: - return False # non-numeric, let Tor try it - if a.version != 4: - return True # IPv6 gets ignored - if (a.is_loopback or a.is_multicast or a.is_private or a.is_reserved - or a.is_unspecified): - return True # too weird, don't connect - return False - - def get_endpoint_for(self, host, port): - assert isinstance(port, six.integer_types) - if self.is_non_public_numeric_address(host): - return None - - # txsocksx doesn't like unicode: it concatenates some binary protocol - # bytes with the hostname when talking to the SOCKS server, so the - # py2 automatic unicode promotion blows up - host = host.encode("ascii") - ep = TorClientEndpoint(host, port, - socks_endpoint=self._tor_socks_endpoint) - return ep + if launch_tor: + print(" launching a new Tor process, this may take a while..", + file=stderr) + with timing.add("launch tor"): + tor = yield txtorcon.launch(reactor, + #data_directory=, + #tor_binary=, + ) + else: + with timing.add("find tor"): + try: + tor = yield txtorcon.connect(reactor, tor_control_port) + print(" using Tor", file=stderr) + except Exception: + #socks_desc = "tcp:127.0.0.1:9050" # fallback + #print(" using Tor (SOCKS port %s)" % socks_desc, + # file=stderr) + print(" unable to find control port, bailing", + file=stderr) + # TODO: something nicer. I think connect() is likely to throw + # a reactor.connectTCP -type error, like ConnectionFailed or + # ConnectionRefused or something + raise + directlyProvides(tor, _interfaces.ITorManager) + returnValue(tor) diff --git a/src/wormhole/transit.py b/src/wormhole/transit.py index ebeb960..ace76e7 100644 --- a/src/wormhole/transit.py +++ b/src/wormhole/transit.py @@ -589,7 +589,7 @@ class Common: RELAY_DELAY = 2.0 TRANSIT_KEY_LENGTH = SecretBox.KEY_SIZE - def __init__(self, transit_relay, no_listen=False, tor_manager=None, + def __init__(self, transit_relay, no_listen=False, tor=None, reactor=reactor, timing=None): self._side = bytes_to_hexstr(os.urandom(8)) # unicode if transit_relay: @@ -603,7 +603,7 @@ class Common: self._transit_relays = [] self._their_direct_hints = [] # hintobjs self._our_relay_hints = set(self._transit_relays) - self._tor_manager = tor_manager + self._tor = tor self._transit_key = None self._no_listen = no_listen self._waiting_for_transit_key = [] @@ -614,7 +614,7 @@ class Common: self._timing.add("transit") def _build_listener(self): - if self._no_listen or self._tor_manager: + if self._no_listen or self._tor: return ([], None) portnum = allocate_tcp_port() addresses = ipaddrs.find_addresses() @@ -820,7 +820,7 @@ class Common: if not ep: continue description = "->%s" % describe_hint_obj(hint_obj) - if self._tor_manager: + if self._tor: description = "tor" + description d = self._start_connector(ep, description) contenders.append(d) @@ -847,7 +847,7 @@ class Common: if not ep: continue description = "->relay:%s" % describe_hint_obj(hint_obj) - if self._tor_manager: + if self._tor: description = "tor" + description d = task.deferLater(self._reactor, relay_delay, self._start_connector, ep, description, @@ -887,12 +887,11 @@ class Common: return d def _endpoint_from_hint_obj(self, hint): - if self._tor_manager: + if self._tor: if isinstance(hint, (DirectTCPV1Hint, TorTCPV1Hint)): - # our TorManager will return None for non-public IPv4 + # this Tor object will return None for non-public IPv4 # addresses and any IPv6 address - return self._tor_manager.get_endpoint_for(hint.hostname, - hint.port) + return self._tor.stream_via(hint.hostname, hint.port) return None if isinstance(hint, DirectTCPV1Hint): return endpoints.HostnameEndpoint(self._reactor, diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 4100f9b..fda8295 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -279,7 +279,7 @@ class _DeferredWormhole(object): def create(appid, relay_url, reactor, # use keyword args for everything else versions={}, - delegate=None, journal=None, tor_manager=None, + delegate=None, journal=None, tor=None, timing=None, stderr=sys.stderr): timing = timing or DebugTiming() @@ -292,13 +292,13 @@ def create(appid, relay_url, reactor, # use keyword args for everything else wormhole_versions = {} # will be used to indicate Wormhole capabilities wormhole_versions["app_versions"] = versions # app-specific capabilities b = Boss(w, side, relay_url, appid, wormhole_versions, - reactor, journal, tor_manager, timing) + reactor, journal, tor, timing) w._set_boss(b) b.start() return w ## def from_serialized(serialized, reactor, delegate, -## journal=None, tor_manager=None, +## journal=None, tor=None, ## timing=None, stderr=sys.stderr): ## assert serialized["serialized_wormhole_version"] == 1 ## timing = timing or DebugTiming() diff --git a/src/wormhole/xfer_util.py b/src/wormhole/xfer_util.py index 1de6873..c62aa12 100644 --- a/src/wormhole/xfer_util.py +++ b/src/wormhole/xfer_util.py @@ -2,8 +2,7 @@ import json from twisted.internet.defer import inlineCallbacks, returnValue from . import wormhole -from .tor_manager import TorManager -from .errors import NoTorError +from .tor_manager import get_tor @inlineCallbacks def receive(reactor, appid, relay_url, code, @@ -27,18 +26,15 @@ def receive(reactor, appid, relay_url, code, :param on_code: if not None, this is called when we have a code (even if you passed in one explicitly) :type on_code: single-argument callable """ - tm = None + tor = None if use_tor: - tm = TorManager(reactor, launch_tor, tor_control_port) + tor = yield get_tor(reactor, launch_tor, tor_control_port) # For now, block everything until Tor has started. Soon: launch - # tor in parallel with everything else, make sure the TorManager + # tor in parallel with everything else, make sure the Tor object # can lazy-provide an endpoint, and overlap the startup process # with the user handing off the wormhole code - if not tm.tor_available(): - raise NoTorError() - yield tm.start() - wh = wormhole.create(appid, relay_url, reactor, tor_manager=tm) + wh = wormhole.create(appid, relay_url, reactor, tor=tor) if code is None: wh.allocate_code() code = yield wh.get_code() @@ -92,17 +88,14 @@ def send(reactor, appid, relay_url, data, code, :param on_code: if not None, this is called when we have a code (even if you passed in one explicitly) :type on_code: single-argument callable """ - tm = None + tor = None if use_tor: - tm = TorManager(reactor, launch_tor, tor_control_port) + tor = yield get_tor(reactor, launch_tor, tor_control_port) # For now, block everything until Tor has started. Soon: launch - # tor in parallel with everything else, make sure the TorManager + # tor in parallel with everything else, make sure the Tor object # can lazy-provide an endpoint, and overlap the startup process # with the user handing off the wormhole code - if not tm.tor_available(): - raise NoTorError() - yield tm.start() - wh = wormhole.create(appid, relay_url, reactor, tor_manager=tm) + wh = wormhole.create(appid, relay_url, reactor, tor=tor) if code is None: wh.allocate_code() code = yield wh.get_code() From 269faf190a2c85a5bc5020615b0022e1dbaef81c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 23 May 2017 15:01:57 -0700 Subject: [PATCH 3/6] fall backs to SOCKS if we can't reach control port --- src/wormhole/test/test_tor_manager.py | 32 +++++++++++++++++---- src/wormhole/tor_manager.py | 40 ++++++++++++++++++--------- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/src/wormhole/test/test_tor_manager.py b/src/wormhole/test/test_tor_manager.py index dea5e5d..f14e97c 100644 --- a/src/wormhole/test/test_tor_manager.py +++ b/src/wormhole/test/test_tor_manager.py @@ -4,7 +4,7 @@ from twisted.trial import unittest from twisted.internet import defer from twisted.internet.error import ConnectError -from ..tor_manager import get_tor +from ..tor_manager import get_tor, SocksOnlyTor from ..errors import NoTorError from .._interfaces import ITorManager @@ -62,7 +62,7 @@ class Tor(unittest.TestCase): tor = self.successResultOf(d) self.assertIs(tor, my_tor) self.assert_(ITorManager.providedBy(tor)) - self.assertEqual(stderr.getvalue(), " using Tor\n") + self.assertEqual(stderr.getvalue(), " using Tor via control port\n") def test_connect_fails(self): reactor = object() @@ -74,7 +74,27 @@ class Tor(unittest.TestCase): d = get_tor(reactor, tor_control_port=tcp, stderr=stderr) self.assertNoResult(d) self.assertEqual(connect.mock_calls, [mock.call(reactor, tcp)]) - connect_d.errback(ConnectError()) - self.failureResultOf(d, ConnectError) - self.assertEqual(stderr.getvalue(), - " unable to find control port, bailing\n") + + connect_d.errback(ConnectError()) + tor = self.successResultOf(d) + self.assertIsInstance(tor, SocksOnlyTor) + self.assert_(ITorManager.providedBy(tor)) + self.assertEqual(tor._reactor, reactor) + self.assertEqual(stderr.getvalue(), + " unable to find Tor control port, using SOCKS\n") + +class SocksOnly(unittest.TestCase): + def test_tor(self): + reactor = object() + sot = SocksOnlyTor(reactor) + fake_ep = object() + with mock.patch("wormhole.tor_manager.txtorcon.TorClientEndpoint", + return_value=fake_ep) as tce: + ep = sot.stream_via("host", "port") + self.assertIs(ep, fake_ep) + self.assertEqual(tce.mock_calls, [mock.call("host", "port", + socks_endpoint=None, + tls=False, + reactor=reactor)]) + + diff --git a/src/wormhole/tor_manager.py b/src/wormhole/tor_manager.py index fc3beaf..3aa2a11 100644 --- a/src/wormhole/tor_manager.py +++ b/src/wormhole/tor_manager.py @@ -1,5 +1,6 @@ from __future__ import print_function, unicode_literals import sys +from attr import attrs, attrib from zope.interface.declarations import directlyProvides from twisted.internet.defer import inlineCallbacks, returnValue try: @@ -9,6 +10,18 @@ except ImportError: from . import _interfaces, errors from .timing import DebugTiming +@attrs +class SocksOnlyTor(object): + _reactor = attrib() + + def stream_via(self, host, port, tls=False): + return txtorcon.TorClientEndpoint( + host, port, + socks_endpoint=None, # tries localhost:9050 and 9150 + tls=tls, + reactor=self._reactor, + ) + @inlineCallbacks def get_tor(reactor, launch_tor=False, tor_control_port=None, timing=None, stderr=sys.stderr): @@ -18,14 +31,15 @@ def get_tor(reactor, launch_tor=False, tor_control_port=None, connections (and inbound onion-service listeners, if necessary). Otherwise if tor_control_port is provided, I will attempt to connect - to an existing Tor's control port at the endpoint it specifies. I'll + to an existing Tor's control port at the endpoint it specifies. I'll ask that Tor for its SOCKS port. With no arguments, I will try to connect to an existing Tor's control port at the usual places: [unix:/var/run/tor/control, - tcp:127.0.0.1:9051, tcp:127.0.0.1:9151]. If any are successful, I'll - ask that Tor for its SOCKS port. If none are successful, I'll attempt - to do SOCKS to tcp:127.0.0.1:9050. + tcp:127.0.0.1:9051, tcp:127.0.0.1:9151]. If any are successful, I'll + ask that Tor for its SOCKS port. If none are successful, I'll + attempt to do SOCKS to the usual places: [tcp:127.0.0.1:9050, + tcp:127.0.0.1:9150]. If I am unable to make a SOCKS connection, the initial connection to the Rendezvous Server will fail, and the program will terminate. @@ -69,17 +83,17 @@ def get_tor(reactor, launch_tor=False, tor_control_port=None, else: with timing.add("find tor"): try: + # If tor_control_port is None (the default), txtorcon + # will look through a list of usual places. If it is set, + # it will look only in the place we tell it to. tor = yield txtorcon.connect(reactor, tor_control_port) - print(" using Tor", file=stderr) + print(" using Tor via control port", file=stderr) except Exception: - #socks_desc = "tcp:127.0.0.1:9050" # fallback - #print(" using Tor (SOCKS port %s)" % socks_desc, - # file=stderr) - print(" unable to find control port, bailing", + # TODO: make this more specific. I think connect() is + # likely to throw a reactor.connectTCP -type error, like + # ConnectionFailed or ConnectionRefused or something + print(" unable to find Tor control port, using SOCKS", file=stderr) - # TODO: something nicer. I think connect() is likely to throw - # a reactor.connectTCP -type error, like ConnectionFailed or - # ConnectionRefused or something - raise + tor = SocksOnlyTor(reactor) directlyProvides(tor, _interfaces.ITorManager) returnValue(tor) From 6b4ed71c8a896c353c4a2207f60d3f619bbc4ff0 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 23 May 2017 17:41:24 -0700 Subject: [PATCH 4/6] skip non-public IP addresses the old TorManager would return None for these, but txtorcon's new API throws ValueError, which we must catch and skip --- src/wormhole/transit.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/wormhole/transit.py b/src/wormhole/transit.py index ace76e7..e0a97dd 100644 --- a/src/wormhole/transit.py +++ b/src/wormhole/transit.py @@ -889,9 +889,12 @@ class Common: def _endpoint_from_hint_obj(self, hint): if self._tor: if isinstance(hint, (DirectTCPV1Hint, TorTCPV1Hint)): - # this Tor object will return None for non-public IPv4 + # this Tor object will throw ValueError for non-public IPv4 # addresses and any IPv6 address - return self._tor.stream_via(hint.hostname, hint.port) + try: + return self._tor.stream_via(hint.hostname, hint.port) + except ValueError: + return None return None if isinstance(hint, DirectTCPV1Hint): return endpoints.HostnameEndpoint(self._reactor, From 598ab8b62a1fffe23d08dde066d3a57664555b7b Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 24 May 2017 17:11:25 -0700 Subject: [PATCH 5/6] drop py3.3 support, since new txtorcon imports asyncio on py3 py3.3 is pretty rare in the wild anyways --- .appveyor.yml | 2 -- .travis.yml | 1 + README.md | 7 +++---- tox.ini | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index d53a4f2..c0005b8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,8 +13,6 @@ environment: # http://www.appveyor.com/docs/installed-software#python - PYTHON: "C:\\Python27" - PYTHON: "C:\\Python27-x64" - - PYTHON: "C:\\Python33" - - PYTHON: "C:\\Python33-x64" DISTUTILS_USE_SDK: "1" - PYTHON: "C:\\Python34" - PYTHON: "C:\\Python34-x64" diff --git a/.travis.yml b/.travis.yml index 2e2ebc4..7b96ecb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,4 +21,5 @@ after_success: - codecov matrix: allow_failures: + - python: "3.3" - python: "nightly" diff --git a/README.md b/README.md index 3408628..11538e4 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,7 @@ a bug in pynacl which gets confused when the libsodium runtime is installed (e.g. `libsodium13`) but not the development package. Developers can clone the source tree and run `tox` to run the unit tests on -all supported (and installed) versions of python: 2.7, 3.3, 3.4, 3.5, and -3.6. +all supported (and installed) versions of python: 2.7, 3.4, 3.5, and 3.6. ## Motivation @@ -234,8 +233,8 @@ If this happens, run `pip install -e .[dev]` again. This library is released under the MIT license, see LICENSE for details. -This library is compatible with python2.7, 3.3, 3.4, 3.5, and 3.6 . It -is probably compatible with py2.6, but the latest Twisted (>=15.5.0) is +This library is compatible with python2.7, 3.4, 3.5, and 3.6 . It is +probably compatible with py2.6, but the latest Twisted (>=15.5.0) is not. diff --git a/tox.ini b/tox.ini index 114911c..1943025 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = {py27,py33,py34,py35,py36,pypy} +envlist = {py27,py34,py35,py36,pypy} skip_missing_interpreters = True minversion = 2.4.0 From b0224cc08c821ff62130c074c6da1da09cb9ef43 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 25 May 2017 01:19:34 -0700 Subject: [PATCH 6/6] require txtorcon-0.19.3, for socks-port bugfix We needed the fix for https://github.com/meejah/txtorcon/issues/237 so that connect-to-running-tor works. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index e759d74..93e19c3 100644 --- a/setup.py +++ b/setup.py @@ -38,8 +38,8 @@ setup(name="magic-wormhole", ], extras_require={ ':sys_platform=="win32"': ["pypiwin32"], - "tor": ["txtorcon >= 0.19.2"], - "dev": ["mock", "tox", "pyflakes", "txtorcon >= 0.19.2"], + "tor": ["txtorcon >= 0.19.3"], + "dev": ["mock", "tox", "pyflakes", "txtorcon >= 0.19.3"], }, test_suite="wormhole.test", cmdclass=commands,