Skip to content

Commit 2cd807b

Browse files
committed
Add contrib.socks
1 parent dbde4c2 commit 2cd807b

File tree

2 files changed

+197
-0
lines changed

2 files changed

+197
-0
lines changed

trip/contrib/__init__.py

Whitespace-only changes.

trip/contrib/socks.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import socket, struct
2+
from functools import partial
3+
4+
from tornado import gen
5+
from tornado.concurrent import run_on_executor
6+
from tornado.netutil import ExecutorResolver as _Resolver
7+
from tornado.iostream import BaseIOStream, IOStream
8+
9+
SOCKS4_ERRORS = {
10+
0x5B: "Request rejected or failed",
11+
0x5C: ("Request rejected because SOCKS server cannot connect to identd on"
12+
" the client"),
13+
0x5D: ("Request rejected because the client program and identd report"
14+
" different user-ids")
15+
}
16+
17+
SOCKS5_ERRORS = {
18+
0x01: "General SOCKS server failure",
19+
0x02: "Connection not allowed by ruleset",
20+
0x03: "Network unreachable",
21+
0x04: "Host unreachable",
22+
0x05: "Connection refused",
23+
0x06: "TTL expired",
24+
0x07: "Command not supported, or protocol error",
25+
0x08: "Address type not supported"
26+
}
27+
28+
GeneralProxyError = SOCKS5AuthError = SOCKS5Error = Exception
29+
30+
31+
class SockIOStream(IOStream):
32+
def __init__(self, proxy_settings):
33+
BaseIOStream.__init__(self)
34+
self.proxy_settings = proxy_settings
35+
self.socket = None
36+
self.resolver = Resolver()
37+
self.peername = self.sockname = None
38+
39+
@gen.coroutine
40+
def connect(self, host, port):
41+
err = None
42+
proxy_type, rdns, ph, pp, username, password = self.proxy_settings
43+
rl = yield self.resolver.resolve(ph, pp, 0)
44+
for r in rl:
45+
family, socket_type, proto, canonname, sa = r
46+
sock = None
47+
try:
48+
if socket_type not in (socket.SOCK_STREAM, socket.SOCK_DGRAM):
49+
msg = 'Socket type must be stream or datagram, not {!r}'
50+
raise ValueError(msg.format(type))
51+
elif socket_type == socket.SOCK_DGRAM:
52+
raise ValueError('SOCK_DGRAM is not supported for now.')
53+
self.socket = socket.socket(family, socket_type, proto)
54+
self.socket.setblocking(False)
55+
super(SockIOStream, self).connect((ph, pp))
56+
if proxy_type == 2:
57+
yield self._negotiate_socks5(host, port)
58+
else:
59+
raise socket.error('Unknown proxy_type')
60+
raise gen.Return(self)
61+
except socket.error as e:
62+
err = e
63+
if self.socket:
64+
self.socket.close()
65+
self.socket = None
66+
67+
if err:
68+
raise err
69+
raise socket.error("gai returned empty list.")
70+
71+
def _negotiate_socks5(self, host, port):
72+
"""Negotiates a stream connection through a SOCKS5 server."""
73+
connect_cmd = b"\x01"
74+
# self.peername, self.sockname
75+
return self._socks5_request(self, connect_cmd, (host, port))
76+
77+
@gen.coroutine
78+
def _socks5_request(self, conn, cmd, dst):
79+
proxy_type, rdns, host, port, username, password = self.proxy_settings
80+
if username and password:
81+
yield self.write(b"\x05\x02\x00\x02")
82+
else:
83+
yield self.write(b"\x05\x01\x00")
84+
85+
chosen_auth = yield self.read_bytes(2)
86+
87+
if chosen_auth[0:1] != b"\x05":
88+
raise GeneralProxyError(
89+
"SOCKS5 proxy server sent invalid data")
90+
91+
if chosen_auth[1:2] == b"\x02":
92+
yield self.write(b"\x01" + chr(len(username)).encode()
93+
+ username
94+
+ chr(len(password)).encode()
95+
+ password)
96+
auth_status = yield self.read_bytes(2)
97+
if auth_status[0:1] != b"\x01":
98+
raise GeneralProxyError(
99+
"SOCKS5 proxy server sent invalid data")
100+
if auth_status[1:2] != b"\x00":
101+
raise SOCKS5AuthError("SOCKS5 authentication failed")
102+
elif chosen_auth[1:2] != b"\x00":
103+
if chosen_auth[1:2] == b"\xFF":
104+
raise SOCKS5AuthError(
105+
"All offered SOCKS5 authentication methods were"
106+
" rejected")
107+
else:
108+
raise GeneralProxyError(
109+
"SOCKS5 proxy server sent invalid data")
110+
111+
yield self.write(b"\x05" + cmd + b"\x00")
112+
resolved = yield self._write_socks5_address(dst)
113+
114+
resp = yield self.read_bytes(3)
115+
if resp[0:1] != b"\x05":
116+
raise GeneralProxyError(
117+
"SOCKS5 proxy server sent invalid data")
118+
119+
status = ord(resp[1:2])
120+
if status != 0x00:
121+
# Connection failed: server returned an error
122+
error = SOCKS5_ERRORS.get(status, "Unknown error")
123+
raise SOCKS5Error("{0:#04x}: {1}".format(status, error))
124+
125+
bnd = yield self._read_socks5_address()
126+
127+
raise gen.Return((resolved, bnd))
128+
129+
@gen.coroutine
130+
def _write_socks5_address(self, addr):
131+
host, port = addr
132+
proxy_type, rdns, _, _, username, password = self.proxy_settings
133+
family_to_byte = {socket.AF_INET: b"\x01", socket.AF_INET6: b"\x04"}
134+
135+
for family in (socket.AF_INET, socket.AF_INET6):
136+
try:
137+
addr_bytes = socket.inet_pton(family, host)
138+
yield self.write(family_to_byte[family] + addr_bytes)
139+
host = socket.inet_ntop(family, addr_bytes)
140+
yield self.write(struct.pack(">H", port))
141+
raise gen.Return((host, port))
142+
except socket.error:
143+
continue
144+
145+
if rdns:
146+
host_bytes = host.encode("idna")
147+
yield self.write(b"\x03" + chr(len(host_bytes)).encode() + host_bytes)
148+
else:
149+
addresses = yield self.resolver.getaddrinfo(host, port, socket.AF_UNSPEC,
150+
socket.SOCK_STREAM,
151+
socket.IPPROTO_TCP,
152+
socket.AI_ADDRCONFIG)
153+
154+
target_addr = addresses[0]
155+
family = target_addr[0]
156+
host = target_addr[4][0]
157+
158+
addr_bytes = socket.inet_pton(family, host)
159+
yield self.write(family_to_byte[family] + addr_bytes)
160+
host = socket.inet_ntop(family, addr_bytes)
161+
yield self.write(struct.pack(">H", port))
162+
raise gen.Return((host, port))
163+
164+
@gen.coroutine
165+
def _read_socks5_address(self):
166+
atyp = yield self.read_bytes(1)
167+
if atyp == b"\x01":
168+
data = yield self.read_bytes(4)
169+
addr = socket.inet_ntoa(data)
170+
elif atyp == b"\x03":
171+
length = yield self.read_bytes(1)
172+
addr = yield self.read_bytes(length)
173+
elif atyp == b"\x04":
174+
data = yield self.read_bytes(16)
175+
addr = socket.inet_ntop(socket.AF_INET6, data)
176+
else:
177+
raise GeneralProxyError("SOCKS5 proxy server sent invalid data")
178+
179+
data = yield self.read_bytes(2)
180+
port = struct.unpack(">H", data)[0]
181+
raise gen.Return((addr, port))
182+
183+
184+
class Resolver(_Resolver):
185+
@run_on_executor
186+
def resolve(self, host, port, family=socket.AF_UNSPEC):
187+
return socket.getaddrinfo(host, port, family, socket.SOCK_STREAM)
188+
189+
def __getattr__(self, name):
190+
if name in ('getaddrinfo',):
191+
return partial(self._execute, getattr(socket, name))
192+
else:
193+
raise AttributeError('%s object has no attribute %s' % (self.__class__.__name__, name))
194+
195+
@run_on_executor
196+
def _execute(self, fn, *args, **kwargs):
197+
return fn(*args, **kwargs)

0 commit comments

Comments
 (0)