• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""SocksiPy - Python SOCKS module.
2
3Version 1.00
4
5Copyright 2006 Dan-Haim. All rights reserved.
6
7Redistribution and use in source and binary forms, with or without modification,
8are permitted provided that the following conditions are met:
91. Redistributions of source code must retain the above copyright notice, this
10   list of conditions and the following disclaimer.
112. Redistributions in binary form must reproduce the above copyright notice,
12   this list of conditions and the following disclaimer in the documentation
13   and/or other materials provided with the distribution.
143. Neither the name of Dan Haim nor the names of his contributors may be used
15   to endorse or promote products derived from this software without specific
16   prior written permission.
17
18THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
19WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
20MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
21EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
22INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
24OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
26OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.
27
28This module provides a standard socket-like interface for Python
29for tunneling connections through SOCKS proxies.
30
31Minor modifications made by Christopher Gilbert (http://motomastyle.com/) for
32use in PyLoris (http://pyloris.sourceforge.net/).
33
34Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/)
35mainly to merge bug fixes found in Sourceforge.
36"""
37
38import base64
39import socket
40import struct
41import sys
42
43if getattr(socket, "socket", None) is None:
44    raise ImportError("socket.socket missing, proxy support unusable")
45
46PROXY_TYPE_SOCKS4 = 1
47PROXY_TYPE_SOCKS5 = 2
48PROXY_TYPE_HTTP = 3
49PROXY_TYPE_HTTP_NO_TUNNEL = 4
50
51_defaultproxy = None
52_orgsocket = socket.socket
53
54
55class ProxyError(Exception):
56    pass
57
58
59class GeneralProxyError(ProxyError):
60    pass
61
62
63class Socks5AuthError(ProxyError):
64    pass
65
66
67class Socks5Error(ProxyError):
68    pass
69
70
71class Socks4Error(ProxyError):
72    pass
73
74
75class HTTPError(ProxyError):
76    pass
77
78
79_generalerrors = (
80    "success",
81    "invalid data",
82    "not connected",
83    "not available",
84    "bad proxy type",
85    "bad input",
86)
87
88_socks5errors = (
89    "succeeded",
90    "general SOCKS server failure",
91    "connection not allowed by ruleset",
92    "Network unreachable",
93    "Host unreachable",
94    "Connection refused",
95    "TTL expired",
96    "Command not supported",
97    "Address type not supported",
98    "Unknown error",
99)
100
101_socks5autherrors = (
102    "succeeded",
103    "authentication is required",
104    "all offered authentication methods were rejected",
105    "unknown username or invalid password",
106    "unknown error",
107)
108
109_socks4errors = (
110    "request granted",
111    "request rejected or failed",
112    "request rejected because SOCKS server cannot connect to identd on the client",
113    "request rejected because the client program and identd report different "
114    "user-ids",
115    "unknown error",
116)
117
118
119def setdefaultproxy(
120    proxytype=None, addr=None, port=None, rdns=True, username=None, password=None
121):
122    """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
123    Sets a default proxy which all further socksocket objects will use,
124    unless explicitly changed.
125    """
126    global _defaultproxy
127    _defaultproxy = (proxytype, addr, port, rdns, username, password)
128
129
130def wrapmodule(module):
131    """wrapmodule(module)
132
133    Attempts to replace a module's socket library with a SOCKS socket. Must set
134    a default proxy using setdefaultproxy(...) first.
135    This will only work on modules that import socket directly into the
136    namespace;
137    most of the Python Standard Library falls into this category.
138    """
139    if _defaultproxy != None:
140        module.socket.socket = socksocket
141    else:
142        raise GeneralProxyError((4, "no proxy specified"))
143
144
145class socksocket(socket.socket):
146    """socksocket([family[, type[, proto]]]) -> socket object
147    Open a SOCKS enabled socket. The parameters are the same as
148    those of the standard socket init. In order for SOCKS to work,
149    you must specify family=AF_INET, type=SOCK_STREAM and proto=0.
150    """
151
152    def __init__(
153        self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None
154    ):
155        _orgsocket.__init__(self, family, type, proto, _sock)
156        if _defaultproxy != None:
157            self.__proxy = _defaultproxy
158        else:
159            self.__proxy = (None, None, None, None, None, None)
160        self.__proxysockname = None
161        self.__proxypeername = None
162        self.__httptunnel = True
163
164    def __recvall(self, count):
165        """__recvall(count) -> data
166        Receive EXACTLY the number of bytes requested from the socket.
167        Blocks until the required number of bytes have been received.
168        """
169        data = self.recv(count)
170        while len(data) < count:
171            d = self.recv(count - len(data))
172            if not d:
173                raise GeneralProxyError((0, "connection closed unexpectedly"))
174            data = data + d
175        return data
176
177    def sendall(self, content, *args):
178        """ override socket.socket.sendall method to rewrite the header
179        for non-tunneling proxies if needed
180        """
181        if not self.__httptunnel:
182            content = self.__rewriteproxy(content)
183        return super(socksocket, self).sendall(content, *args)
184
185    def __rewriteproxy(self, header):
186        """ rewrite HTTP request headers to support non-tunneling proxies
187        (i.e. those which do not support the CONNECT method).
188        This only works for HTTP (not HTTPS) since HTTPS requires tunneling.
189        """
190        host, endpt = None, None
191        hdrs = header.split("\r\n")
192        for hdr in hdrs:
193            if hdr.lower().startswith("host:"):
194                host = hdr
195            elif hdr.lower().startswith("get") or hdr.lower().startswith("post"):
196                endpt = hdr
197        if host and endpt:
198            hdrs.remove(host)
199            hdrs.remove(endpt)
200            host = host.split(" ")[1]
201            endpt = endpt.split(" ")
202            if self.__proxy[4] != None and self.__proxy[5] != None:
203                hdrs.insert(0, self.__getauthheader())
204            hdrs.insert(0, "Host: %s" % host)
205            hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2]))
206        return "\r\n".join(hdrs)
207
208    def __getauthheader(self):
209        auth = self.__proxy[4] + ":" + self.__proxy[5]
210        return "Proxy-Authorization: Basic " + base64.b64encode(auth)
211
212    def setproxy(
213        self,
214        proxytype=None,
215        addr=None,
216        port=None,
217        rdns=True,
218        username=None,
219        password=None,
220        headers=None,
221    ):
222        """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
223
224        Sets the proxy to be used.
225        proxytype -    The type of the proxy to be used. Three types
226                are supported: PROXY_TYPE_SOCKS4 (including socks4a),
227                PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP
228        addr -        The address of the server (IP or DNS).
229        port -        The port of the server. Defaults to 1080 for SOCKS
230                servers and 8080 for HTTP proxy servers.
231        rdns -        Should DNS queries be preformed on the remote side
232                (rather than the local side). The default is True.
233                Note: This has no effect with SOCKS4 servers.
234        username -    Username to authenticate with to the server.
235                The default is no authentication.
236        password -    Password to authenticate with to the server.
237                Only relevant when username is also provided.
238        headers -     Additional or modified headers for the proxy connect
239        request.
240        """
241        self.__proxy = (proxytype, addr, port, rdns, username, password, headers)
242
243    def __negotiatesocks5(self, destaddr, destport):
244        """__negotiatesocks5(self,destaddr,destport)
245        Negotiates a connection through a SOCKS5 server.
246        """
247        # First we'll send the authentication packages we support.
248        if (self.__proxy[4] != None) and (self.__proxy[5] != None):
249            # The username/password details were supplied to the
250            # setproxy method so we support the USERNAME/PASSWORD
251            # authentication (in addition to the standard none).
252            self.sendall(struct.pack("BBBB", 0x05, 0x02, 0x00, 0x02))
253        else:
254            # No username/password were entered, therefore we
255            # only support connections with no authentication.
256            self.sendall(struct.pack("BBB", 0x05, 0x01, 0x00))
257        # We'll receive the server's response to determine which
258        # method was selected
259        chosenauth = self.__recvall(2)
260        if chosenauth[0:1] != chr(0x05).encode():
261            self.close()
262            raise GeneralProxyError((1, _generalerrors[1]))
263        # Check the chosen authentication method
264        if chosenauth[1:2] == chr(0x00).encode():
265            # No authentication is required
266            pass
267        elif chosenauth[1:2] == chr(0x02).encode():
268            # Okay, we need to perform a basic username/password
269            # authentication.
270            self.sendall(
271                chr(0x01).encode()
272                + chr(len(self.__proxy[4]))
273                + self.__proxy[4]
274                + chr(len(self.__proxy[5]))
275                + self.__proxy[5]
276            )
277            authstat = self.__recvall(2)
278            if authstat[0:1] != chr(0x01).encode():
279                # Bad response
280                self.close()
281                raise GeneralProxyError((1, _generalerrors[1]))
282            if authstat[1:2] != chr(0x00).encode():
283                # Authentication failed
284                self.close()
285                raise Socks5AuthError((3, _socks5autherrors[3]))
286            # Authentication succeeded
287        else:
288            # Reaching here is always bad
289            self.close()
290            if chosenauth[1] == chr(0xFF).encode():
291                raise Socks5AuthError((2, _socks5autherrors[2]))
292            else:
293                raise GeneralProxyError((1, _generalerrors[1]))
294        # Now we can request the actual connection
295        req = struct.pack("BBB", 0x05, 0x01, 0x00)
296        # If the given destination address is an IP address, we'll
297        # use the IPv4 address request even if remote resolving was specified.
298        try:
299            ipaddr = socket.inet_aton(destaddr)
300            req = req + chr(0x01).encode() + ipaddr
301        except socket.error:
302            # Well it's not an IP number,  so it's probably a DNS name.
303            if self.__proxy[3]:
304                # Resolve remotely
305                ipaddr = None
306                req = (
307                    req
308                    + chr(0x03).encode()
309                    + chr(len(destaddr)).encode()
310                    + destaddr.encode()
311                )
312            else:
313                # Resolve locally
314                ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
315                req = req + chr(0x01).encode() + ipaddr
316        req = req + struct.pack(">H", destport)
317        self.sendall(req)
318        # Get the response
319        resp = self.__recvall(4)
320        if resp[0:1] != chr(0x05).encode():
321            self.close()
322            raise GeneralProxyError((1, _generalerrors[1]))
323        elif resp[1:2] != chr(0x00).encode():
324            # Connection failed
325            self.close()
326            if ord(resp[1:2]) <= 8:
327                raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])]))
328            else:
329                raise Socks5Error((9, _socks5errors[9]))
330        # Get the bound address/port
331        elif resp[3:4] == chr(0x01).encode():
332            boundaddr = self.__recvall(4)
333        elif resp[3:4] == chr(0x03).encode():
334            resp = resp + self.recv(1)
335            boundaddr = self.__recvall(ord(resp[4:5]))
336        else:
337            self.close()
338            raise GeneralProxyError((1, _generalerrors[1]))
339        boundport = struct.unpack(">H", self.__recvall(2))[0]
340        self.__proxysockname = (boundaddr, boundport)
341        if ipaddr != None:
342            self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
343        else:
344            self.__proxypeername = (destaddr, destport)
345
346    def getproxysockname(self):
347        """getsockname() -> address info
348        Returns the bound IP address and port number at the proxy.
349        """
350        return self.__proxysockname
351
352    def getproxypeername(self):
353        """getproxypeername() -> address info
354        Returns the IP and port number of the proxy.
355        """
356        return _orgsocket.getpeername(self)
357
358    def getpeername(self):
359        """getpeername() -> address info
360        Returns the IP address and port number of the destination
361        machine (note: getproxypeername returns the proxy)
362        """
363        return self.__proxypeername
364
365    def __negotiatesocks4(self, destaddr, destport):
366        """__negotiatesocks4(self,destaddr,destport)
367        Negotiates a connection through a SOCKS4 server.
368        """
369        # Check if the destination address provided is an IP address
370        rmtrslv = False
371        try:
372            ipaddr = socket.inet_aton(destaddr)
373        except socket.error:
374            # It's a DNS name. Check where it should be resolved.
375            if self.__proxy[3]:
376                ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01)
377                rmtrslv = True
378            else:
379                ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
380        # Construct the request packet
381        req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr
382        # The username parameter is considered userid for SOCKS4
383        if self.__proxy[4] != None:
384            req = req + self.__proxy[4]
385        req = req + chr(0x00).encode()
386        # DNS name if remote resolving is required
387        # NOTE: This is actually an extension to the SOCKS4 protocol
388        # called SOCKS4A and may not be supported in all cases.
389        if rmtrslv:
390            req = req + destaddr + chr(0x00).encode()
391        self.sendall(req)
392        # Get the response from the server
393        resp = self.__recvall(8)
394        if resp[0:1] != chr(0x00).encode():
395            # Bad data
396            self.close()
397            raise GeneralProxyError((1, _generalerrors[1]))
398        if resp[1:2] != chr(0x5A).encode():
399            # Server returned an error
400            self.close()
401            if ord(resp[1:2]) in (91, 92, 93):
402                self.close()
403                raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90]))
404            else:
405                raise Socks4Error((94, _socks4errors[4]))
406        # Get the bound address/port
407        self.__proxysockname = (
408            socket.inet_ntoa(resp[4:]),
409            struct.unpack(">H", resp[2:4])[0],
410        )
411        if rmtrslv != None:
412            self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
413        else:
414            self.__proxypeername = (destaddr, destport)
415
416    def __negotiatehttp(self, destaddr, destport):
417        """__negotiatehttp(self,destaddr,destport)
418        Negotiates a connection through an HTTP server.
419        """
420        # If we need to resolve locally, we do this now
421        if not self.__proxy[3]:
422            addr = socket.gethostbyname(destaddr)
423        else:
424            addr = destaddr
425        headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"]
426        wrote_host_header = False
427        wrote_auth_header = False
428        if self.__proxy[6] != None:
429            for key, val in self.__proxy[6].iteritems():
430                headers += [key, ": ", val, "\r\n"]
431                wrote_host_header = key.lower() == "host"
432                wrote_auth_header = key.lower() == "proxy-authorization"
433        if not wrote_host_header:
434            headers += ["Host: ", destaddr, "\r\n"]
435        if not wrote_auth_header:
436            if self.__proxy[4] != None and self.__proxy[5] != None:
437                headers += [self.__getauthheader(), "\r\n"]
438        headers.append("\r\n")
439        self.sendall("".join(headers).encode())
440        # We read the response until we get the string "\r\n\r\n"
441        resp = self.recv(1)
442        while resp.find("\r\n\r\n".encode()) == -1:
443            resp = resp + self.recv(1)
444        # We just need the first line to check if the connection
445        # was successful
446        statusline = resp.splitlines()[0].split(" ".encode(), 2)
447        if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()):
448            self.close()
449            raise GeneralProxyError((1, _generalerrors[1]))
450        try:
451            statuscode = int(statusline[1])
452        except ValueError:
453            self.close()
454            raise GeneralProxyError((1, _generalerrors[1]))
455        if statuscode != 200:
456            self.close()
457            raise HTTPError((statuscode, statusline[2]))
458        self.__proxysockname = ("0.0.0.0", 0)
459        self.__proxypeername = (addr, destport)
460
461    def connect(self, destpair):
462        """connect(self, despair)
463        Connects to the specified destination through a proxy.
464        destpar - A tuple of the IP/DNS address and the port number.
465        (identical to socket's connect).
466        To select the proxy server use setproxy().
467        """
468        # Do a minimal input check first
469        if (
470            (not type(destpair) in (list, tuple))
471            or (len(destpair) < 2)
472            or (not isinstance(destpair[0], basestring))
473            or (type(destpair[1]) != int)
474        ):
475            raise GeneralProxyError((5, _generalerrors[5]))
476        if self.__proxy[0] == PROXY_TYPE_SOCKS5:
477            if self.__proxy[2] != None:
478                portnum = self.__proxy[2]
479            else:
480                portnum = 1080
481            _orgsocket.connect(self, (self.__proxy[1], portnum))
482            self.__negotiatesocks5(destpair[0], destpair[1])
483        elif self.__proxy[0] == PROXY_TYPE_SOCKS4:
484            if self.__proxy[2] != None:
485                portnum = self.__proxy[2]
486            else:
487                portnum = 1080
488            _orgsocket.connect(self, (self.__proxy[1], portnum))
489            self.__negotiatesocks4(destpair[0], destpair[1])
490        elif self.__proxy[0] == PROXY_TYPE_HTTP:
491            if self.__proxy[2] != None:
492                portnum = self.__proxy[2]
493            else:
494                portnum = 8080
495            _orgsocket.connect(self, (self.__proxy[1], portnum))
496            self.__negotiatehttp(destpair[0], destpair[1])
497        elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL:
498            if self.__proxy[2] != None:
499                portnum = self.__proxy[2]
500            else:
501                portnum = 8080
502            _orgsocket.connect(self, (self.__proxy[1], portnum))
503            if destpair[1] == 443:
504                self.__negotiatehttp(destpair[0], destpair[1])
505            else:
506                self.__httptunnel = False
507        elif self.__proxy[0] == None:
508            _orgsocket.connect(self, (destpair[0], destpair[1]))
509        else:
510            raise GeneralProxyError((4, _generalerrors[4]))
511