214 lines
7.9 KiB
Python
214 lines
7.9 KiB
Python
"""
|
|
src/network/socks5.py
|
|
=====================
|
|
|
|
"""
|
|
# pylint: disable=attribute-defined-outside-init
|
|
|
|
import socket
|
|
import struct
|
|
|
|
from proxy import GeneralProxyError, Proxy, ProxyError
|
|
|
|
|
|
class Socks5AuthError(ProxyError):
|
|
"""Thrown when the socks5 protocol encounters an authentication error"""
|
|
errorCodes = ("Succeeded",
|
|
"Authentication is required",
|
|
"All offered authentication methods were rejected",
|
|
"Unknown username or invalid password",
|
|
"Unknown error")
|
|
|
|
|
|
class Socks5Error(ProxyError):
|
|
"""Thrown when socks5 protocol encounters an error"""
|
|
errorCodes = ("Succeeded",
|
|
"General SOCKS server failure",
|
|
"Connection not allowed by ruleset",
|
|
"Network unreachable",
|
|
"Host unreachable",
|
|
"Connection refused",
|
|
"TTL expired",
|
|
"Command not supported",
|
|
"Address type not supported",
|
|
"Unknown error")
|
|
|
|
|
|
class Socks5(Proxy):
|
|
"""A socks5 proxy base class"""
|
|
def __init__(self, address=None):
|
|
Proxy.__init__(self, address)
|
|
self.ipaddr = None
|
|
self.destport = address[1]
|
|
|
|
def state_init(self):
|
|
"""Protocol initialisation (before connection is established)"""
|
|
if self._auth:
|
|
self.append_write_buf(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02))
|
|
else:
|
|
self.append_write_buf(struct.pack('BBB', 0x05, 0x01, 0x00))
|
|
self.set_state("auth_1", length=0, expectBytes=2)
|
|
return True
|
|
|
|
def state_auth_1(self):
|
|
"""Perform authentication if peer is requesting it."""
|
|
ret = struct.unpack('BB', self.read_buf[:2])
|
|
if ret[0] != 5:
|
|
# general error
|
|
raise GeneralProxyError(1)
|
|
elif ret[1] == 0:
|
|
# no auth required
|
|
self.set_state("auth_done", length=2)
|
|
elif ret[1] == 2:
|
|
# username/password
|
|
self.append_write_buf(struct.pack('BB', 1, len(self._auth[0])) +
|
|
self._auth[0] + struct.pack('B', len(self._auth[1])) +
|
|
self._auth[1])
|
|
self.set_state("auth_needed", length=2, expectBytes=2)
|
|
else:
|
|
if ret[1] == 0xff:
|
|
# auth error
|
|
raise Socks5AuthError(2)
|
|
else:
|
|
# other error
|
|
raise GeneralProxyError(1)
|
|
return True
|
|
|
|
def state_auth_needed(self):
|
|
"""Handle response to authentication attempt"""
|
|
ret = struct.unpack('BB', self.read_buf[0:2])
|
|
if ret[0] != 1:
|
|
# general error
|
|
raise GeneralProxyError(1)
|
|
if ret[1] != 0:
|
|
# auth error
|
|
raise Socks5AuthError(3)
|
|
# all ok
|
|
self.set_state("auth_done", length=2)
|
|
return True
|
|
|
|
def state_pre_connect(self):
|
|
"""Handle feedback from socks5 while it is connecting on our behalf."""
|
|
# Get the response
|
|
if self.read_buf[0:1] != chr(0x05).encode():
|
|
self.close()
|
|
raise GeneralProxyError(1)
|
|
elif self.read_buf[1:2] != chr(0x00).encode():
|
|
# Connection failed
|
|
self.close()
|
|
if ord(self.read_buf[1:2]) <= 8:
|
|
raise Socks5Error(ord(self.read_buf[1:2]))
|
|
else:
|
|
raise Socks5Error(9)
|
|
# Get the bound address/port
|
|
elif self.read_buf[3:4] == chr(0x01).encode():
|
|
self.set_state("proxy_addr_1", length=4, expectBytes=4)
|
|
elif self.read_buf[3:4] == chr(0x03).encode():
|
|
self.set_state("proxy_addr_2_1", length=4, expectBytes=1)
|
|
else:
|
|
self.close()
|
|
raise GeneralProxyError(1)
|
|
return True
|
|
|
|
def state_proxy_addr_1(self):
|
|
"""Handle IPv4 address returned for peer"""
|
|
self.boundaddr = self.read_buf[0:4]
|
|
self.set_state("proxy_port", length=4, expectBytes=2)
|
|
return True
|
|
|
|
def state_proxy_addr_2_1(self):
|
|
"""
|
|
Handle other addresses than IPv4 returned for peer (e.g. IPv6, onion, ...). This is part 1 which retrieves the
|
|
length of the data.
|
|
"""
|
|
self.address_length = ord(self.read_buf[0:1])
|
|
self.set_state("proxy_addr_2_2", length=1, expectBytes=self.address_length)
|
|
return True
|
|
|
|
def state_proxy_addr_2_2(self):
|
|
"""
|
|
Handle other addresses than IPv4 returned for peer (e.g. IPv6, onion, ...). This is part 2 which retrieves the
|
|
data.
|
|
"""
|
|
self.boundaddr = self.read_buf[0:self.address_length]
|
|
self.set_state("proxy_port", length=self.address_length, expectBytes=2)
|
|
return True
|
|
|
|
def state_proxy_port(self):
|
|
"""Handle peer's port being returned."""
|
|
self.boundport = struct.unpack(">H", self.read_buf[0:2])[0]
|
|
self.__proxysockname = (self.boundaddr, self.boundport)
|
|
if self.ipaddr is not None:
|
|
self.__proxypeername = (socket.inet_ntoa(self.ipaddr), self.destination[1])
|
|
else:
|
|
self.__proxypeername = (self.destination[0], self.destport)
|
|
self.set_state("proxy_handshake_done", length=2)
|
|
return True
|
|
|
|
def proxy_sock_name(self):
|
|
"""Handle return value when using SOCKS5 for DNS resolving instead of connecting."""
|
|
return socket.inet_ntoa(self.__proxysockname[0])
|
|
|
|
|
|
class Socks5Connection(Socks5):
|
|
"""Child socks5 class used for making outbound connections."""
|
|
def __init__(self, address):
|
|
Socks5.__init__(self, address=address)
|
|
|
|
def state_auth_done(self):
|
|
"""Request connection to be made"""
|
|
# Now we can request the actual connection
|
|
self.append_write_buf(struct.pack('BBB', 0x05, 0x01, 0x00))
|
|
# If the given destination address is an IP address, we'll
|
|
# use the IPv4 address request even if remote resolving was specified.
|
|
try:
|
|
self.ipaddr = socket.inet_aton(self.destination[0])
|
|
self.append_write_buf(chr(0x01).encode() + self.ipaddr)
|
|
except socket.error:
|
|
# Well it's not an IP number, so it's probably a DNS name.
|
|
if Proxy._remote_dns: # pylint: disable=protected-access
|
|
# Resolve remotely
|
|
self.ipaddr = None
|
|
self.append_write_buf(chr(0x03).encode() +
|
|
chr(len(self.destination[0])).encode() +
|
|
self.destination[0])
|
|
else:
|
|
# Resolve locally
|
|
self.ipaddr = socket.inet_aton(socket.gethostbyname(self.destination[0]))
|
|
self.append_write_buf(chr(0x01).encode() + self.ipaddr)
|
|
self.append_write_buf(struct.pack(">H", self.destination[1]))
|
|
self.set_state("pre_connect", length=0, expectBytes=4)
|
|
return True
|
|
|
|
def state_pre_connect(self):
|
|
"""Tell socks5 to initiate a connection"""
|
|
try:
|
|
return Socks5.state_pre_connect(self)
|
|
except Socks5Error as e:
|
|
self.close_reason = e.message
|
|
self.set_state("close")
|
|
|
|
|
|
class Socks5Resolver(Socks5):
|
|
"""DNS resolver class using socks5"""
|
|
def __init__(self, host):
|
|
self.host = host
|
|
self.port = 8444
|
|
Socks5.__init__(self, address=(self.host, self.port))
|
|
|
|
def state_auth_done(self):
|
|
"""Perform resolving"""
|
|
# Now we can request the actual connection
|
|
self.append_write_buf(struct.pack('BBB', 0x05, 0xF0, 0x00))
|
|
self.append_write_buf(chr(0x03).encode() + chr(len(self.host)).encode() + str(self.host))
|
|
self.append_write_buf(struct.pack(">H", self.port))
|
|
self.set_state("pre_connect", length=0, expectBytes=4)
|
|
return True
|
|
|
|
def resolved(self):
|
|
"""
|
|
Resolving is done, process the return value. To use this within PyBitmessage, a callback needs to be
|
|
implemented which hasn't been done yet.
|
|
"""
|
|
print "Resolved %s as %s" % (self.host, self.proxy_sock_name())
|