1#!/usr/bin/env python 2"""SocksiPy - Python SOCKS module. 3 4Copyright 2006 Dan-Haim. All rights reserved. 5 6Redistribution and use in source and binary forms, with or without 7modification, are permitted provided that the following conditions are met: 81. Redistributions of source code must retain the above copyright notice, this 9 list of conditions and the following disclaimer. 102. Redistributions in binary form must reproduce the above copyright notice, 11 this list of conditions and the following disclaimer in the documentation 12 and/or other materials provided with the distribution. 133. Neither the name of Dan Haim nor the names of his contributors may be used 14 to endorse or promote products derived from this software without specific 15 prior written permission. 16 17THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED 18WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 20EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA 23OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 25OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 27 28This module provides a standard socket-like interface for Python 29for tunneling connections through SOCKS proxies. 30 31=============================================================================== 32 33Minor modifications made by Christopher Gilbert (http://motomastyle.com/) 34for use in PyLoris (http://pyloris.sourceforge.net/) 35 36Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) 37mainly to merge bug fixes found in Sourceforge 38 39Modifications made by Anorov (https://github.com/Anorov) 40-Forked and renamed to PySocks 41-Fixed issue with HTTP proxy failure checking (same bug that was in the 42 old ___recvall() method) 43-Included SocksiPyHandler (sockshandler.py), to be used as a urllib2 handler, 44 courtesy of e000 (https://github.com/e000): 45 https://gist.github.com/869791#file_socksipyhandler.py 46-Re-styled code to make it readable 47 -Aliased PROXY_TYPE_SOCKS5 -> SOCKS5 etc. 48 -Improved exception handling and output 49 -Removed irritating use of sequence indexes, replaced with tuple unpacked 50 variables 51 -Fixed up Python 3 bytestring handling - chr(0x03).encode() -> b"\x03" 52 -Other general fixes 53-Added clarification that the HTTP proxy connection method only supports 54 CONNECT-style tunneling HTTP proxies 55-Various small bug fixes 56""" 57 58from base64 import b64encode 59from collections import Callable 60from errno import EOPNOTSUPP, EINVAL, EAGAIN 61import functools 62from io import BytesIO 63import logging 64import os 65from os import SEEK_CUR 66import socket 67import struct 68import sys 69 70__version__ = "1.6.7" 71 72 73if os.name == "nt" and sys.version_info < (3, 0): 74 try: 75 import win_inet_pton 76 except ImportError: 77 raise ImportError( 78 "To run PySocks on Windows you must install win_inet_pton") 79 80log = logging.getLogger(__name__) 81 82PROXY_TYPE_SOCKS4 = SOCKS4 = 1 83PROXY_TYPE_SOCKS5 = SOCKS5 = 2 84PROXY_TYPE_HTTP = HTTP = 3 85 86PROXY_TYPES = {"SOCKS4": SOCKS4, "SOCKS5": SOCKS5, "HTTP": HTTP} 87PRINTABLE_PROXY_TYPES = dict(zip(PROXY_TYPES.values(), PROXY_TYPES.keys())) 88 89_orgsocket = _orig_socket = socket.socket 90 91 92def set_self_blocking(function): 93 94 @functools.wraps(function) 95 def wrapper(*args, **kwargs): 96 self = args[0] 97 try: 98 _is_blocking = self.gettimeout() 99 if _is_blocking == 0: 100 self.setblocking(True) 101 return function(*args, **kwargs) 102 except Exception as e: 103 raise 104 finally: 105 # set orgin blocking 106 if _is_blocking == 0: 107 self.setblocking(False) 108 return wrapper 109 110 111class ProxyError(IOError): 112 """Socket_err contains original socket.error exception.""" 113 def __init__(self, msg, socket_err=None): 114 self.msg = msg 115 self.socket_err = socket_err 116 117 if socket_err: 118 self.msg += ": {0}".format(socket_err) 119 120 def __str__(self): 121 return self.msg 122 123 124class GeneralProxyError(ProxyError): 125 pass 126 127 128class ProxyConnectionError(ProxyError): 129 pass 130 131 132class SOCKS5AuthError(ProxyError): 133 pass 134 135 136class SOCKS5Error(ProxyError): 137 pass 138 139 140class SOCKS4Error(ProxyError): 141 pass 142 143 144class HTTPError(ProxyError): 145 pass 146 147SOCKS4_ERRORS = { 148 0x5B: "Request rejected or failed", 149 0x5C: ("Request rejected because SOCKS server cannot connect to identd on" 150 " the client"), 151 0x5D: ("Request rejected because the client program and identd report" 152 " different user-ids") 153} 154 155SOCKS5_ERRORS = { 156 0x01: "General SOCKS server failure", 157 0x02: "Connection not allowed by ruleset", 158 0x03: "Network unreachable", 159 0x04: "Host unreachable", 160 0x05: "Connection refused", 161 0x06: "TTL expired", 162 0x07: "Command not supported, or protocol error", 163 0x08: "Address type not supported" 164} 165 166DEFAULT_PORTS = {SOCKS4: 1080, SOCKS5: 1080, HTTP: 8080} 167 168 169def set_default_proxy(proxy_type=None, addr=None, port=None, rdns=True, 170 username=None, password=None): 171 """Sets a default proxy. 172 173 All further socksocket objects will use the default unless explicitly 174 changed. All parameters are as for socket.set_proxy().""" 175 if hasattr(username, "encode"): 176 username = username.encode() 177 if hasattr(password, "encode"): 178 password = password.encode() 179 socksocket.default_proxy = (proxy_type, addr, port, rdns, 180 username if username else None, 181 password if password else None) 182 183 184def setdefaultproxy(*args, **kwargs): 185 if "proxytype" in kwargs: 186 kwargs["proxy_type"] = kwargs.pop("proxytype") 187 return set_default_proxy(*args, **kwargs) 188 189 190def get_default_proxy(): 191 """Returns the default proxy, set by set_default_proxy.""" 192 return socksocket.default_proxy 193 194getdefaultproxy = get_default_proxy 195 196 197def wrap_module(module): 198 """Attempts to replace a module's socket library with a SOCKS socket. 199 200 Must set a default proxy using set_default_proxy(...) first. This will 201 only work on modules that import socket directly into the namespace; 202 most of the Python Standard Library falls into this category.""" 203 if socksocket.default_proxy: 204 module.socket.socket = socksocket 205 else: 206 raise GeneralProxyError("No default proxy specified") 207 208wrapmodule = wrap_module 209 210 211def create_connection(dest_pair, 212 timeout=None, source_address=None, 213 proxy_type=None, proxy_addr=None, 214 proxy_port=None, proxy_rdns=True, 215 proxy_username=None, proxy_password=None, 216 socket_options=None): 217 """create_connection(dest_pair, *[, timeout], **proxy_args) -> socket object 218 219 Like socket.create_connection(), but connects to proxy 220 before returning the socket object. 221 222 dest_pair - 2-tuple of (IP/hostname, port). 223 **proxy_args - Same args passed to socksocket.set_proxy() if present. 224 timeout - Optional socket timeout value, in seconds. 225 source_address - tuple (host, port) for the socket to bind to as its source 226 address before connecting (only for compatibility) 227 """ 228 # Remove IPv6 brackets on the remote address and proxy address. 229 remote_host, remote_port = dest_pair 230 if remote_host.startswith("["): 231 remote_host = remote_host.strip("[]") 232 if proxy_addr and proxy_addr.startswith("["): 233 proxy_addr = proxy_addr.strip("[]") 234 235 err = None 236 237 # Allow the SOCKS proxy to be on IPv4 or IPv6 addresses. 238 for r in socket.getaddrinfo(proxy_addr, proxy_port, 0, socket.SOCK_STREAM): 239 family, socket_type, proto, canonname, sa = r 240 sock = None 241 try: 242 sock = socksocket(family, socket_type, proto) 243 244 if socket_options: 245 for opt in socket_options: 246 sock.setsockopt(*opt) 247 248 if isinstance(timeout, (int, float)): 249 sock.settimeout(timeout) 250 251 if proxy_type: 252 sock.set_proxy(proxy_type, proxy_addr, proxy_port, proxy_rdns, 253 proxy_username, proxy_password) 254 if source_address: 255 sock.bind(source_address) 256 257 sock.connect((remote_host, remote_port)) 258 return sock 259 260 except (socket.error, ProxyConnectionError) as e: 261 err = e 262 if sock: 263 sock.close() 264 sock = None 265 266 if err: 267 raise err 268 269 raise socket.error("gai returned empty list.") 270 271 272class _BaseSocket(socket.socket): 273 """Allows Python 2 delegated methods such as send() to be overridden.""" 274 def __init__(self, *pos, **kw): 275 _orig_socket.__init__(self, *pos, **kw) 276 277 self._savedmethods = dict() 278 for name in self._savenames: 279 self._savedmethods[name] = getattr(self, name) 280 delattr(self, name) # Allows normal overriding mechanism to work 281 282 _savenames = list() 283 284 285def _makemethod(name): 286 return lambda self, *pos, **kw: self._savedmethods[name](*pos, **kw) 287for name in ("sendto", "send", "recvfrom", "recv"): 288 method = getattr(_BaseSocket, name, None) 289 290 # Determine if the method is not defined the usual way 291 # as a function in the class. 292 # Python 2 uses __slots__, so there are descriptors for each method, 293 # but they are not functions. 294 if not isinstance(method, Callable): 295 _BaseSocket._savenames.append(name) 296 setattr(_BaseSocket, name, _makemethod(name)) 297 298 299class socksocket(_BaseSocket): 300 """socksocket([family[, type[, proto]]]) -> socket object 301 302 Open a SOCKS enabled socket. The parameters are the same as 303 those of the standard socket init. In order for SOCKS to work, 304 you must specify family=AF_INET and proto=0. 305 The "type" argument must be either SOCK_STREAM or SOCK_DGRAM. 306 """ 307 308 default_proxy = None 309 310 def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, 311 proto=0, *args, **kwargs): 312 if type not in (socket.SOCK_STREAM, socket.SOCK_DGRAM): 313 msg = "Socket type must be stream or datagram, not {!r}" 314 raise ValueError(msg.format(type)) 315 316 super(socksocket, self).__init__(family, type, proto, *args, **kwargs) 317 self._proxyconn = None # TCP connection to keep UDP relay alive 318 319 if self.default_proxy: 320 self.proxy = self.default_proxy 321 else: 322 self.proxy = (None, None, None, None, None, None) 323 self.proxy_sockname = None 324 self.proxy_peername = None 325 326 self._timeout = None 327 328 def _readall(self, file, count): 329 """Receive EXACTLY the number of bytes requested from the file object. 330 331 Blocks until the required number of bytes have been received.""" 332 data = b"" 333 while len(data) < count: 334 d = file.read(count - len(data)) 335 if not d: 336 raise GeneralProxyError("Connection closed unexpectedly") 337 data += d 338 return data 339 340 def settimeout(self, timeout): 341 self._timeout = timeout 342 try: 343 # test if we're connected, if so apply timeout 344 peer = self.get_proxy_peername() 345 super(socksocket, self).settimeout(self._timeout) 346 except socket.error: 347 pass 348 349 def gettimeout(self): 350 return self._timeout 351 352 def setblocking(self, v): 353 if v: 354 self.settimeout(None) 355 else: 356 self.settimeout(0.0) 357 358 def set_proxy(self, proxy_type=None, addr=None, port=None, rdns=True, 359 username=None, password=None): 360 """ Sets the proxy to be used. 361 362 proxy_type - The type of the proxy to be used. Three types 363 are supported: PROXY_TYPE_SOCKS4 (including socks4a), 364 PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP 365 addr - The address of the server (IP or DNS). 366 port - The port of the server. Defaults to 1080 for SOCKS 367 servers and 8080 for HTTP proxy servers. 368 rdns - Should DNS queries be performed on the remote side 369 (rather than the local side). The default is True. 370 Note: This has no effect with SOCKS4 servers. 371 username - Username to authenticate with to the server. 372 The default is no authentication. 373 password - Password to authenticate with to the server. 374 Only relevant when username is also provided.""" 375 if hasattr(username, "encode"): 376 username = username.encode() 377 if hasattr(password, "encode"): 378 password = password.encode() 379 self.proxy = (proxy_type, addr, port, rdns, 380 username if username else None, 381 password if password else None) 382 383 def setproxy(self, *args, **kwargs): 384 if "proxytype" in kwargs: 385 kwargs["proxy_type"] = kwargs.pop("proxytype") 386 return self.set_proxy(*args, **kwargs) 387 388 def bind(self, *pos, **kw): 389 """Implements proxy connection for UDP sockets. 390 391 Happens during the bind() phase.""" 392 (proxy_type, proxy_addr, proxy_port, rdns, username, 393 password) = self.proxy 394 if not proxy_type or self.type != socket.SOCK_DGRAM: 395 return _orig_socket.bind(self, *pos, **kw) 396 397 if self._proxyconn: 398 raise socket.error(EINVAL, "Socket already bound to an address") 399 if proxy_type != SOCKS5: 400 msg = "UDP only supported by SOCKS5 proxy type" 401 raise socket.error(EOPNOTSUPP, msg) 402 super(socksocket, self).bind(*pos, **kw) 403 404 # Need to specify actual local port because 405 # some relays drop packets if a port of zero is specified. 406 # Avoid specifying host address in case of NAT though. 407 _, port = self.getsockname() 408 dst = ("0", port) 409 410 self._proxyconn = _orig_socket() 411 proxy = self._proxy_addr() 412 self._proxyconn.connect(proxy) 413 414 UDP_ASSOCIATE = b"\x03" 415 _, relay = self._SOCKS5_request(self._proxyconn, UDP_ASSOCIATE, dst) 416 417 # The relay is most likely on the same host as the SOCKS proxy, 418 # but some proxies return a private IP address (10.x.y.z) 419 host, _ = proxy 420 _, port = relay 421 super(socksocket, self).connect((host, port)) 422 super(socksocket, self).settimeout(self._timeout) 423 self.proxy_sockname = ("0.0.0.0", 0) # Unknown 424 425 def sendto(self, bytes, *args, **kwargs): 426 if self.type != socket.SOCK_DGRAM: 427 return super(socksocket, self).sendto(bytes, *args, **kwargs) 428 if not self._proxyconn: 429 self.bind(("", 0)) 430 431 address = args[-1] 432 flags = args[:-1] 433 434 header = BytesIO() 435 RSV = b"\x00\x00" 436 header.write(RSV) 437 STANDALONE = b"\x00" 438 header.write(STANDALONE) 439 self._write_SOCKS5_address(address, header) 440 441 sent = super(socksocket, self).send(header.getvalue() + bytes, *flags, 442 **kwargs) 443 return sent - header.tell() 444 445 def send(self, bytes, flags=0, **kwargs): 446 if self.type == socket.SOCK_DGRAM: 447 return self.sendto(bytes, flags, self.proxy_peername, **kwargs) 448 else: 449 return super(socksocket, self).send(bytes, flags, **kwargs) 450 451 def recvfrom(self, bufsize, flags=0): 452 if self.type != socket.SOCK_DGRAM: 453 return super(socksocket, self).recvfrom(bufsize, flags) 454 if not self._proxyconn: 455 self.bind(("", 0)) 456 457 buf = BytesIO(super(socksocket, self).recv(bufsize + 1024, flags)) 458 buf.seek(2, SEEK_CUR) 459 frag = buf.read(1) 460 if ord(frag): 461 raise NotImplementedError("Received UDP packet fragment") 462 fromhost, fromport = self._read_SOCKS5_address(buf) 463 464 if self.proxy_peername: 465 peerhost, peerport = self.proxy_peername 466 if fromhost != peerhost or peerport not in (0, fromport): 467 raise socket.error(EAGAIN, "Packet filtered") 468 469 return (buf.read(bufsize), (fromhost, fromport)) 470 471 def recv(self, *pos, **kw): 472 bytes, _ = self.recvfrom(*pos, **kw) 473 return bytes 474 475 def close(self): 476 if self._proxyconn: 477 self._proxyconn.close() 478 return super(socksocket, self).close() 479 480 def get_proxy_sockname(self): 481 """Returns the bound IP address and port number at the proxy.""" 482 return self.proxy_sockname 483 484 getproxysockname = get_proxy_sockname 485 486 def get_proxy_peername(self): 487 """ 488 Returns the IP and port number of the proxy. 489 """ 490 return self.getpeername() 491 492 getproxypeername = get_proxy_peername 493 494 def get_peername(self): 495 """Returns the IP address and port number of the destination machine. 496 497 Note: get_proxy_peername returns the proxy.""" 498 return self.proxy_peername 499 500 getpeername = get_peername 501 502 def _negotiate_SOCKS5(self, *dest_addr): 503 """Negotiates a stream connection through a SOCKS5 server.""" 504 CONNECT = b"\x01" 505 self.proxy_peername, self.proxy_sockname = self._SOCKS5_request( 506 self, CONNECT, dest_addr) 507 508 def _SOCKS5_request(self, conn, cmd, dst): 509 """ 510 Send SOCKS5 request with given command (CMD field) and 511 address (DST field). Returns resolved DST address that was used. 512 """ 513 proxy_type, addr, port, rdns, username, password = self.proxy 514 515 writer = conn.makefile("wb") 516 reader = conn.makefile("rb", 0) # buffering=0 renamed in Python 3 517 try: 518 # First we'll send the authentication packages we support. 519 if username and password: 520 # The username/password details were supplied to the 521 # set_proxy method so we support the USERNAME/PASSWORD 522 # authentication (in addition to the standard none). 523 writer.write(b"\x05\x02\x00\x02") 524 else: 525 # No username/password were entered, therefore we 526 # only support connections with no authentication. 527 writer.write(b"\x05\x01\x00") 528 529 # We'll receive the server's response to determine which 530 # method was selected 531 writer.flush() 532 chosen_auth = self._readall(reader, 2) 533 534 if chosen_auth[0:1] != b"\x05": 535 # Note: string[i:i+1] is used because indexing of a bytestring 536 # via bytestring[i] yields an integer in Python 3 537 raise GeneralProxyError( 538 "SOCKS5 proxy server sent invalid data") 539 540 # Check the chosen authentication method 541 542 if chosen_auth[1:2] == b"\x02": 543 # Okay, we need to perform a basic username/password 544 # authentication. 545 writer.write(b"\x01" + chr(len(username)).encode() 546 + username 547 + chr(len(password)).encode() 548 + password) 549 writer.flush() 550 auth_status = self._readall(reader, 2) 551 if auth_status[0:1] != b"\x01": 552 # Bad response 553 raise GeneralProxyError( 554 "SOCKS5 proxy server sent invalid data") 555 if auth_status[1:2] != b"\x00": 556 # Authentication failed 557 raise SOCKS5AuthError("SOCKS5 authentication failed") 558 559 # Otherwise, authentication succeeded 560 561 # No authentication is required if 0x00 562 elif chosen_auth[1:2] != b"\x00": 563 # Reaching here is always bad 564 if chosen_auth[1:2] == b"\xFF": 565 raise SOCKS5AuthError( 566 "All offered SOCKS5 authentication methods were" 567 " rejected") 568 else: 569 raise GeneralProxyError( 570 "SOCKS5 proxy server sent invalid data") 571 572 # Now we can request the actual connection 573 writer.write(b"\x05" + cmd + b"\x00") 574 resolved = self._write_SOCKS5_address(dst, writer) 575 writer.flush() 576 577 # Get the response 578 resp = self._readall(reader, 3) 579 if resp[0:1] != b"\x05": 580 raise GeneralProxyError( 581 "SOCKS5 proxy server sent invalid data") 582 583 status = ord(resp[1:2]) 584 if status != 0x00: 585 # Connection failed: server returned an error 586 error = SOCKS5_ERRORS.get(status, "Unknown error") 587 raise SOCKS5Error("{0:#04x}: {1}".format(status, error)) 588 589 # Get the bound address/port 590 bnd = self._read_SOCKS5_address(reader) 591 592 super(socksocket, self).settimeout(self._timeout) 593 return (resolved, bnd) 594 finally: 595 reader.close() 596 writer.close() 597 598 def _write_SOCKS5_address(self, addr, file): 599 """ 600 Return the host and port packed for the SOCKS5 protocol, 601 and the resolved address as a tuple object. 602 """ 603 host, port = addr 604 proxy_type, _, _, rdns, username, password = self.proxy 605 family_to_byte = {socket.AF_INET: b"\x01", socket.AF_INET6: b"\x04"} 606 607 # If the given destination address is an IP address, we'll 608 # use the IP address request even if remote resolving was specified. 609 # Detect whether the address is IPv4/6 directly. 610 for family in (socket.AF_INET, socket.AF_INET6): 611 try: 612 addr_bytes = socket.inet_pton(family, host) 613 file.write(family_to_byte[family] + addr_bytes) 614 host = socket.inet_ntop(family, addr_bytes) 615 file.write(struct.pack(">H", port)) 616 return host, port 617 except socket.error: 618 continue 619 620 # Well it's not an IP number, so it's probably a DNS name. 621 if rdns: 622 # Resolve remotely 623 host_bytes = host.encode("idna") 624 file.write(b"\x03" + chr(len(host_bytes)).encode() + host_bytes) 625 else: 626 # Resolve locally 627 addresses = socket.getaddrinfo(host, port, socket.AF_UNSPEC, 628 socket.SOCK_STREAM, 629 socket.IPPROTO_TCP, 630 socket.AI_ADDRCONFIG) 631 # We can't really work out what IP is reachable, so just pick the 632 # first. 633 target_addr = addresses[0] 634 family = target_addr[0] 635 host = target_addr[4][0] 636 637 addr_bytes = socket.inet_pton(family, host) 638 file.write(family_to_byte[family] + addr_bytes) 639 host = socket.inet_ntop(family, addr_bytes) 640 file.write(struct.pack(">H", port)) 641 return host, port 642 643 def _read_SOCKS5_address(self, file): 644 atyp = self._readall(file, 1) 645 if atyp == b"\x01": 646 addr = socket.inet_ntoa(self._readall(file, 4)) 647 elif atyp == b"\x03": 648 length = self._readall(file, 1) 649 addr = self._readall(file, ord(length)) 650 elif atyp == b"\x04": 651 addr = socket.inet_ntop(socket.AF_INET6, self._readall(file, 16)) 652 else: 653 raise GeneralProxyError("SOCKS5 proxy server sent invalid data") 654 655 port = struct.unpack(">H", self._readall(file, 2))[0] 656 return addr, port 657 658 def _negotiate_SOCKS4(self, dest_addr, dest_port): 659 """Negotiates a connection through a SOCKS4 server.""" 660 proxy_type, addr, port, rdns, username, password = self.proxy 661 662 writer = self.makefile("wb") 663 reader = self.makefile("rb", 0) # buffering=0 renamed in Python 3 664 try: 665 # Check if the destination address provided is an IP address 666 remote_resolve = False 667 try: 668 addr_bytes = socket.inet_aton(dest_addr) 669 except socket.error: 670 # It's a DNS name. Check where it should be resolved. 671 if rdns: 672 addr_bytes = b"\x00\x00\x00\x01" 673 remote_resolve = True 674 else: 675 addr_bytes = socket.inet_aton( 676 socket.gethostbyname(dest_addr)) 677 678 # Construct the request packet 679 writer.write(struct.pack(">BBH", 0x04, 0x01, dest_port)) 680 writer.write(addr_bytes) 681 682 # The username parameter is considered userid for SOCKS4 683 if username: 684 writer.write(username) 685 writer.write(b"\x00") 686 687 # DNS name if remote resolving is required 688 # NOTE: This is actually an extension to the SOCKS4 protocol 689 # called SOCKS4A and may not be supported in all cases. 690 if remote_resolve: 691 writer.write(dest_addr.encode("idna") + b"\x00") 692 writer.flush() 693 694 # Get the response from the server 695 resp = self._readall(reader, 8) 696 if resp[0:1] != b"\x00": 697 # Bad data 698 raise GeneralProxyError( 699 "SOCKS4 proxy server sent invalid data") 700 701 status = ord(resp[1:2]) 702 if status != 0x5A: 703 # Connection failed: server returned an error 704 error = SOCKS4_ERRORS.get(status, "Unknown error") 705 raise SOCKS4Error("{0:#04x}: {1}".format(status, error)) 706 707 # Get the bound address/port 708 self.proxy_sockname = (socket.inet_ntoa(resp[4:]), 709 struct.unpack(">H", resp[2:4])[0]) 710 if remote_resolve: 711 self.proxy_peername = socket.inet_ntoa(addr_bytes), dest_port 712 else: 713 self.proxy_peername = dest_addr, dest_port 714 finally: 715 reader.close() 716 writer.close() 717 718 def _negotiate_HTTP(self, dest_addr, dest_port): 719 """Negotiates a connection through an HTTP server. 720 721 NOTE: This currently only supports HTTP CONNECT-style proxies.""" 722 proxy_type, addr, port, rdns, username, password = self.proxy 723 724 # If we need to resolve locally, we do this now 725 addr = dest_addr if rdns else socket.gethostbyname(dest_addr) 726 727 http_headers = [ 728 (b"CONNECT " + addr.encode("idna") + b":" 729 + str(dest_port).encode() + b" HTTP/1.1"), 730 b"Host: " + dest_addr.encode("idna") 731 ] 732 733 if username and password: 734 http_headers.append(b"Proxy-Authorization: basic " 735 + b64encode(username + b":" + password)) 736 737 http_headers.append(b"\r\n") 738 739 self.sendall(b"\r\n".join(http_headers)) 740 741 # We just need the first line to check if the connection was successful 742 fobj = self.makefile() 743 status_line = fobj.readline() 744 fobj.close() 745 746 if not status_line: 747 raise GeneralProxyError("Connection closed unexpectedly") 748 749 try: 750 proto, status_code, status_msg = status_line.split(" ", 2) 751 except ValueError: 752 raise GeneralProxyError("HTTP proxy server sent invalid response") 753 754 if not proto.startswith("HTTP/"): 755 raise GeneralProxyError( 756 "Proxy server does not appear to be an HTTP proxy") 757 758 try: 759 status_code = int(status_code) 760 except ValueError: 761 raise HTTPError( 762 "HTTP proxy server did not return a valid HTTP status") 763 764 if status_code != 200: 765 error = "{0}: {1}".format(status_code, status_msg) 766 if status_code in (400, 403, 405): 767 # It's likely that the HTTP proxy server does not support the 768 # CONNECT tunneling method 769 error += ("\n[*] Note: The HTTP proxy server may not be" 770 " supported by PySocks (must be a CONNECT tunnel" 771 " proxy)") 772 raise HTTPError(error) 773 774 self.proxy_sockname = (b"0.0.0.0", 0) 775 self.proxy_peername = addr, dest_port 776 777 _proxy_negotiators = { 778 SOCKS4: _negotiate_SOCKS4, 779 SOCKS5: _negotiate_SOCKS5, 780 HTTP: _negotiate_HTTP 781 } 782 783 @set_self_blocking 784 def connect(self, dest_pair): 785 """ 786 Connects to the specified destination through a proxy. 787 Uses the same API as socket's connect(). 788 To select the proxy server, use set_proxy(). 789 790 dest_pair - 2-tuple of (IP/hostname, port). 791 """ 792 if len(dest_pair) != 2 or dest_pair[0].startswith("["): 793 # Probably IPv6, not supported -- raise an error, and hope 794 # Happy Eyeballs (RFC6555) makes sure at least the IPv4 795 # connection works... 796 raise socket.error("PySocks doesn't support IPv6: %s" 797 % str(dest_pair)) 798 799 dest_addr, dest_port = dest_pair 800 801 if self.type == socket.SOCK_DGRAM: 802 if not self._proxyconn: 803 self.bind(("", 0)) 804 dest_addr = socket.gethostbyname(dest_addr) 805 806 # If the host address is INADDR_ANY or similar, reset the peer 807 # address so that packets are received from any peer 808 if dest_addr == "0.0.0.0" and not dest_port: 809 self.proxy_peername = None 810 else: 811 self.proxy_peername = (dest_addr, dest_port) 812 return 813 814 (proxy_type, proxy_addr, proxy_port, rdns, username, 815 password) = self.proxy 816 817 # Do a minimal input check first 818 if (not isinstance(dest_pair, (list, tuple)) 819 or len(dest_pair) != 2 820 or not dest_addr 821 or not isinstance(dest_port, int)): 822 # Inputs failed, raise an error 823 raise GeneralProxyError( 824 "Invalid destination-connection (host, port) pair") 825 826 # We set the timeout here so that we don't hang in connection or during 827 # negotiation. 828 super(socksocket, self).settimeout(self._timeout) 829 830 if proxy_type is None: 831 # Treat like regular socket object 832 self.proxy_peername = dest_pair 833 super(socksocket, self).settimeout(self._timeout) 834 super(socksocket, self).connect((dest_addr, dest_port)) 835 return 836 837 proxy_addr = self._proxy_addr() 838 839 try: 840 # Initial connection to proxy server. 841 super(socksocket, self).connect(proxy_addr) 842 843 except socket.error as error: 844 # Error while connecting to proxy 845 self.close() 846 proxy_addr, proxy_port = proxy_addr 847 proxy_server = "{0}:{1}".format(proxy_addr, proxy_port) 848 printable_type = PRINTABLE_PROXY_TYPES[proxy_type] 849 850 msg = "Error connecting to {0} proxy {1}".format(printable_type, 851 proxy_server) 852 log.debug("%s due to: %s", msg, error) 853 raise ProxyConnectionError(msg, error) 854 855 else: 856 # Connected to proxy server, now negotiate 857 try: 858 # Calls negotiate_{SOCKS4, SOCKS5, HTTP} 859 negotiate = self._proxy_negotiators[proxy_type] 860 negotiate(self, dest_addr, dest_port) 861 except socket.error as error: 862 # Wrap socket errors 863 self.close() 864 raise GeneralProxyError("Socket error", error) 865 except ProxyError: 866 # Protocol error while negotiating with proxy 867 self.close() 868 raise 869 870 def _proxy_addr(self): 871 """ 872 Return proxy address to connect to as tuple object 873 """ 874 (proxy_type, proxy_addr, proxy_port, rdns, username, 875 password) = self.proxy 876 proxy_port = proxy_port or DEFAULT_PORTS.get(proxy_type) 877 if not proxy_port: 878 raise GeneralProxyError("Invalid proxy type") 879 return proxy_addr, proxy_port 880