qa: Only allow disconnecting all NodeConns
[bitcoinplatinum.git] / test / functional / test_framework / socks5.py
blob7b40c47fbf401f8a9af93a1ddcbc36c32506ef75
1 #!/usr/bin/env python3
2 # Copyright (c) 2015-2016 The Bitcoin Core developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 """Dummy Socks5 server for testing."""
7 import socket, threading, queue
8 import logging
10 logger = logging.getLogger("TestFramework.socks5")
12 ### Protocol constants
13 class Command:
14 CONNECT = 0x01
16 class AddressType:
17 IPV4 = 0x01
18 DOMAINNAME = 0x03
19 IPV6 = 0x04
21 ### Utility functions
22 def recvall(s, n):
23 """Receive n bytes from a socket, or fail."""
24 rv = bytearray()
25 while n > 0:
26 d = s.recv(n)
27 if not d:
28 raise IOError('Unexpected end of stream')
29 rv.extend(d)
30 n -= len(d)
31 return rv
33 ### Implementation classes
34 class Socks5Configuration():
35 """Proxy configuration."""
36 def __init__(self):
37 self.addr = None # Bind address (must be set)
38 self.af = socket.AF_INET # Bind address family
39 self.unauth = False # Support unauthenticated
40 self.auth = False # Support authentication
42 class Socks5Command():
43 """Information about an incoming socks5 command."""
44 def __init__(self, cmd, atyp, addr, port, username, password):
45 self.cmd = cmd # Command (one of Command.*)
46 self.atyp = atyp # Address type (one of AddressType.*)
47 self.addr = addr # Address
48 self.port = port # Port to connect to
49 self.username = username
50 self.password = password
51 def __repr__(self):
52 return 'Socks5Command(%s,%s,%s,%s,%s,%s)' % (self.cmd, self.atyp, self.addr, self.port, self.username, self.password)
54 class Socks5Connection():
55 def __init__(self, serv, conn, peer):
56 self.serv = serv
57 self.conn = conn
58 self.peer = peer
60 def handle(self):
61 """Handle socks5 request according to RFC192."""
62 try:
63 # Verify socks version
64 ver = recvall(self.conn, 1)[0]
65 if ver != 0x05:
66 raise IOError('Invalid socks version %i' % ver)
67 # Choose authentication method
68 nmethods = recvall(self.conn, 1)[0]
69 methods = bytearray(recvall(self.conn, nmethods))
70 method = None
71 if 0x02 in methods and self.serv.conf.auth:
72 method = 0x02 # username/password
73 elif 0x00 in methods and self.serv.conf.unauth:
74 method = 0x00 # unauthenticated
75 if method is None:
76 raise IOError('No supported authentication method was offered')
77 # Send response
78 self.conn.sendall(bytearray([0x05, method]))
79 # Read authentication (optional)
80 username = None
81 password = None
82 if method == 0x02:
83 ver = recvall(self.conn, 1)[0]
84 if ver != 0x01:
85 raise IOError('Invalid auth packet version %i' % ver)
86 ulen = recvall(self.conn, 1)[0]
87 username = str(recvall(self.conn, ulen))
88 plen = recvall(self.conn, 1)[0]
89 password = str(recvall(self.conn, plen))
90 # Send authentication response
91 self.conn.sendall(bytearray([0x01, 0x00]))
93 # Read connect request
94 ver, cmd, _, atyp = recvall(self.conn, 4)
95 if ver != 0x05:
96 raise IOError('Invalid socks version %i in connect request' % ver)
97 if cmd != Command.CONNECT:
98 raise IOError('Unhandled command %i in connect request' % cmd)
100 if atyp == AddressType.IPV4:
101 addr = recvall(self.conn, 4)
102 elif atyp == AddressType.DOMAINNAME:
103 n = recvall(self.conn, 1)[0]
104 addr = recvall(self.conn, n)
105 elif atyp == AddressType.IPV6:
106 addr = recvall(self.conn, 16)
107 else:
108 raise IOError('Unknown address type %i' % atyp)
109 port_hi,port_lo = recvall(self.conn, 2)
110 port = (port_hi << 8) | port_lo
112 # Send dummy response
113 self.conn.sendall(bytearray([0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]))
115 cmdin = Socks5Command(cmd, atyp, addr, port, username, password)
116 self.serv.queue.put(cmdin)
117 logger.info('Proxy: %s', cmdin)
118 # Fall through to disconnect
119 except Exception as e:
120 logger.exception("socks5 request handling failed.")
121 self.serv.queue.put(e)
122 finally:
123 self.conn.close()
125 class Socks5Server():
126 def __init__(self, conf):
127 self.conf = conf
128 self.s = socket.socket(conf.af)
129 self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
130 self.s.bind(conf.addr)
131 self.s.listen(5)
132 self.running = False
133 self.thread = None
134 self.queue = queue.Queue() # report connections and exceptions to client
136 def run(self):
137 while self.running:
138 (sockconn, peer) = self.s.accept()
139 if self.running:
140 conn = Socks5Connection(self, sockconn, peer)
141 thread = threading.Thread(None, conn.handle)
142 thread.daemon = True
143 thread.start()
145 def start(self):
146 assert(not self.running)
147 self.running = True
148 self.thread = threading.Thread(None, self.run)
149 self.thread.daemon = True
150 self.thread.start()
152 def stop(self):
153 self.running = False
154 # connect to self to end run loop
155 s = socket.socket(self.conf.af)
156 s.connect(self.conf.addr)
157 s.close()
158 self.thread.join()