Ticket #3508: socks_twisted.py

File socks_twisted.py, 14.7 KB (added by gr0gmint, 6 years ago)
Line 
1"""
2Ported from:
3SocksiPy - Python SOCKS module.
4Version 1.00
5
6Copyright 2006 Dan-Haim. All rights reserved.
7
8Redistribution and use in source and binary forms, with or without modification,
9are permitted provided that the following conditions are met:
101. Redistributions of source code must retain the above copyright notice, this
11   list of conditions and the following disclaimer.
122. Redistributions in binary form must reproduce the above copyright notice,
13   this list of conditions and the following disclaimer in the documentation
14   and/or other materials provided with the distribution.
153. Neither the name of Dan Haim nor the names of his contributors may be used
16   to endorse or promote products derived from this software without specific
17   prior written permission.
18   
19THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
20WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
21MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
22EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
25OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
27OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.
28
29
30"""
31
32import socket
33import struct
34from twisted.internet.protocol import Protocol,ClientFactory
35
36PROXY_TYPE_SOCKS4 = 1
37PROXY_TYPE_SOCKS5 = 2
38PROXY_TYPE_HTTP = 3
39
40_defaultproxy = None
41_orgsocket = socket.socket
42
43
44
45class ProxyError(Exception):
46        def __init__(self, value):
47                self.value = value
48        def __str__(self):
49                return repr(self.value)
50
51class GeneralProxyError(ProxyError):
52        def __init__(self, value):
53                self.value = value
54        def __str__(self):
55                return repr(self.value)
56
57class Socks5AuthError(ProxyError):
58        def __init__(self, value):
59                self.value = value
60        def __str__(self):
61                return repr(self.value)
62
63class Socks5Error(ProxyError):
64        def __init__(self, value):
65                self.value = value
66        def __str__(self):
67                return repr(self.value)
68
69class Socks4Error(ProxyError):
70        def __init__(self, value):
71                self.value = value
72        def __str__(self):
73                return repr(self.value)
74
75class HTTPError(ProxyError):
76        def __init__(self, value):
77                self.value = value
78        def __str__(self):
79                return repr(self.value)
80
81_generalerrors = ("success",
82                   "invalid data",
83                   "not connected",
84                   "not available",
85                   "bad proxy type",
86                   "bad input")
87
88_socks5errors = ("succeeded",
89                  "general SOCKS server failure",
90                  "connection not allowed by ruleset",
91                  "Network unreachable",
92                  "Host unreachable",
93                  "Connection refused",
94                  "TTL expired",
95                  "Command not supported",
96                  "Address type not supported",
97                  "Unknown error")
98
99_socks5autherrors = ("succeeded",
100                      "authentication is required",
101                      "all offered authentication methods were rejected",
102                      "unknown username or invalid password",
103                      "unknown error")
104
105_socks4errors = ("request granted",
106                  "request rejected or failed",
107                  "request rejected because SOCKS server cannot connect to identd on the client",
108                  "request rejected because the client program and identd report different user-ids",
109                  "unknown error")
110
111
112class SOCKSClient(Protocol):
113        def __init__(self, destpair,proxyconf):
114                self.destpair = destpair
115                self.__proxy = proxyconf
116       
117        def setproxy(self,proxytype=None,addr=None,port=None,rdns=True,username=None,password=None):
118                """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
119                Sets the proxy to be used.
120                proxytype -     The type of the proxy to be used. Three types
121                                are supported: PROXY_TYPE_SOCKS4 (including socks4a),
122                                PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP
123                addr -          The address of the server (IP or DNS).
124                port -          The port of the server. Defaults to 1080 for SOCKS
125                                servers and 8080 for HTTP proxy servers.
126                rdns -          Should DNS queries be preformed on the remote side
127                                (rather than the local side). The default is True.
128                                Note: This has no effect with SOCKS4 servers.
129                username -      Username to authenticate with to the server.
130                                The default is no authentication.
131                password -      Password to authenticate with to the server.
132                                Only relevant when username is also provided.
133                """
134                self.__proxy = (proxytype,addr,port,rdns,username,password)
135       
136        def __negotiatesocks5(self,destaddr,destport):
137                """__negotiatesocks5(self,destaddr,destport)
138                Negotiates a connection through a SOCKS5 server.
139                """
140                # First we'll send the authentication packages we support.
141                if (self.__proxy[4]!=None) and (self.__proxy[5]!=None):
142                        # The username/password details were supplied to the
143                        # setproxy method so we support the USERNAME/PASSWORD
144                        # authentication (in addition to the standard none).
145                        self.transport.write("\x05\x02\x00\x02")
146                else:
147                        # No username/password were entered, therefore we
148                        # only support connections with no authentication.
149                        self.transport.write("\x05\x01\x00")
150                # We'll receive the server's response to determine which
151                # method was selected
152                chosenauth = self.checkbuf(2)
153                if not chosenauth:
154                        self.cocommand = ("MoreBytes",2)
155                        chosenauth = yield
156                        self.cocommand = ""
157                if chosenauth[0] != "\x05":
158                        self.transport.loseConnection()
159                        raise GeneralProxyError((1,_generalerrors[1]))
160                # Check the chosen authentication method
161                if chosenauth[1] == "\x00":
162                        pass
163                elif chosenauth[1] == "\x02":
164                        # Okay, we need to perform a basic username/password
165                        # authentication.
166                        self.transport.write("\x01" + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.proxy[5])) + self.__proxy[5])
167                        authstat = self.checkbuf(8)
168                        if not authstat:
169                                self.cocommand = ("MoreBytes",8)
170                                authstat = yield
171                                self.cocommand = ""
172                        if authstat[0] != "\x01":
173                                # Bad response
174                                self.transport.loseConnection()
175                                raise GeneralProxyError((1,_generalerrors[1]))
176                        if authstat[1] != "\x00":
177                                # Authentication failed
178                                self.transport.loseConnection()
179                                raise Socks5AuthError,((3,_socks5autherrors[3]))
180                        # Authentication succeeded
181                else:
182                        # Reaching here is always bad
183                        self.transport.loseConnection()
184                        if chosenauth[1] == "\xFF":
185                                raise Socks5AuthError((2,_socks5autherrors[2]))
186                        else:
187                                raise GeneralProxyError((1,_generalerrors[1]))
188                # Now we can request the actual connection
189                req = "\x05\x01\x00"
190                # If the given destination address is an IP address, we'll
191                # use the IPv4 address request even if remote resolving was specified.
192                try:
193                        ipaddr = socket.inet_aton(destaddr)
194                        req = req + "\x01" + ipaddr
195                except socket.error:
196                        # Well it's not an IP number,  so it's probably a DNS name.
197                        if self.__proxy[3]==True:
198                                # Resolve remotely
199                                ipaddr = None
200                                req = req + "\x03" + chr(len(destaddr)) + destaddr
201                        else:
202                                # Resolve locally
203                                ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
204                                req = req + "\x01" + ipaddr
205                req = req + struct.pack(">H",destport)
206                self.transport.write(req)
207                # Get the response
208                resp = self.checkbuf(4)
209                if not resp:
210                        self.cocommand = ("MoreBytes",4)
211                        resp = yield
212                        self.cocommand = ""
213                if resp[0] != "\x05":
214                        self.transport.loseConnection()
215                        raise GeneralProxyError((1,_generalerrors[1]))
216                elif resp[1] != "\x00":
217                        # Connection failed
218                        self.transport.loseConnection()
219                        if ord(resp[1])<=8:
220                                raise Socks5Error(ord(resp[1]),_generalerrors[ord(resp[1])])
221                        else:
222                                raise Socks5Error(9,_generalerrors[9])
223                # Get the bound address/port
224                elif resp[3] == "\x01":
225                        boundaddr = self.checkbuf(4)
226                        if not boundaddr:
227                                self.cocommand = ("MoreBytes",4)
228                                boundaddr = yield
229                                self.cocommand = ""
230                elif resp[3] == "\x03":
231                        resp2 = self.checkbuf(1)
232                        if not resp2:
233                                self.cocommand = ("MoreBytes",1)
234                                resp2 = yield
235                                self.cocommand = ""
236                        resp = resp + resp2
237                        boundaddr = self.checkbuf(resp[4])
238                        if not boundaddr:
239                                self.cocommand = ("MoreBytes",resp[4])
240                                boundaddr = yield
241                                self.cocommand = ""
242                else:
243                        self.transport.loseConnection()
244                        raise GeneralProxyError((1,_generalerrors[1]))
245                recv = self.checkbuf(2)
246                if not recv:
247                        self.cocommand=("MoreBytes",2)
248                        recv = yield
249                        self.cocommand=""
250                print "Got the last 2 bytes: ", len(recv)
251                boundport = struct.unpack(">H",recv)[0]
252                self.__proxysockname = (boundaddr,boundport)
253                if ipaddr != None:
254                        self.__proxypeername = (socket.inet_ntoa(ipaddr),destport)
255                else:
256                        self.__proxypeername = (destaddr,destport)
257                self.established = True
258                self.connectionEstablished()
259        def getproxysockname(self):
260                """getsockname() -> address info
261                Returns the bound IP address and port number at the proxy.
262                """
263                return self.__proxysockname
264
265        def getpeername(self):
266                """getpeername() -> address info
267                Returns the IP address and port number of the destination
268                machine (note: getproxypeername returns the proxy)
269                """
270                return self.__proxypeername
271
272        def checkbuf(self, num=None):
273                answer = ''
274                if num == None and len(self.buf) > 0:
275                        answer = self.buf
276                        self.buf = ""
277                elif num and len(self.buf) >= num:
278                        answer = self.buf[:num]
279                        self.buf = self.buf[num:]
280                return answer
281
282
283        def __negotiatesocks4(self,destaddr,destport):
284                """__negotiatesocks4(self,destaddr,destport)
285                Negotiates a connection through a SOCKS4 server.
286                """
287                # Check if the destination address provided is an IP address
288                rmtrslv = False
289                try:
290                        ipaddr = socket.inet_aton(destaddr) # make asynchronous
291                except socket.error:
292                        # It's a DNS name. Check where it should be resolved.
293                        if self.__proxy[3]==True:
294                                ipaddr = "\x00\x00\x00\x01"
295                                rmtrslv = True
296                        else:
297                                ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
298                # Construct the request packet
299                req = "\x04\x01" + struct.pack(">H",destport) + ipaddr
300                # The username parameter is considered userid for SOCKS4
301                if self.__proxy[4] != None:
302                        req = req + self.__proxy[4]
303                req = req + "\x00"
304                # DNS name if remote resolving is required
305                # NOTE: This is actually an extension to the SOCKS4 protocol
306                # called SOCKS4A and may not be supported in all cases.
307                if rmtrslv==True:
308                        req = req + destaddr + "\x00"
309                self.transport.write(req)
310                # Get the response from the server
311                resp = self.checkbuf(8)
312                if not resp:
313                        self.cocommand = ("MoreBytes",8)
314                        resp = yield
315                        self.cocommand = ""
316                if resp[0] != "\x00":
317                        # Bad data
318                        self.transport.loseConnection()
319                        raise GeneralProxyError((1,_generalerrors[1]))
320                if resp[1] != "\x5A":
321                        # Server returned an error
322                        self.transport.loseConnection()
323                        if ord(resp[1]) in (91,92,93):
324                                self.transport.loseConnection()
325                                raise Socks4Error((ord(resp[1]),_socks4errors[ord(resp[1])-90]))
326                        else:
327                                raise Socks4Error((94,_socks4errors[4]))
328                # Get the bound address/port
329                self.__proxysockname = (socket.inet_ntoa(resp[4:]),struct.unpack(">H",resp[2:4])[0])
330                if rmtrslv != None:
331                        self.__proxypeername = (socket.inet_ntoa(ipaddr),destport)
332                else:
333                        self.__proxypeername = (destaddr,destport)
334                self.established = True
335                self.connectionEstablished()
336               
337               
338        def __negotiatehttp(self,destaddr,destport):
339                """__negotiatehttp(self,destaddr,destport)
340                Negotiates a connection through an HTTP server.
341                """
342                # If we need to resolve locally, we do this now
343                if self.__proxy[3] == False:
344                        addr = socket.gethostbyname(destaddr)    # from __future__ import better_asynchronous_way_to_do_this
345                else:
346                        addr = destaddr
347                self.transport.write("CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" + "Host: " + destaddr + "\r\n\r\n")
348                # We read the response until we get the string "\r\n\r\n"
349                resp = self.checkbuf()
350                while resp.find("\r\n\r\n")==-1:
351                        self.cocommand = "MoreBytes"
352                        resp2 = yield
353                        self.cocommand = ""
354                        resp += resp2
355                # We just need the first line to check if the connection
356                # was successful
357                statusline = resp.splitlines()[0].split(" ",2)
358                if statusline[0] not in ("HTTP/1.0","HTTP/1.1"):
359                        self.transport.loseConnection()
360                        raise GeneralProxyError((1,_generalerrors[1]))
361                try:
362                        statuscode = int(statusline[1])
363                except ValueError:
364                        self.transport.loseConnection()
365                        raise GeneralProxyError((1,_generalerrors[1]))
366                if statuscode != 200:
367                        self.transport.loseConnection()
368                        raise HTTPError((statuscode,statusline[2]))
369                self.__proxysockname = ("0.0.0.0",0)
370                self.__proxypeername = (addr,destport)
371
372                self.established = True
373                self.connectionEstablished()
374               
375        def dataReceived(self,data):
376                    if self.cocommand == "MoreBytes":
377                            tosend = self.buf + data
378                            self.buf = ""
379                            try:
380                                    self.coroutine.send(tosend)
381                            except:
382                                    pass
383                    elif type(self.cocommand) == tuple and self.cocommand[0] == "MoreBytes":
384                            bytesneeded = self.cocommand[1]
385                            self.buf += data
386                            if len(self.buf) >= bytesneeded:
387                                    try:
388                                            self.coroutine.send(self.buf[:bytesneeded])
389                                    except:
390                                            pass
391                                    self.buf = self.buf[bytesneeded:]
392                    else:
393                            self.buf += data
394               
395        def connectionMade(self):
396                """
397                Connects to the specified destination through a proxy.
398                destpar - A tuple of the IP/DNS address and the port number.
399                (identical to socket's connect).
400                To select the proxy server use setproxy().
401                """
402                self.buf=""
403                self.established = False
404                self.cocommand = ""
405                # Do a minimal input check first
406                if (type(self.destpair) in (list,tuple)==False) or (len(self.destpair)<2) or (type(self.destpair[0])!=str) or (type(self.destpair[1])!=int):
407                        raise GeneralProxyError((5,_generalerrors[5]))
408                if self.__proxy[0] == PROXY_TYPE_SOCKS5:
409                        if self.__proxy[2] != None:
410                                portnum = self.__proxy[2]
411                        else:
412                                portnum = 1080
413                        print "Trying SOCKSv5"
414                        self.coroutine = self.__negotiatesocks5(self.destpair[0],self.destpair[1])
415                elif self.__proxy[0] == PROXY_TYPE_SOCKS4:
416                        if self.__proxy[2] != None:
417                                portnum = self.__proxy[2]
418                        else:
419                                portnum = 1080
420                        print "Trying SOCKSv4"
421                        self.coroutine = self.__negotiatesocks4(self.destpair[0],self.destpair[1])
422                elif self.__proxy[0] == PROXY_TYPE_HTTP:
423                        if self.__proxy[2] != None:
424                                portnum = self.__proxy[2]
425                        else:
426                                portnum = 8080
427                        print "Trying HTTP"
428                        self.coroutine = __negotiatehttp(self.destpair[0],self.destpair[1])
429                else:
430                        raise GeneralProxyError((4,_generalerrors[4]))
431                self.coroutine.next()
432        def connectionEstablished(self):
433                pass
434class SOCKSClientFactory(ClientFactory):
435        def __init__(self, destpair, proxytype=None,addr=None,port=None,rdns=True,username=None,password=None,protocol=None):
436                """
437                @type destpair = [list|tuple]
438                """
439                self.destpair = destpair
440                self.proxyconf = [proxytype,addr,port,rdns,username,password]
441        def buildProtocol(self, addr):
442                return SOCKSClient(self.destpair,self.proxyconf)