Skip to content
Permalink
Browse files
Implement basic parsing of SOCKS messages with parsley
* Does not parse authenticated messages
* XXX Currently parsing of reply messages and methods is broken because of
https://bugs.launchpad.net/parsley/+bug/1085492

Update parser and add unittests

Refactoring of SOCKSClient to not depend on qbuf.
* Adapt unittests to support the changes in design
* XXX disable some unittests that are no longer needed

Finish implementation of SOCKS5 proxy client with example usage.
  • Loading branch information
hellais committed Dec 5, 2012
1 parent 0082cb6 commit dcea1b008b57523e02d0eeecc4c750e671847be2
Showing with 504 additions and 148 deletions.
  1. +39 −0 example.py
  2. +21 −0 scripts/codegen.py
  3. +22 −9 txsocksx/auth.py
  4. +93 −53 txsocksx/client.py
  5. +59 −14 txsocksx/errors.py
  6. +112 −0 txsocksx/parser.py
  7. +20 −72 txsocksx/test/test_client.py
  8. +138 −0 txsocksx/test/test_parser.py
@@ -0,0 +1,39 @@
from twisted.internet import reactor
from twisted.internet.protocol import Protocol, Factory
from twisted.internet.endpoints import TCP4ClientEndpoint

from txsocksx.client import SOCKS5ClientEndpoint

class GETSlash(Protocol):
def connectionMade(self):
self.transport.write("GET / HTTP/1.1\n\r\n\r")

def buildProtocol(self):
return self

def dataReceived(self, data):
print "Got this as a response"
print data

class GETSlashFactory(Factory):
def buildProtocol(self, addr):
print "Building protocol towards"
return GETSlash()

socks_addr = '127.0.0.1'
socks_port = 9050
TCPPoint = TCP4ClientEndpoint(reactor, socks_addr, socks_port)

dst_addr = 'checkip.dyndns.com'
dst_port = 80
SOCKSPoint = SOCKS5ClientEndpoint(dst_addr,
dst_port, TCPPoint)

d = SOCKSPoint.connect(GETSlashFactory())
@d.addErrback
def _gotError(error):
print "Error in connection"
reactor.stop()

reactor.run()

@@ -0,0 +1,21 @@


def makeTestFailure():
l = ['ServerFailure',
'ConnectionNotAllowed',
'NetworkUnreachable',
'HostUnreachable',
'ConnectionRefused',
'TTLExpired',
'CommandNotSupported',
'AddressNotSupported']

base = """
def test_ServerReply%(error_name)s(self):
p = SOCKSGrammar(dummyServerReplyFail%(idx)sIPV4)
failure, addr, port = p.serverReply()
self.assertIs(failure, e.%(error_name)s)
"""
for i, v in enumerate(l):
print base % {'idx': i+1, 'error_name': v}
makeTestFailure()
@@ -1,28 +1,41 @@
from twisted.internet import defer
from txsocksx.errors import SocksError
from txsocksx.errors import SOCKSError

class Anonymous(object):
"""
( 0 )
"""
method = '\x00'

def negotiate(self, proto):
self.negotiated = True
return defer.succeed(None)

class UsernamePasswordAuthFailed(SocksError):
class GSSAPI(object):
"""
( 1 )
"""
method = '\x01'
def negotiate(self, proto):
raise NotImplemented

class UsernamePasswordAuthFailed(SOCKSError):
pass

class UsernamePassword(object):
"""
( 2 )
"""
method = '\x02'

def __init__(self, uname, passwd):
self.uname = uname
self.passwd = passwd

@defer.inlineCallbacks
def negotiate(self, proto):
proto.transport.write(
'\x01'
self.method
+ chr(len(self.uname)) + self.uname
+ chr(len(self.passwd)) + self.passwd)
resp, = yield proto.unpack('!xB')
if resp != 0:
raise UsernamePasswordAuthFailed(resp)
# XXX implement the reading of the response and make sure
# authentication suceeded
return defer.succeed(None)

@@ -1,88 +1,127 @@
import struct

from qbuf.support.twisted import MultiBufferer, MODE_RAW
from twisted.internet import protocol, defer, interfaces
from twisted.python import failure
from zope.interface import implements

import txsocksx.constants as c, txsocksx.errors as e
from txsocksx import constants as c
import txsocksx.errors as e

from txsocksx import auth

def socks_host(host):
return chr(c.ATYP_DOMAINNAME) + chr(len(host)) + host
from txsocksx.parser import SOCKSGrammar

def shortToBytes(i):
return chr(i >> 8) + chr(i & 0xff)

class Socks5ClientTransport(object):
class SOCKS5ClientTransport(object):
def __init__(self, wrappedClient):
self.wrappedClient = wrappedClient
self.transport = self.wrappedClient.transport

def __getattr__(self, attr):
return getattr(self.transport, attr)

class Socks5Client(MultiBufferer):
class SOCKS5Client(protocol.Protocol):
implements(interfaces.ITransport)

otherProtocol = None
debug = True

def __init__(self):
self._state = 'ServerVersionMethod'

def connectionMade(self):
d = self.readReply()
@d.addErrback
def _eb(reason):
self.factory.proxyConnectionFailed(reason)
self.close()
return d

@defer.inlineCallbacks
def readReply(self):
methodMap = dict((m.method, m) for m in self.factory.authMethods)
self.writeVersionMethod()

def writeVersionMethod(self):
"""
This creates:
ver octet:nmethods octet{2, 255}:methods
"""
supported_methods = [m.method for m in self.factory.authMethods]

message = struct.pack('!BB', c.VER_SOCKS5,
len(supported_methods))
message += ''.join(supported_methods)

self.transport.write(message)

def writeRequest(self, result, cmd=c.CMD_CONNECT):
"""
This creates:
clientRequestMessage =
ver cmd rsv SOCKSAddress port
"""
# XXX-Security audit makeGrammar
message = SOCKSGrammar(self.factory.host)
req = struct.pack('!BBB', c.VER_SOCKS5, cmd, 0)
self.transport.write(
struct.pack('!BB', c.VER_SOCKS5, len(methodMap))
+ ''.join(methodMap))
method, = yield self.unpack('!xc')
if method not in methodMap:
raise e.MethodsNotAcceptedError('no method proprosed was accepted',
methodMap.keys(), method)
yield methodMap[method].negotiate(self)
data = struct.pack('!BBB', c.VER_SOCKS5, c.CMD_CONNECT, c.RSV)
port = struct.pack('!H', self.factory.port)
self.transport.write(data + socks_host(self.factory.host) + port)
status, address_type = yield self.unpack('!xBxB')
if status != c.SOCKS5_GRANTED:
raise e.ConnectionError('connection rejected by SOCKS server',
status,
e.socks5ErrorMap.get(status, status))

# Discard the host and port data from the server.
if address_type == c.ATYP_IPV4:
yield self.read(4)
elif address_type == c.ATYP_DOMAINNAME:
host_length, = yield self.unpack('!B')
yield self.read(host_length)
elif address_type == c.ATYP_IPV6:
yield self.read(16)
yield self.read(2)

self.setMode(MODE_RAW)
self.factory.proxyConnectionEstablished(self)
req + \
message.hostToSOCKSAddress() + \
shortToBytes(self.factory.port)
)
self.log("writeRequest %s" % (repr(req)))
self._state = 'ServerReply'

def readServerVersionMethod(self, message):
self.log("readServerVersionMethod")
ver, method = message.serverVersionMethod()
if method not in self.factory.authMethods:
raise e.MethodsNotAcceptedError(
'no method proprosed was accepted',
self.factory.authMethods, method)
else:
auth_method = method()
d = defer.maybeDeferred(auth_method.negotiate, self)
d.addCallback(self.writeRequest)

def proxyEstablished(self, other):
self.otherProtocol = other
other.makeConnection(Socks5ClientTransport(self))
def readServerReply(self, message):
status, address, port = message.serverReply()
self.log("readServerReply %s %s %s" % (status, address, port))
if status != 0:
raise status

def rawDataReceived(self, data):
self._state = 'ProxyData'
self.factory.proxyConnectionEstablished(self)

def readProxyData(self, data):
# There really is no reason for this to get called; we shouldn't be in
# raw mode until after SOCKS negotiation finishes.
assert self.otherProtocol is not None
self.otherProtocol.dataReceived(data)

def dataReceived(self, data):
self.log("Got some data %s" % repr(data))
if self._state == 'ProxyData':
self.readProxyData(data)
return

# XXX-Security audit makeGrammar
message = SOCKSGrammar(data)

current_state_method = getattr(self, 'read' + self._state)
d = defer.maybeDeferred(current_state_method,
message)

def proxyEstablished(self, other):
self.otherProtocol = other
other.makeConnection(SOCKS5ClientTransport(self))

def connectionLost(self, reason):
if self.otherProtocol:
self.log("Connection Lost with other protocol")
self.otherProtocol.connectionLost(reason)
else:
self.log("Connection Lost with no protocol")
self.factory.proxyConnectionFailed(
failure.Failure(e.ConnectionLostEarly()))
def log(self, msg):
if self.debug:
print msg

class Socks5ClientFactory(protocol.ClientFactory):
protocol = Socks5Client
class SOCKS5ClientFactory(protocol.ClientFactory):
protocol = SOCKS5Client

def __init__(self, host, port, proxiedFactory, authMethods):
self.host = host
@@ -95,6 +134,7 @@ def proxyConnectionFailed(self, reason):
self.deferred.errback(reason)

def clientConnectionFailed(self, connector, reason):
print "Failed because of %s" % reason
self.proxyConnectionFailed(reason)

def proxyConnectionEstablished(self, proxyProtocol):
@@ -104,17 +144,17 @@ def proxyConnectionEstablished(self, proxyProtocol):
proxyProtocol.proxyEstablished(proto)
self.deferred.callback(proto)

class Socks5ClientEndpoint(object):
class SOCKS5ClientEndpoint(object):
implements(interfaces.IStreamClientEndpoint)

def __init__(self, host, port, proxyEndpoint, authMethods=(auth.Anonymous(),)):
def __init__(self, host, port, proxyEndpoint, authMethods=(auth.Anonymous,)):
self.host = host
self.port = port
self.proxyEndpoint = proxyEndpoint
self.authMethods = authMethods

def connect(self, fac):
proxyFac = Socks5ClientFactory(self.host, self.port, fac, self.authMethods)
proxyFac = SOCKS5ClientFactory(self.host, self.port, fac, self.authMethods)
self.proxyEndpoint.connect(proxyFac)
# XXX: maybe use the deferred returned here? need to more different
# ways/times a connection can fail before connectionMade is called.

0 comments on commit dcea1b0

Please sign in to comment.