diff --git a/setup.py b/setup.py index b5456030..102282f5 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def run(self): "platforms": ["any"], "package_dir": {"": "src"}, - "packages": ["foolscap", "foolscap.slicers", "foolscap.logging", + "packages": ["twisted.plugins", "foolscap", "foolscap.slicers", "foolscap.logging", "foolscap.appserver", "foolscap.test"], "entry_points": {"console_scripts": [ "flogtool = foolscap.logging.cli:run_flogtool", @@ -58,7 +58,10 @@ def run(self): "flappclient = foolscap.appserver.client:run_flappclient", ] }, "cmdclass": commands, - "install_requires": ["twisted[tls] >= 16.0.0", "pyOpenSSL"], + "install_requires": ["twisted[tls] >= 16.0.0", "pyOpenSSL", "txsocksx"], + "extras_require": { + 'socks': ['txsocksx',], + } } if __name__ == "__main__": diff --git a/src/foolscap/connection_plugins.py b/src/foolscap/connection_plugins.py index ec2e045f..6444a865 100644 --- a/src/foolscap/connection_plugins.py +++ b/src/foolscap/connection_plugins.py @@ -1,8 +1,23 @@ + import re from zope.interface import implementer from twisted.internet import endpoints +from twisted.plugin import IPlugin + from foolscap.ipb import IConnectionHintHandler, InvalidHintError + +try: + import txsocksx +except ImportError: + txsocksx = None + +class PluginDependencyNotLoaded(Exception): + """ + PluginDependencyNotLoaded is raised when a plugin is instantiated + and a dependency is missing. + """ + # This can match IPv4 IP addresses + port numbers *or* host names + # port numbers. DOTTED_QUAD_RESTR=r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" @@ -11,6 +26,8 @@ DNS_NAME_RESTR)) NEW_STYLE_HINT_RE=re.compile(r"^tcp:(%s|%s):(\d+){1,5}$" % (DOTTED_QUAD_RESTR, DNS_NAME_RESTR)) +ANY_HINT_RE=re.compile(r"^[^:]*:(%s|%s):(\d+){1,5}$" % (DOTTED_QUAD_RESTR, + DNS_NAME_RESTR)) # Each location hint must start with "TYPE:" (where TYPE is alphanumeric) and # then can contain any characters except "," and "/". These are expected to @@ -42,8 +59,8 @@ def convert_legacy_hint(location): return "tcp:%s:%d" % (host, port) return location -@implementer(IConnectionHintHandler) -class DefaultTCP: +@implementer(IConnectionHintHandler, IPlugin) +class DefaultTCP(object): def hint_to_endpoint(self, hint, reactor): # Return (endpoint, hostname), where "hostname" is what we pass to the # HTTP "Host:" header so a dumb HTTP server can be used to redirect us. @@ -52,3 +69,27 @@ def hint_to_endpoint(self, hint, reactor): raise InvalidHintError("unrecognized TCP hint") host, port = mo.group(1), int(mo.group(2)) return endpoints.HostnameEndpoint(reactor, host, port), host + +@implementer(IConnectionHintHandler, IPlugin) +class SOCKS5(object): + def __init__(self, endpoint=None, proxy_endpoint_factory=None): + if txsocksx is None: + raise PluginDependencyNotLoaded("""SOCKS5 foolscap client transport plugin requires txsocksx.\n +If you are using a Python virtual env you can simply: pip install txsocksx;\n +Debian users can install via the APT repo: apt-get install txsocksx;\n""") + self.proxy_endpoint_factory = proxy_endpoint_factory + self.proxy_endpoint_desc = endpoint + self.proxy_endpoint = None + + def hint_to_endpoint(self, hint, reactor): + if self.proxy_endpoint_factory is not None: + self.proxy_endpoint = self.proxy_endpoint_factory() + else: + if self.proxy_endpoint is None: + self.proxy_endpoint = endpoints.clientFromString(reactor, self.proxy_endpoint_desc) + + mo = ANY_HINT_RE.search(hint) + if not mo: + raise InvalidHintError("Invalid SOCKS5 client connection hint") + host, port = mo.group(1), int(mo.group(2)) + return txsocksx.client.SOCKS5ClientEndpoint(host, port, self.proxy_endpoint), host diff --git a/src/foolscap/test/test_connection.py b/src/foolscap/test/test_connection.py index e434d26c..7fe720ba 100644 --- a/src/foolscap/test/test_connection.py +++ b/src/foolscap/test/test_connection.py @@ -1,15 +1,49 @@ + +from txsocksx.client import SOCKS5ClientEndpoint +from txsocksx.test.util import FakeEndpoint + from zope.interface import implementer from twisted.trial import unittest -from twisted.internet import endpoints +from twisted.internet import endpoints, reactor from twisted.application import service from foolscap.api import Tub from foolscap.connection import get_endpoint -from foolscap.connection_plugins import convert_legacy_hint, DefaultTCP +from foolscap.connection_plugins import convert_legacy_hint, DefaultTCP, SOCKS5 from foolscap.tokens import NoLocationHintsError from foolscap.test.common import (certData_low, certData_high, Target, ShouldFailMixin) from foolscap import ipb, util +class SocksPluginTests(unittest.TestCase): + def test_defaultFactory(self): + def SocksEndpointGenerator(): + return FakeEndpoint() + plugin = SOCKS5(endpoint = "tcp:127.0.0.1:9050", proxy_endpoint_factory = SocksEndpointGenerator) + hint = "tor:meowhost:80" + endpoint, host = plugin.hint_to_endpoint(hint, reactor) + + self.failUnlessEqual("meowhost", host) + self.failUnless(isinstance(endpoint, SOCKS5ClientEndpoint)) + + endpoint.connect(None) + # equivalent to the TestSOCKS5ClientEndpoint.test_defaultFactory test-case. + self.assertEqual(endpoint.proxyEndpoint.transport.value(), '\x05\x01\x00') + + def test_override(self): + SocksEndpointGenerator = lambda: FakeEndpoint() + ep, host = get_endpoint("tcp:meowhost:80", {"tcp": SOCKS5( + endpoint = "tcp:127.0.0.1:9050", proxy_endpoint_factory=SocksEndpointGenerator)}) + self.failUnless(isinstance(ep, SOCKS5ClientEndpoint), ep) + self.failUnlessEqual(host, "meowhost") + + def test_invalid(self): + SocksEndpointGenerator = lambda: FakeEndpoint() + hint = "tcp:meowhost80" + self.failUnlessRaises(ipb.InvalidHintError, + get_endpoint, hint, {"tcp": SOCKS5( + endpoint = "tcp:127.0.0.1:9050", proxy_endpoint_factory=SocksEndpointGenerator + )}) + class Convert(unittest.TestCase): def checkTCPEndpoint(self, hint, expected_host, expected_port): ep, host = get_endpoint(hint, {"tcp": DefaultTCP()}) diff --git a/src/twisted/plugins/load_transport_plugins.py b/src/twisted/plugins/load_transport_plugins.py new file mode 100644 index 00000000..aad86e75 --- /dev/null +++ b/src/twisted/plugins/load_transport_plugins.py @@ -0,0 +1,3 @@ +import foolscap +tcpTransportPlugin = foolscap.DefaultTCP() +socksTransportPlugin = foolscap.SOCKS5()