commit 4fad239939f250cdbd192e842ebcee3d3cbbebee Author: Giovanni `evilaliv3` Pellerano giovanni.pellerano@evilaliv3.org Date: Thu Nov 15 23:58:05 2012 +0100
implemented a SOCKv5 endpoint using the same _WrappingFactory used for TCP and SSL. Twisted Agent class extended to use SOCKv5 andpoint to permit HTTP connections throught Tor (shttp:// standing for socksified http) --- ooni/lib/txagentwithsocks.py | 203 ++++++++++++++++++++++++++++++++++++++++++ ooni/templates/httpt.py | 15 ++-- 2 files changed, 212 insertions(+), 6 deletions(-)
diff --git a/ooni/lib/txagentwithsocks.py b/ooni/lib/txagentwithsocks.py new file mode 100644 index 0000000..fecd9fc --- /dev/null +++ b/ooni/lib/txagentwithsocks.py @@ -0,0 +1,203 @@ +# -*- encoding: utf-8 -*- +# +# :authors: Giovanni Pellerano +# :licence: see LICENSE + +import struct + +from zope.interface import implements +from twisted.web import client +from twisted.internet.protocol import ClientFactory, Protocol +from twisted.internet.endpoints import TCP4ClientEndpoint, SSL4ClientEndpoint, _WrappingProtocol, _WrappingFactory +from twisted.internet import interfaces, defer + +class SOCKSError(Exception): + def __init__(self, value): + Exception.__init__(self) + self.code = value + +class SOCKSv5ClientProtocol(_WrappingProtocol): + state = 0 + + def __init__(self, connectedDeferred, wrappedProtocol, host, port): + _WrappingProtocol.__init__(self, connectedDeferred, wrappedProtocol) + self._host = host + self._port = port + self.ready = False + self.buf = [] + + def socks_state_0(self, data): + # error state + self._connectedDeferred.errback(SOCKSError(0x00)) + return + + def socks_state_1(self, data): + if data != "\x05\x00": + self._connectedDeferred.errback(SOCKSError(0x00)) + return + + # Anonymous access allowed - let's issue connect + self.transport.write(struct.pack("!BBBBB", 5, 1, 0, 3, + len(self._host)) + + self._host + + struct.pack("!H", self._port)) + + def socks_state_2(self, data): + if data[:2] != "\x05\x00": + # Anonymous access denied + + errcode = ord(data[1]) + self._connectedDeferred.errback(SOCKSError(errcode)) + + return + + self.ready = True + self._wrappedProtocol.transport = self.transport + self._wrappedProtocol.connectionMade() + + if self.buf != []: + self.transport.write(''.join(self.buf)) + self.buf = [] + + self._connectedDeferred.callback(self._wrappedProtocol) + + def connectionMade(self): + # We implement only Anonymous access + self.transport.write(struct.pack("!BB", 5, len("\x00")) + "\x00") + + self.state = self.state + 1 + + def connectionLost(self, reason): + pass + + def write(self, data): + if self.ready: + self.transport.write(data) + else: + self.buf.append(data) + + def dataReceived(self, data): + if self.state != 3: + getattr(self, 'socks_state_%s' % (self.state), + self.socks_state_0)(data) + self.state = self.state + 1 + else: + self._wrappedProtocol.dataReceived(data) + +class SOCKSv5ClientFactory(_WrappingFactory): + protocol = SOCKSv5ClientProtocol + + def __init__(self, wrappedFactory, host, port): + _WrappingFactory.__init__(self, wrappedFactory) + self._host, self._port = host, port + + def buildProtocol(self, addr): + """ + Proxy C{buildProtocol} to our C{self._wrappedFactory} or errback + the C{self._onConnection} L{Deferred}. + + @return: An instance of L{_WrappingProtocol} or C{None} + """'' + try: + proto = self._wrappedFactory.buildProtocol(addr) + except: + self._onConnection.errback() + else: + print self._host + return self.protocol(self._onConnection, proto, + self._host, self._port) + +class SOCKS5ClientEndpoint(object): + """ + TCP client endpoint with an IPv4 configuration. + """ + implements(interfaces.IStreamClientEndpoint) + + def __init__(self, reactor, sockhost, sockport, + host, port, timeout=30,bindAddress=None): + """ + @param reactor: An L{IReactorTCP} provider + + @param host: A hostname, used when connecting + @type host: str + + @param port: The port number, used when connecting + @type port: int + + @param timeout: The number of seconds to wait before assuming the + connection has failed. + @type timeout: int + + @param bindAddress: A (host, port) tuple of local address to bind to, + or None. + @type bindAddress: tuple + """ + self._reactor = reactor + self._sockhost = sockhost + self._sockport = sockport + self._host = host + self._port = port + self._timeout = timeout + self._bindAddress = bindAddress + + def connect(self, protocolFactory): + """ + Implement L{IStreamClientEndpoint.connect} to connect via TCP. + """ + try: + wf = SOCKSv5ClientFactory(protocolFactory, self._host, self._port) + self._reactor.connectTCP( + self._sockhost, self._sockport, wf, + timeout=self._timeout, bindAddress=self._bindAddress) + return wf._onConnection + except: + return defer.fail() + +class Agent(client.Agent): + def __init__(self, reactor, + contextFactory=client.WebClientContextFactory(), + connectTimeout=None, bindAddress=None, + pool=None, sockhost=None, sockport=None): + if pool is None: + pool = client.HTTPConnectionPool(reactor, False) + self._reactor = reactor + self._pool = pool + self._contextFactory = contextFactory + self._connectTimeout = connectTimeout + self._bindAddress = bindAddress + self._sockhost = sockhost + self._sockport = sockport + + def _getEndpoint(self, scheme, host, port): + """ + Get an endpoint for the given host and port, using a transport + selected based on scheme. + + @param scheme: A string like C{'http'} or C{'https'} (the only two + supported values) to use to determine how to establish the + connection. + + @param host: A C{str} giving the hostname which will be connected to in + order to issue a request. + + @param port: An C{int} giving the port number the connection will be + on. + + @return: An endpoint which can be used to connect to given address. + """ + kwargs = {} + if self._connectTimeout is not None: + kwargs['timeout'] = self._connectTimeout + kwargs['bindAddress'] = self._bindAddress + if scheme == 'http': + return TCP4ClientEndpoint(self._reactor, host, port, **kwargs) + elif scheme == 'shttp': + return SOCKS5ClientEndpoint(self._reactor, self._sockhost, + self._sockport, host, port, **kwargs) + elif scheme == 'https': + return SSL4ClientEndpoint(self._reactor, host, port, + self._wrapContextFactory(host, port), + **kwargs) + else: + raise SchemeNotSupported("Unsupported scheme: %r" % (scheme,)) + diff --git a/ooni/templates/httpt.py b/ooni/templates/httpt.py index ee6af90..011ff5b 100644 --- a/ooni/templates/httpt.py +++ b/ooni/templates/httpt.py @@ -4,15 +4,13 @@ # :licence: see LICENSE import copy import random - -from zope.interface import implements +import struct
from twisted.python import usage from twisted.plugin import IPlugin from twisted.internet import protocol, defer from twisted.internet.ssl import ClientContextFactory
-from twisted.web.client import Agent from twisted.internet import reactor from twisted.internet.error import ConnectionRefusedError
@@ -23,6 +21,8 @@ from ooni.utils import log
from ooni.utils.net import BodyReceiver, StringProducer, userAgents
+from ooni.lib.txagentwithsocks import Agent, SOCKSError + class HTTPTest(NetTestCase): """ A utility class for dealing with HTTP based testing. It provides methods to @@ -47,7 +47,7 @@ class HTTPTest(NetTestCase): log.err("Warning! pyOpenSSL is not installed. https websites will" "not work")
- self.agent = Agent(reactor) + self.agent = Agent(reactor, sockhost="127.0.0.1", sockport=9050)
if self.followRedirects: try: @@ -131,8 +131,11 @@ class HTTPTest(NetTestCase): d = self.build_request(url, method, headers, body)
def errback(failure): - failure.trap(ConnectionRefusedError) - log.err("Connection refused. The backend may be down") + failure.trap(ConnectionRefusedError, SOCKSError) + if type(failure.value) is ConnectionRefusedError: + log.err("Connection refused. The backend may be down") + else: + log.err("Sock error. The SOCK proxy may be down") self.report["failure"] = str(failure.value)
def finished(data):
tor-commits@lists.torproject.org