fix compat with py3
[rofl0r-nat-tunnel.git] / rocksock.py
blob2a97ba0fc77c06568595fc80cd4f49f17196ec7f
1 import socket, ssl, select, copy, errno
3 # rs_proxyType
4 RS_PT_NONE = 0
5 RS_PT_SOCKS4 = 1
6 RS_PT_SOCKS5 = 2
7 RS_PT_HTTP = 3
9 # rs_errorType
10 RS_ET_OWN = 0 # rocksock-specific error
11 RS_ET_SYS = 1 # system error with errno
12 RS_ET_GAI = 2 # dns resolution subsystem error
13 RS_ET_SSL = 3 # ssl subsystem error
15 # rs_error
16 RS_E_NO_ERROR = 0
17 RS_E_NULL = 1
18 RS_E_EXCEED_PROXY_LIMIT = 2
19 RS_E_NO_SSL = 3
20 RS_E_NO_SOCKET = 4
21 RS_E_HIT_TIMEOUT = 5
22 RS_E_OUT_OF_BUFFER = 6
23 RS_E_SSL_GENERIC = 7
24 RS_E_SOCKS4_NOAUTH = 8
25 RS_E_SOCKS5_AUTH_EXCEEDSIZE = 9
26 RS_E_SOCKS4_NO_IP6 = 10
27 RS_E_PROXY_UNEXPECTED_RESPONSE = 11
28 RS_E_TARGETPROXY_CONNECT_FAILED = 12
29 RS_E_PROXY_AUTH_FAILED = 13
30 RS_E_HIT_READTIMEOUT = 14
31 RS_E_HIT_WRITETIMEOUT = 15
32 RS_E_HIT_CONNECTTIMEOUT = 16
33 RS_E_PROXY_GENERAL_FAILURE = 17
34 RS_E_TARGET_NET_UNREACHABLE = 18
35 RS_E_TARGETPROXY_NET_UNREACHABLE = 18
36 RS_E_TARGET_HOST_UNREACHABLE = 19
37 RS_E_TARGETPROXY_HOST_UNREACHABLE = 19
38 RS_E_TARGET_CONN_REFUSED = 20
39 RS_E_TARGETPROXY_CONN_REFUSED = 20
40 RS_E_TARGET_TTL_EXPIRED = 21
41 RS_E_TARGETPROXY_TTL_EXPIRED = 21
42 RS_E_PROXY_COMMAND_NOT_SUPPORTED = 22
43 RS_E_PROXY_ADDRESSTYPE_NOT_SUPPORTED = 23
44 RS_E_REMOTE_DISCONNECTED = 24
45 RS_E_NO_PROXYSTORAGE = 25
46 RS_E_HOSTNAME_TOO_LONG = 26
47 RS_E_INVALID_PROXY_URL = 27
50 class RocksockException(Exception):
51 def __init__(self, error, failedproxy=None, errortype=RS_ET_OWN, *args, **kwargs):
52 Exception.__init__(self,*args,**kwargs)
53 self.error = error
54 self.errortype = errortype
55 self.failedproxy = failedproxy
57 def get_failedproxy(self):
58 return self.failedproxy
60 def get_error(self):
61 return self.error
63 def get_errortype(self):
64 return self.errortype
66 def reraise(self):
67 import sys
68 ei = sys.exc_info()
69 raise(ei[0], ei[1], ei[2])
70 # import traceback, sys
71 # traceback.print_exc(file=sys.stderr)
72 # raise(self)
74 def get_errormessage(self):
75 errordict = {
76 RS_E_NO_ERROR : "no error",
77 RS_E_NULL: "NULL pointer passed",
78 RS_E_EXCEED_PROXY_LIMIT: "exceeding maximum number of proxies",
79 RS_E_NO_SSL: "can not establish SSL connection, since library was not compiled with USE_SSL define",
80 RS_E_NO_SOCKET: "socket is not set up, maybe you should call connect first",
81 RS_E_HIT_TIMEOUT: "timeout reached on operation",
82 RS_E_OUT_OF_BUFFER: "supplied buffer is too small",
83 RS_E_SSL_GENERIC: "generic SSL error", # the C version uses this error when the SSL library does not report any specific error, otherwise errortype SSL will be set and the SSL errorcode be used
84 RS_E_SOCKS4_NOAUTH:"SOCKS4 authentication not implemented",
85 RS_E_SOCKS5_AUTH_EXCEEDSIZE: "maximum length for SOCKS5 servername/password/username is 255",
86 RS_E_SOCKS4_NO_IP6: "SOCKS4 is not compatible with IPv6",
87 RS_E_PROXY_UNEXPECTED_RESPONSE: "the proxy sent an unexpected response",
88 RS_E_TARGETPROXY_CONNECT_FAILED: "could not connect to target proxy",
89 RS_E_PROXY_AUTH_FAILED: "proxy authentication failed or authd not enabled",
90 RS_E_HIT_READTIMEOUT : "timeout reached on read operation",
91 RS_E_HIT_WRITETIMEOUT : "timeout reached on write operation",
92 RS_E_HIT_CONNECTTIMEOUT : "timeout reached on connect operation",
93 RS_E_PROXY_GENERAL_FAILURE : "proxy general failure",
94 RS_E_TARGETPROXY_NET_UNREACHABLE : "proxy-target: net unreachable",
95 RS_E_TARGETPROXY_HOST_UNREACHABLE : "proxy-target: host unreachable",
96 RS_E_TARGETPROXY_CONN_REFUSED : "proxy-target: connection refused",
97 RS_E_TARGETPROXY_TTL_EXPIRED : "proxy-target: TTL expired",
98 RS_E_PROXY_COMMAND_NOT_SUPPORTED : "proxy: command not supported",
99 RS_E_PROXY_ADDRESSTYPE_NOT_SUPPORTED : "proxy: addresstype not supported",
100 RS_E_REMOTE_DISCONNECTED : "remote socket closed connection",
101 RS_E_NO_PROXYSTORAGE : "no proxy storage assigned",
102 RS_E_HOSTNAME_TOO_LONG : "hostname exceeds 255 chars",
103 RS_E_INVALID_PROXY_URL : "invalid proxy URL string"
105 if self.errortype == RS_ET_SYS:
106 if self.error in errno.errorcode:
107 msg = "ERRNO: " + errno.errorcode[self.error]
108 else:
109 msg = "ERRNO: invalid errno: " + str(self.error)
110 elif self.errortype == RS_ET_GAI:
111 msg = "GAI: " + self.failedproxy
112 elif self.errortype == RS_ET_SSL:
113 msg = errordict[self.error]
114 if self.error == RS_E_SSL_GENERIC and self.failedproxy != None:
115 msg += ': ' + self.failedproxy #failedproxy is repurposed for SSL exceptions
116 else: #RS_ET_OWN
117 msg = errordict[self.error] + " (proxy %d)"%self.failedproxy
118 return msg
121 class RocksockHostinfo():
122 def __init__(self, host, port):
123 if port < 0 or port > 65535:
124 raise(RocksockException(RS_E_INVALID_PROXY_URL, failedproxy=-1))
125 self.host = host
126 self.port = port
128 def RocksockHostinfoFromString(s):
129 host, port = s.split(':')
130 return RocksockHostinfo(host, port)
132 def isnumericipv4(ip):
133 try:
134 a,b,c,d = ip.split('.')
135 if int(a) < 256 and int(b) < 256 and int(c) < 256 and int(d) < 256:
136 return True
137 return False
138 except:
139 return False
141 def resolve(hostinfo, want_v4=True):
142 if isnumericipv4(hostinfo.host):
143 return socket.AF_INET, (hostinfo.host, hostinfo.port)
144 try:
145 for res in socket.getaddrinfo(hostinfo.host, hostinfo.port, \
146 socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
147 af, socktype, proto, canonname, sa = res
148 if want_v4 and af != socket.AF_INET: continue
149 if af != socket.AF_INET and af != socket.AF_INET6: continue
150 else: return af, sa
152 except socket.gaierror as e:
153 eno, str = e.args
154 raise(RocksockException(eno, str, errortype=RS_ET_GAI))
156 return None, None
159 class RocksockProxy():
160 def __init__(self, host, port, type, username = None, password=None, **kwargs):
161 typemap = { 'none' : RS_PT_NONE,
162 'socks4' : RS_PT_SOCKS4,
163 'socks5' : RS_PT_SOCKS5,
164 'http' : RS_PT_HTTP }
165 self.type = typemap[type] if type in typemap else type
166 if not self.type in [RS_PT_NONE, RS_PT_SOCKS4, RS_PT_SOCKS5, RS_PT_HTTP]:
167 raise(ValueError('Invalid proxy type'))
168 self.username = username
169 self.password = password
170 self.hostinfo = RocksockHostinfo(host, port)
172 def RocksockProxyFromURL(url):
173 # valid URL: socks5://[user:pass@]hostname:port
174 x = url.find('://')
175 if x == -1: return None
176 t = url[:x]
177 url = url[x+len('://'):]
178 x = url.rfind(':')
179 if x == -1: return None # port is obligatory
180 port = int(url[x+len(':'):]) #TODO: catch exception when port is non-numeric
181 url = url[:x]
182 x = url.rfind('@')
183 if x != -1:
184 u, p = url[:x].split(':')
185 url = url[x+len('@'):]
186 else:
187 u, p = (None, None)
188 return RocksockProxy(host=url, port=port, type=t, username=u, password=p)
191 class Rocksock():
192 def __init__(self, host=None, port=0, verifycert=False, timeout=0, proxies=None, **kwargs):
193 if 'ssl' in kwargs and kwargs['ssl'] == True:
194 self.sslcontext = ssl.create_default_context()
195 self.sslcontext.check_hostname = False
196 if not verifycert: self.sslcontext.verify_mode = ssl.CERT_NONE
197 else:
198 self.sslcontext = None
199 self.proxychain = []
200 if proxies is not None:
201 for p in proxies:
202 if isinstance(p, basestring):
203 self.proxychain.append(RocksockProxyFromURL(p))
204 else:
205 self.proxychain.append(p)
206 target = RocksockProxy(host, port, RS_PT_NONE)
207 self.proxychain.append(target)
208 self.sock = None
209 self.timeout = timeout
211 def _translate_socket_error(self, e, pnum):
212 fp = self._failed_proxy(pnum)
213 if e.errno == errno.ECONNREFUSED:
214 return RocksockException(RS_E_TARGET_CONN_REFUSED, failedproxy=fp)
215 return RocksockException(e.errno, errortype=RS_ET_SYS, failedproxy=fp)
217 def _failed_proxy(self, pnum):
218 if pnum < 0: return -1
219 if pnum >= len(self.proxychain)-1: return -1
220 return pnum
222 def connect(self):
224 af, sa = resolve(self.proxychain[0].hostinfo, True)
225 try:
226 x = af+1
227 except TypeError:
228 raise(RocksockException(-3, "unexpected problem resolving DNS, try again", failedproxy=self._failed_proxy(0), errortype=RS_ET_GAI))
229 # print("GOT A WEIRD AF")
230 # print(af)
231 # raise(RocksockException(-6666, af, errortype=RS_ET_GAI))
233 self.sock = socket.socket(af, socket.SOCK_STREAM)
234 self.sock.settimeout(None if self.timeout == 0 else self.timeout)
235 try:
236 self.sock.connect((sa[0], sa[1]))
237 except socket.timeout:
238 raise(RocksockException(RS_E_HIT_TIMEOUT, failedproxy=self._failed_proxy(0)))
239 except socket.error as e:
240 raise(self._translate_socket_error(e, 0))
242 for pnum in range(1, len(self.proxychain)):
243 curr = self.proxychain[pnum]
244 prev = self.proxychain[pnum-1]
245 self._connect_step(pnum)
247 if self.sslcontext:
248 try:
249 self.sock = self.sslcontext.wrap_socket(self.sock, server_hostname=self.proxychain[len(self.proxychain)-1].hostinfo.host)
250 except ssl.SSLError as e:
251 reason = self._get_ssl_exception_reason(e)
252 #if hasattr(e, 'library'): subsystem = e.library
253 raise(RocksockException(RS_E_SSL_GENERIC, failedproxy=reason, errortype=RS_ET_SSL))
254 except socket.error as e:
255 raise(self._translate_socket_error(e, -1))
256 except Exception as e:
257 raise(e)
259 while True:
260 try:
261 self.sock.do_handshake()
262 break
263 except ssl.SSLWantReadError:
264 select.select([self.sock], [], [])
265 except ssl.SSLWantWriteError:
266 select.select([], [self.sock], [])
270 def disconnect(self):
271 if self.sock is None: return
272 try:
273 self.sock.shutdown(socket.SHUT_RDWR)
274 except socket.error:
275 pass
276 self.sock.close()
277 self.sock = None
279 def canread(self):
280 return select.select([self.sock], [], [], 0)[0]
282 def send(self, buf, pnum=-1):
283 if self.sock is None:
284 raise(RocksockException(RS_E_NO_SOCKET, failedproxy=self._failed_proxy(pnum)))
285 try:
286 return self.sock.sendall(buf)
287 except socket.error as e:
288 raise(self._translate_socket_error(e, pnum))
290 def _get_ssl_exception_reason(self, e):
291 s = ''
292 if hasattr(e, 'reason'): s = e.reason
293 elif hasattr(e, 'message'): s = e.message
294 elif hasattr(e, 'args'): s = e.args[0]
295 return s
297 def recv(self, count=-1, pnum=-1):
298 data = b''
299 while count:
300 try:
301 n = count if count != -1 else 4096
302 if n >= 1024*1024: n = 1024*1024
303 chunk = self.sock.recv(n)
304 except socket.timeout:
305 raise(RocksockException(RS_E_HIT_TIMEOUT, failedproxy=self._failed_proxy(pnum)))
306 except socket.error as e:
307 raise(self._translate_socket_error(e, pnum))
308 except ssl.SSLError as e:
309 s = self._get_ssl_exception_reason(e)
310 if s == 'The read operation timed out':
311 raise(RocksockException(RS_E_HIT_READTIMEOUT, failedproxy=self._failed_proxy(pnum)))
312 else:
313 raise(RocksockException(RS_E_SSL_GENERIC, failedproxy=s, errortype=RS_ET_SSL))
314 if len(chunk) == 0:
315 raise(RocksockException(RS_E_REMOTE_DISCONNECTED, failedproxy=self._failed_proxy(pnum)))
316 data += chunk
317 if count == -1: break
318 else: count -= len(chunk)
319 return data
321 def recvline(self):
322 s = b''
323 c = b'\0'
324 while c != b'\n':
325 c = self.recv(1)
326 if c == '': return s
327 s += c
328 return s
330 def recvuntil(self, until):
331 s = self.recv(len(until))
332 endc = until[-1:]
333 while not (s[-1:] == endc and s.endswith(until)):
334 s += self.recv(1)
335 return s
337 def _ip_to_int(self, ip):
338 a,b,c,d = ip.split('.')
339 h = "0x%.2X%.2X%.2X%.2X"%(int(a),int(b),int(c),int(d))
340 return int(h, 16)
342 def _ip_to_bytes(self, ip):
343 ip = self._ip_to_int(ip)
344 a = (ip & 0xff000000) >> 24
345 b = (ip & 0x00ff0000) >> 16
346 c = (ip & 0x0000ff00) >> 8
347 d = (ip & 0x000000ff) >> 0
348 return chr(a) + chr(b) + chr(c) + chr(d)
350 def _setup_socks4_header(self, v4a, dest):
351 buf = '\x04\x01'
352 buf += chr(dest.hostinfo.port / 256)
353 buf += chr(dest.hostinfo.port % 256)
354 if v4a:
355 buf += '\0\0\0\x01'
356 else:
357 af, sa = resolve(dest.hostinfo, True)
358 if af != socket.AF_INET: raise(RocksockException(RS_E_SOCKS4_NO_IP6, failedproxy=-1))
359 buf += self._ip_to_bytes(sa[0])
360 buf += '\0'
361 if v4a: buf += dest.hostinfo.host + '\0'
362 return buf
364 def _connect_socks4(self, header, pnum):
365 self.send(header)
366 res = self.recv(8, pnum=pnum)
367 if len(res) < 8 or ord(res[0]) != 0:
368 raise(RocksockException(RS_E_PROXY_UNEXPECTED_RESPONSE, failedproxy=self._failed_proxy(pnum)))
369 ch = ord(res[1])
370 if ch == 0x5a:
371 pass
372 elif ch == 0x5b:
373 raise(RocksockException(RS_E_TARGETPROXY_CONNECT_FAILED, failedproxy=self._failed_proxy(pnum)))
374 elif ch == 0x5c or ch == 0x5d:
375 return RocksockException(RS_E_PROXY_AUTH_FAILED, failedproxy=self._failed_proxy(pnum))
376 else:
377 raise(RocksockException(RS_E_PROXY_UNEXPECTED_RESPONSE, failedproxy=self._failed_proxy(pnum)))
379 def _setup_socks5_header(self, proxy):
380 buf = '\x05'
381 if proxy.username and proxy.password:
382 buf += '\x02\x00\x02'
383 else:
384 buf += '\x01\x00'
385 return buf
387 def _connect_socks5(self, header, pnum):
388 self.send(header)
389 res = self.recv(2, pnum=pnum)
390 if len(res) != 2 or res[0] != '\x05':
391 raise(RocksockException(RS_E_PROXY_UNEXPECTED_RESPONSE, failedproxy=self._failed_proxy(pnum)))
392 if res[1] == '\xff':
393 raise(RocksockException(RS_E_PROXY_AUTH_FAILED, failedproxy=self._failed_proxy(pnum)))
395 if ord(res[1]) == 2:
396 px = self.proxychain[pnum-1]
397 if px.username and px.password:
398 pkt = '\x01%c%s%c%s'%(len(px.username),px.username,len(px.password),px.password)
399 self.send(pkt)
400 res = self.recv(2, pnum=pnum)
401 if len(res) < 2 or res[1] != '\0':
402 raise(RocksockException(RS_E_PROXY_AUTH_FAILED, failedproxy=self._failed_proxy(pnum)))
403 else: raise(RocksockException(RS_E_PROXY_AUTH_FAILED, failedproxy=self._failed_proxy(pnum)))
404 dst = self.proxychain[pnum]
405 numeric = isnumericipv4(dst.hostinfo.host)
406 if numeric:
407 dstaddr = self._ip_to_bytes(dst.hostinfo.host)
408 else:
409 dstaddr = chr(len(dst.hostinfo.host)) + dst.hostinfo.host
411 pkt = '\x05\x01\x00%c%s%c%c'% (1 if numeric else 3, dstaddr, dst.hostinfo.port / 256, dst.hostinfo.port % 256)
412 self.send(pkt)
413 res = self.recv(pnum=pnum)
414 if len(res) < 2 or res[0] != '\x05':
415 raise(RocksockException(RS_E_PROXY_UNEXPECTED_RESPONSE, failedproxy=self._failed_proxy(pnum)))
416 ch = ord(res[1])
417 if ch == 0: pass
418 elif ch == 1: raise(RocksockException(RS_E_PROXY_GENERAL_FAILURE, failedproxy=self._failed_proxy(pnum)))
419 elif ch == 2: raise(RocksockException(RS_E_PROXY_AUTH_FAILED, failedproxy=self._failed_proxy(pnum)))
420 elif ch == 3: raise(RocksockException(RS_E_TARGETPROXY_NET_UNREACHABLE, failedproxy=self._failed_proxy(pnum)))
421 elif ch == 4: raise(RocksockException(RS_E_TARGETPROXY_HOST_UNREACHABLE, failedproxy=self._failed_proxy(pnum)))
422 elif ch == 5: raise(RocksockException(RS_E_TARGETPROXY_CONN_REFUSED, failedproxy=self._failed_proxy(pnum)))
423 elif ch == 6: raise(RocksockException(RS_E_TARGETPROXY_TTL_EXPIRED, failedproxy=self._failed_proxy(pnum)))
424 elif ch == 7: raise(RocksockException(RS_E_PROXY_COMMAND_NOT_SUPPORTED, failedproxy=self._failed_proxy(pnum)))
425 elif ch == 8: raise(RocksockException(RS_E_PROXY_ADDRESSTYPE_NOT_SUPPORTED, failedproxy=self._failed_proxy(pnum)))
426 else: raise(RocksockException(RS_E_PROXY_UNEXPECTED_RESPONSE, failedproxy=self._failed_proxy(pnum)))
429 def _connect_step(self, pnum):
430 prev = self.proxychain[pnum -1]
431 curr = self.proxychain[pnum]
432 if prev.type == RS_PT_SOCKS4:
433 s4a = self._setup_socks4_header(True, curr)
434 try:
435 self._connect_socks4(s4a, pnum)
436 except RocksockException as e:
437 if e.get_error() == RS_E_TARGETPROXY_CONNECT_FAILED:
438 s4 = self._setup_socks4_header(False, curr)
439 self._connect_socks4(s4a, pnum)
440 else: raise(e)
441 elif prev.type == RS_PT_SOCKS5:
442 s5 = self._setup_socks5_header(prev)
443 self._connect_socks5(s5, pnum)
444 elif prev.type == RS_PT_HTTP:
445 dest = self.proxychain[pnum]
446 self.send("CONNECT %s:%d HTTP/1.1\r\n\r\n"%(dest.hostinfo.host, dest.hostinfo.port))
447 resp = self.recv(pnum=pnum)
448 if len(resp) <12:
449 raise(RocksockException(RS_E_PROXY_UNEXPECTED_RESPONSE, failedproxy=self._failed_proxy(pnum)))
450 if resp[9] != '2':
451 raise(RocksockException(RS_E_TARGETPROXY_CONNECT_FAILED, failedproxy=self._failed_proxy(pnum)))
454 if __name__ == '__main__':
455 proxies = [
456 # RocksockProxyFromURL("socks5://foo:bar@localhost:1080"),
457 # RocksockProxyFromURL("socks5://10.0.0.3:1080"),
458 RocksockProxyFromURL("socks5://127.0.0.1:31339"),
460 proxies = None
461 #rs = Rocksock(host='googleff242342423f.com', port=443, ssl=True, proxies=proxies)
462 rs = Rocksock(host='google.com', port=80, ssl=False, proxies=proxies)
463 try:
464 rs.connect()
465 except RocksockException as e:
466 print(e.get_errormessage())
467 e.reraise()
468 rs.send('GET / HTTP/1.0\r\n\r\n')
469 print(rs.recvline())
470 rs.disconnect()
471 rs.connect()
472 rs.send('GET / HTTP/1.0\r\n\r\n')
473 print(rs.recvline())