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