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/setup.py b/setup.py index 24101ac..93e19c3 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.3"], + "dev": ["mock", "tox", "pyflakes", "txtorcon >= 0.19.3"], }, test_suite="wormhole.test", cmdclass=commands, 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..f14e97c 100644 --- a/src/wormhole/test/test_tor_manager.py +++ b/src/wormhole/test/test_tor_manager.py @@ -3,335 +3,98 @@ 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, SocksOnlyTor +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 via control port\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) + 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)]) - control_port = object() + 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") - 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): +class SocksOnly(unittest.TestCase): + def test_tor(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, []) + 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/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..3aa2a11 100644 --- a/src/wormhole/tor_manager.py +++ b/src/wormhole/tor_manager.py @@ -1,171 +1,99 @@ from __future__ import print_function, unicode_literals -import sys, re -import six -from zope.interface import implementer +import sys +from attr import attrs, attrib +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 +@attrs +class SocksOnlyTor(object): + _reactor = attrib() -@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). + 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, + ) - 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. +@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). - 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. + 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. - If I am unable to make a SOCKS connection, the initial connection to - the Rendezvous Server will fail, and the program will terminate. + 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 the usual places: [tcp:127.0.0.1:9050, + tcp:127.0.0.1:9150]. - 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 I am unable to make a SOCKS connection, the initial connection to + the Rendezvous Server will fail, and the program will terminate. - 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 + 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. - 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) + if not txtorcon: + raise errors.NoTorError() - @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 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() - 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) + # 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. - 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: + # 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 via control port", file=stderr) + except Exception: + # 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) + tor = SocksOnlyTor(reactor) + directlyProvides(tor, _interfaces.ITorManager) + returnValue(tor) diff --git a/src/wormhole/transit.py b/src/wormhole/transit.py index ebeb960..e0a97dd 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,14 @@ 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 throw ValueError for non-public IPv4 # addresses and any IPv6 address - return self._tor_manager.get_endpoint_for(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, 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() 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