browsertime 14.20.2 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -1
- package/README.md +5 -5
- package/lib/chrome/webdriver/chromium.js +0 -9
- package/lib/chrome/webdriver/setupChromiumOptions.js +18 -29
- package/lib/connectivity/index.js +0 -9
- package/lib/core/engine/index.js +40 -59
- package/lib/core/engine/iteration.js +5 -2
- package/lib/core/seleniumRunner.js +5 -15
- package/lib/firefox/settings/firefoxPreferences.js +1 -4
- package/lib/firefox/webdriver/builder.js +0 -14
- package/lib/firefox/webdriver/firefox.js +20 -21
- package/lib/support/cli.js +2 -14
- package/lib/support/dns.js +44 -0
- package/package.json +34 -26
- package/browserscripts/browser/appConstants.js +0 -9
- package/browserscripts/browser/asyncAppConstants.js +0 -11
- package/lib/chrome/speedline.js +0 -84
- package/lib/connectivity/tsProxy.js +0 -108
- package/vendor/tsproxy.py +0 -842
package/vendor/tsproxy.py
DELETED
|
@@ -1,842 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
"""
|
|
3
|
-
Copyright 2016 Google Inc. All Rights Reserved.
|
|
4
|
-
|
|
5
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
you may not use this file except in compliance with the License.
|
|
7
|
-
You may obtain a copy of the License at
|
|
8
|
-
|
|
9
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
|
|
11
|
-
Unless required by applicable law or agreed to in writing, software
|
|
12
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
-
See the License for the specific language governing permissions and
|
|
15
|
-
limitations under the License.
|
|
16
|
-
"""
|
|
17
|
-
import asyncore
|
|
18
|
-
import gc
|
|
19
|
-
import logging
|
|
20
|
-
import platform
|
|
21
|
-
try:
|
|
22
|
-
from Queue import Queue
|
|
23
|
-
from Queue import Empty
|
|
24
|
-
except ImportError:
|
|
25
|
-
from queue import Queue
|
|
26
|
-
from queue import Empty
|
|
27
|
-
import re
|
|
28
|
-
import signal
|
|
29
|
-
import socket
|
|
30
|
-
import sys
|
|
31
|
-
import threading
|
|
32
|
-
import time
|
|
33
|
-
|
|
34
|
-
server = None
|
|
35
|
-
in_pipe = None
|
|
36
|
-
out_pipe = None
|
|
37
|
-
must_exit = False
|
|
38
|
-
options = None
|
|
39
|
-
dest_addresses = None
|
|
40
|
-
connections = {}
|
|
41
|
-
dns_cache = {}
|
|
42
|
-
port_mappings = None
|
|
43
|
-
map_localhost = False
|
|
44
|
-
needs_flush = False
|
|
45
|
-
flush_pipes = False
|
|
46
|
-
last_activity = None
|
|
47
|
-
last_client_disconnected = None
|
|
48
|
-
REMOVE_TCP_OVERHEAD = 1460.0 / 1500.0
|
|
49
|
-
lock = threading.Lock()
|
|
50
|
-
background_activity_count = 0
|
|
51
|
-
current_time = time.clock if sys.platform == "win32" else time.time
|
|
52
|
-
try:
|
|
53
|
-
import monotonic
|
|
54
|
-
current_time = monotonic.monotonic
|
|
55
|
-
except Exception:
|
|
56
|
-
pass
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def PrintMessage(msg):
|
|
60
|
-
# Print the message to stdout & flush to make sure that the message is not
|
|
61
|
-
# buffered when tsproxy is run as a subprocess.
|
|
62
|
-
sys.stdout.write(msg)
|
|
63
|
-
sys.stdout.flush()
|
|
64
|
-
|
|
65
|
-
########################################################################################################################
|
|
66
|
-
# Traffic-shaping pipe (just passthrough for now)
|
|
67
|
-
########################################################################################################################
|
|
68
|
-
class TSPipe():
|
|
69
|
-
PIPE_IN = 0
|
|
70
|
-
PIPE_OUT = 1
|
|
71
|
-
|
|
72
|
-
def __init__(self, direction, latency, kbps):
|
|
73
|
-
self.direction = direction
|
|
74
|
-
self.latency = latency
|
|
75
|
-
self.kbps = kbps
|
|
76
|
-
self.queue = Queue()
|
|
77
|
-
self.last_tick = current_time()
|
|
78
|
-
self.next_message = None
|
|
79
|
-
self.available_bytes = .0
|
|
80
|
-
self.peer = 'server'
|
|
81
|
-
if self.direction == self.PIPE_IN:
|
|
82
|
-
self.peer = 'client'
|
|
83
|
-
|
|
84
|
-
def SendMessage(self, message, main_thread = True):
|
|
85
|
-
global connections, in_pipe, out_pipe
|
|
86
|
-
message_sent = False
|
|
87
|
-
now = current_time()
|
|
88
|
-
if message['message'] == 'closed':
|
|
89
|
-
message['time'] = now
|
|
90
|
-
else:
|
|
91
|
-
message['time'] = current_time() + self.latency
|
|
92
|
-
message['size'] = .0
|
|
93
|
-
if 'data' in message:
|
|
94
|
-
message['size'] = float(len(message['data']))
|
|
95
|
-
try:
|
|
96
|
-
connection_id = message['connection']
|
|
97
|
-
# Send messages directly, bypassing the queues is throttling is disabled and we are on the main thread
|
|
98
|
-
if main_thread and connection_id in connections and self.peer in connections[connection_id]and self.latency == 0 and self.kbps == .0:
|
|
99
|
-
message_sent = self.SendPeerMessage(message)
|
|
100
|
-
except:
|
|
101
|
-
pass
|
|
102
|
-
if not message_sent:
|
|
103
|
-
try:
|
|
104
|
-
self.queue.put(message)
|
|
105
|
-
except:
|
|
106
|
-
pass
|
|
107
|
-
|
|
108
|
-
def SendPeerMessage(self, message):
|
|
109
|
-
global last_activity, last_client_disconnected
|
|
110
|
-
last_activity = current_time()
|
|
111
|
-
message_sent = False
|
|
112
|
-
connection_id = message['connection']
|
|
113
|
-
if connection_id in connections:
|
|
114
|
-
if self.peer in connections[connection_id]:
|
|
115
|
-
try:
|
|
116
|
-
connections[connection_id][self.peer].handle_message(message)
|
|
117
|
-
message_sent = True
|
|
118
|
-
except:
|
|
119
|
-
# Clean up any disconnected connections
|
|
120
|
-
try:
|
|
121
|
-
connections[connection_id]['server'].close()
|
|
122
|
-
except:
|
|
123
|
-
pass
|
|
124
|
-
try:
|
|
125
|
-
connections[connection_id]['client'].close()
|
|
126
|
-
except:
|
|
127
|
-
pass
|
|
128
|
-
del connections[connection_id]
|
|
129
|
-
if not connections:
|
|
130
|
-
last_client_disconnected = current_time()
|
|
131
|
-
logging.info('[{0:d}] Last connection closed'.format(self.client_id))
|
|
132
|
-
return message_sent
|
|
133
|
-
|
|
134
|
-
def tick(self):
|
|
135
|
-
global connections
|
|
136
|
-
global flush_pipes
|
|
137
|
-
next_packet_time = None
|
|
138
|
-
processed_messages = False
|
|
139
|
-
now = current_time()
|
|
140
|
-
try:
|
|
141
|
-
if self.next_message is None:
|
|
142
|
-
self.next_message = self.queue.get_nowait()
|
|
143
|
-
|
|
144
|
-
# Accumulate bandwidth if an available packet/message was waiting since our last tick
|
|
145
|
-
if self.next_message is not None and self.kbps > .0 and self.next_message['time'] <= now:
|
|
146
|
-
elapsed = now - self.last_tick
|
|
147
|
-
accumulated_bytes = elapsed * self.kbps * 1000.0 / 8.0
|
|
148
|
-
self.available_bytes += accumulated_bytes
|
|
149
|
-
|
|
150
|
-
# process messages as long as the next message is sendable (latency or available bytes)
|
|
151
|
-
while (self.next_message is not None) and\
|
|
152
|
-
(flush_pipes or ((self.next_message['time'] <= now) and
|
|
153
|
-
(self.kbps <= .0 or self.next_message['size'] <= self.available_bytes))):
|
|
154
|
-
processed_messages = True
|
|
155
|
-
if self.kbps > .0:
|
|
156
|
-
self.available_bytes -= self.next_message['size']
|
|
157
|
-
self.SendPeerMessage(self.next_message)
|
|
158
|
-
self.next_message = None
|
|
159
|
-
self.next_message = self.queue.get_nowait()
|
|
160
|
-
except Empty:
|
|
161
|
-
pass
|
|
162
|
-
except Exception as e:
|
|
163
|
-
logging.exception('Tick Exception')
|
|
164
|
-
|
|
165
|
-
# Only accumulate bytes while we have messages that are ready to send
|
|
166
|
-
if self.next_message is None or self.next_message['time'] > now:
|
|
167
|
-
self.available_bytes = .0
|
|
168
|
-
self.last_tick = now
|
|
169
|
-
|
|
170
|
-
# Figure out how long until the next packet can be sent
|
|
171
|
-
if self.next_message is not None:
|
|
172
|
-
# First, just the latency
|
|
173
|
-
next_packet_time = self.next_message['time'] - now
|
|
174
|
-
# Additional time for bandwidth
|
|
175
|
-
if self.kbps > .0:
|
|
176
|
-
accumulated_bytes = self.available_bytes + next_packet_time * self.kbps * 1000.0 / 8.0
|
|
177
|
-
needed_bytes = self.next_message['size'] - accumulated_bytes
|
|
178
|
-
if needed_bytes > 0:
|
|
179
|
-
needed_time = needed_bytes / (self.kbps * 1000.0 / 8.0)
|
|
180
|
-
next_packet_time += needed_time
|
|
181
|
-
|
|
182
|
-
return next_packet_time
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
########################################################################################################################
|
|
186
|
-
# Threaded DNS resolver
|
|
187
|
-
########################################################################################################################
|
|
188
|
-
class AsyncDNS(threading.Thread):
|
|
189
|
-
def __init__(self, client_id, hostname, port, is_localhost, result_pipe):
|
|
190
|
-
threading.Thread.__init__(self)
|
|
191
|
-
self.hostname = hostname
|
|
192
|
-
self.port = port
|
|
193
|
-
self.client_id = client_id
|
|
194
|
-
self.is_localhost = is_localhost
|
|
195
|
-
self.result_pipe = result_pipe
|
|
196
|
-
|
|
197
|
-
def run(self):
|
|
198
|
-
global lock, background_activity_count
|
|
199
|
-
try:
|
|
200
|
-
logging.debug('[{0:d}] AsyncDNS - calling getaddrinfo for {1}:{2:d}'.format(self.client_id, self.hostname, self.port))
|
|
201
|
-
addresses = socket.getaddrinfo(self.hostname, self.port)
|
|
202
|
-
logging.info('[{0:d}] Resolving {1}:{2:d} Completed'.format(self.client_id, self.hostname, self.port))
|
|
203
|
-
except:
|
|
204
|
-
addresses = ()
|
|
205
|
-
logging.info('[{0:d}] Resolving {1}:{2:d} Failed'.format(self.client_id, self.hostname, self.port))
|
|
206
|
-
message = {'message': 'resolved', 'connection': self.client_id, 'addresses': addresses, 'localhost': self.is_localhost}
|
|
207
|
-
self.result_pipe.SendMessage(message, False)
|
|
208
|
-
lock.acquire()
|
|
209
|
-
if background_activity_count > 0:
|
|
210
|
-
background_activity_count -= 1
|
|
211
|
-
lock.release()
|
|
212
|
-
# open and close a local socket which will interrupt the long polling loop to process the message
|
|
213
|
-
s = socket.socket()
|
|
214
|
-
s.connect((server.ipaddr, server.port))
|
|
215
|
-
s.close()
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
########################################################################################################################
|
|
219
|
-
# TCP Client
|
|
220
|
-
########################################################################################################################
|
|
221
|
-
class TCPConnection(asyncore.dispatcher):
|
|
222
|
-
STATE_ERROR = -1
|
|
223
|
-
STATE_IDLE = 0
|
|
224
|
-
STATE_RESOLVING = 1
|
|
225
|
-
STATE_CONNECTING = 2
|
|
226
|
-
STATE_CONNECTED = 3
|
|
227
|
-
|
|
228
|
-
def __init__(self, client_id):
|
|
229
|
-
global options
|
|
230
|
-
asyncore.dispatcher.__init__(self)
|
|
231
|
-
self.client_id = client_id
|
|
232
|
-
self.state = self.STATE_IDLE
|
|
233
|
-
self.buffer = ''
|
|
234
|
-
self.addr = None
|
|
235
|
-
self.dns_thread = None
|
|
236
|
-
self.hostname = None
|
|
237
|
-
self.port = None
|
|
238
|
-
self.needs_config = True
|
|
239
|
-
self.needs_close = False
|
|
240
|
-
self.did_resolve = False
|
|
241
|
-
|
|
242
|
-
def SendMessage(self, type, message):
|
|
243
|
-
message['message'] = type
|
|
244
|
-
message['connection'] = self.client_id
|
|
245
|
-
in_pipe.SendMessage(message)
|
|
246
|
-
|
|
247
|
-
def handle_message(self, message):
|
|
248
|
-
if message['message'] == 'data' and 'data' in message and len(message['data']):
|
|
249
|
-
self.buffer += message['data']
|
|
250
|
-
if self.state == self.STATE_CONNECTED:
|
|
251
|
-
self.handle_write()
|
|
252
|
-
elif message['message'] == 'resolve':
|
|
253
|
-
self.HandleResolve(message)
|
|
254
|
-
elif message['message'] == 'connect':
|
|
255
|
-
self.HandleConnect(message)
|
|
256
|
-
elif message['message'] == 'closed':
|
|
257
|
-
if len(self.buffer) == 0:
|
|
258
|
-
self.handle_close()
|
|
259
|
-
else:
|
|
260
|
-
self.needs_close = True
|
|
261
|
-
|
|
262
|
-
def handle_error(self):
|
|
263
|
-
logging.warning('[{0:d}] Error'.format(self.client_id))
|
|
264
|
-
if self.state == self.STATE_CONNECTING:
|
|
265
|
-
self.SendMessage('connected', {'success': False, 'address': self.addr})
|
|
266
|
-
|
|
267
|
-
def handle_close(self):
|
|
268
|
-
global last_client_disconnected
|
|
269
|
-
logging.info('[{0:d}] Server Connection Closed'.format(self.client_id))
|
|
270
|
-
self.state = self.STATE_ERROR
|
|
271
|
-
self.close()
|
|
272
|
-
try:
|
|
273
|
-
if self.client_id in connections:
|
|
274
|
-
if 'server' in connections[self.client_id]:
|
|
275
|
-
del connections[self.client_id]['server']
|
|
276
|
-
if 'client' in connections[self.client_id]:
|
|
277
|
-
self.SendMessage('closed', {})
|
|
278
|
-
else:
|
|
279
|
-
del connections[self.client_id]
|
|
280
|
-
if not connections:
|
|
281
|
-
last_client_disconnected = current_time()
|
|
282
|
-
logging.info('[{0:d}] Last Browser disconnected'.format(self.client_id))
|
|
283
|
-
except:
|
|
284
|
-
pass
|
|
285
|
-
|
|
286
|
-
def handle_connect(self):
|
|
287
|
-
if self.state == self.STATE_CONNECTING:
|
|
288
|
-
self.state = self.STATE_CONNECTED
|
|
289
|
-
self.SendMessage('connected', {'success': True, 'address': self.addr})
|
|
290
|
-
logging.info('[{0:d}] Connected'.format(self.client_id))
|
|
291
|
-
self.handle_write()
|
|
292
|
-
|
|
293
|
-
def writable(self):
|
|
294
|
-
if self.state == self.STATE_CONNECTING:
|
|
295
|
-
return True
|
|
296
|
-
return len(self.buffer) > 0
|
|
297
|
-
|
|
298
|
-
def handle_write(self):
|
|
299
|
-
if self.needs_config:
|
|
300
|
-
self.needs_config = False
|
|
301
|
-
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
302
|
-
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 128 * 1024)
|
|
303
|
-
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 128 * 1024)
|
|
304
|
-
if len(self.buffer) > 0:
|
|
305
|
-
sent = self.send(self.buffer)
|
|
306
|
-
logging.debug('[{0:d}] TCP => {1:d} byte(s)'.format(self.client_id, sent))
|
|
307
|
-
self.buffer = self.buffer[sent:]
|
|
308
|
-
if self.needs_close and len(self.buffer) == 0:
|
|
309
|
-
self.needs_close = False
|
|
310
|
-
self.handle_close()
|
|
311
|
-
|
|
312
|
-
def handle_read(self):
|
|
313
|
-
try:
|
|
314
|
-
while True:
|
|
315
|
-
data = self.recv(1460)
|
|
316
|
-
if data:
|
|
317
|
-
if self.state == self.STATE_CONNECTED:
|
|
318
|
-
logging.debug('[{0:d}] TCP <= {1:d} byte(s)'.format(self.client_id, len(data)))
|
|
319
|
-
self.SendMessage('data', {'data': data})
|
|
320
|
-
else:
|
|
321
|
-
return
|
|
322
|
-
except:
|
|
323
|
-
pass
|
|
324
|
-
|
|
325
|
-
def HandleResolve(self, message):
|
|
326
|
-
global in_pipe, map_localhost, lock, background_activity_count
|
|
327
|
-
self.did_resolve = True
|
|
328
|
-
is_localhost = False
|
|
329
|
-
if 'hostname' in message:
|
|
330
|
-
self.hostname = message['hostname']
|
|
331
|
-
self.port = 0
|
|
332
|
-
if 'port' in message:
|
|
333
|
-
self.port = message['port']
|
|
334
|
-
logging.info('[{0:d}] Resolving {1}:{2:d}'.format(self.client_id, self.hostname, self.port))
|
|
335
|
-
if self.hostname == 'localhost':
|
|
336
|
-
self.hostname = '127.0.0.1'
|
|
337
|
-
if self.hostname == '127.0.0.1':
|
|
338
|
-
logging.info('[{0:d}] Connection to localhost detected'.format(self.client_id))
|
|
339
|
-
is_localhost = True
|
|
340
|
-
if (dest_addresses is not None) and (not is_localhost or map_localhost):
|
|
341
|
-
logging.info('[{0:d}] Resolving {1}:{2:d} to mapped address {3}'.format(self.client_id, self.hostname, self.port, dest_addresses))
|
|
342
|
-
self.SendMessage('resolved', {'addresses': dest_addresses, 'localhost': False})
|
|
343
|
-
else:
|
|
344
|
-
lock.acquire()
|
|
345
|
-
background_activity_count += 1
|
|
346
|
-
lock.release()
|
|
347
|
-
self.state = self.STATE_RESOLVING
|
|
348
|
-
self.dns_thread = AsyncDNS(self.client_id, self.hostname, self.port, is_localhost, in_pipe)
|
|
349
|
-
self.dns_thread.start()
|
|
350
|
-
|
|
351
|
-
def HandleConnect(self, message):
|
|
352
|
-
global map_localhost
|
|
353
|
-
if 'addresses' in message and len(message['addresses']):
|
|
354
|
-
self.state = self.STATE_CONNECTING
|
|
355
|
-
is_localhost = False
|
|
356
|
-
if 'localhost' in message:
|
|
357
|
-
is_localhost = message['localhost']
|
|
358
|
-
elif not self.did_resolve and message['addresses'][0] == '127.0.0.1':
|
|
359
|
-
logging.info('[{0:d}] Connection to localhost detected'.format(self.client_id))
|
|
360
|
-
is_localhost = True
|
|
361
|
-
if (dest_addresses is not None) and (not is_localhost or map_localhost):
|
|
362
|
-
self.addr = dest_addresses[0]
|
|
363
|
-
else:
|
|
364
|
-
self.addr = message['addresses'][0]
|
|
365
|
-
self.create_socket(self.addr[0], socket.SOCK_STREAM)
|
|
366
|
-
addr = self.addr[4][0]
|
|
367
|
-
if not is_localhost or map_localhost:
|
|
368
|
-
port = GetDestPort(message['port'])
|
|
369
|
-
else:
|
|
370
|
-
port = message['port']
|
|
371
|
-
logging.info('[{0:d}] Connecting to {1}:{2:d}'.format(self.client_id, addr, port))
|
|
372
|
-
self.connect((addr, port))
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
########################################################################################################################
|
|
376
|
-
# Socks5 Server
|
|
377
|
-
########################################################################################################################
|
|
378
|
-
class Socks5Server(asyncore.dispatcher):
|
|
379
|
-
|
|
380
|
-
def __init__(self, host, port):
|
|
381
|
-
asyncore.dispatcher.__init__(self)
|
|
382
|
-
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
383
|
-
try:
|
|
384
|
-
self.set_reuse_addr()
|
|
385
|
-
self.bind((host, port))
|
|
386
|
-
self.listen(socket.SOMAXCONN)
|
|
387
|
-
self.ipaddr, self.port = self.socket.getsockname()
|
|
388
|
-
self.current_client_id = 0
|
|
389
|
-
except:
|
|
390
|
-
PrintMessage("Unable to listen on {0}:{1}. Is the port already in use?".format(host, port))
|
|
391
|
-
exit(1)
|
|
392
|
-
|
|
393
|
-
def handle_accept(self):
|
|
394
|
-
global connections, last_client_disconnected
|
|
395
|
-
pair = self.accept()
|
|
396
|
-
if pair is not None:
|
|
397
|
-
last_client_disconnected = None
|
|
398
|
-
sock, addr = pair
|
|
399
|
-
self.current_client_id += 1
|
|
400
|
-
logging.info('[{0:d}] Incoming connection from {1}'.format(self.current_client_id, repr(addr)))
|
|
401
|
-
connections[self.current_client_id] = {
|
|
402
|
-
'client' : Socks5Connection(sock, self.current_client_id),
|
|
403
|
-
'server' : None
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
# Socks5 reference: https://en.wikipedia.org/wiki/SOCKS#SOCKS5
|
|
408
|
-
class Socks5Connection(asyncore.dispatcher):
|
|
409
|
-
STATE_ERROR = -1
|
|
410
|
-
STATE_WAITING_FOR_HANDSHAKE = 0
|
|
411
|
-
STATE_WAITING_FOR_CONNECT_REQUEST = 1
|
|
412
|
-
STATE_RESOLVING = 2
|
|
413
|
-
STATE_CONNECTING = 3
|
|
414
|
-
STATE_CONNECTED = 4
|
|
415
|
-
|
|
416
|
-
def __init__(self, connected_socket, client_id):
|
|
417
|
-
global options
|
|
418
|
-
asyncore.dispatcher.__init__(self, connected_socket)
|
|
419
|
-
self.client_id = client_id
|
|
420
|
-
self.state = self.STATE_WAITING_FOR_HANDSHAKE
|
|
421
|
-
self.ip = None
|
|
422
|
-
self.addresses = None
|
|
423
|
-
self.hostname = None
|
|
424
|
-
self.port = None
|
|
425
|
-
self.requested_address = None
|
|
426
|
-
self.buffer = ''
|
|
427
|
-
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
428
|
-
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 128 * 1024)
|
|
429
|
-
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 128 * 1024)
|
|
430
|
-
self.needs_close = False
|
|
431
|
-
|
|
432
|
-
def SendMessage(self, type, message):
|
|
433
|
-
message['message'] = type
|
|
434
|
-
message['connection'] = self.client_id
|
|
435
|
-
out_pipe.SendMessage(message)
|
|
436
|
-
|
|
437
|
-
def handle_message(self, message):
|
|
438
|
-
if message['message'] == 'data' and 'data' in message and len(message['data']) > 0:
|
|
439
|
-
self.buffer += message['data']
|
|
440
|
-
if self.state == self.STATE_CONNECTED:
|
|
441
|
-
self.handle_write()
|
|
442
|
-
elif message['message'] == 'resolved':
|
|
443
|
-
self.HandleResolved(message)
|
|
444
|
-
elif message['message'] == 'connected':
|
|
445
|
-
self.HandleConnected(message)
|
|
446
|
-
self.handle_write()
|
|
447
|
-
elif message['message'] == 'closed':
|
|
448
|
-
if len(self.buffer) == 0:
|
|
449
|
-
logging.info('[{0:d}] Server connection close being processed, closing Browser connection'.format(self.client_id))
|
|
450
|
-
self.handle_close()
|
|
451
|
-
else:
|
|
452
|
-
logging.info('[{0:d}] Server connection close being processed, queuing browser connection close'.format(self.client_id))
|
|
453
|
-
self.needs_close = True
|
|
454
|
-
|
|
455
|
-
def writable(self):
|
|
456
|
-
return len(self.buffer) > 0
|
|
457
|
-
|
|
458
|
-
def handle_write(self):
|
|
459
|
-
if len(self.buffer) > 0:
|
|
460
|
-
sent = self.send(self.buffer)
|
|
461
|
-
logging.debug('[{0:d}] SOCKS <= {1:d} byte(s)'.format(self.client_id, sent))
|
|
462
|
-
self.buffer = self.buffer[sent:]
|
|
463
|
-
if self.needs_close and len(self.buffer) == 0:
|
|
464
|
-
logging.info('[{0:d}] queued browser connection close being processed, closing Browser connection'.format(self.client_id))
|
|
465
|
-
self.needs_close = False
|
|
466
|
-
self.handle_close()
|
|
467
|
-
|
|
468
|
-
def handle_read(self):
|
|
469
|
-
global connections
|
|
470
|
-
global dns_cache
|
|
471
|
-
try:
|
|
472
|
-
while True:
|
|
473
|
-
# Consume in up-to packet-sized chunks (TCP packet payload as 1460 bytes from 1500 byte ethernet frames)
|
|
474
|
-
data = self.recv(1460)
|
|
475
|
-
if data:
|
|
476
|
-
data_len = len(data)
|
|
477
|
-
if self.state == self.STATE_CONNECTED:
|
|
478
|
-
logging.debug('[{0:d}] SOCKS => {1:d} byte(s)'.format(self.client_id, data_len))
|
|
479
|
-
self.SendMessage('data', {'data': data})
|
|
480
|
-
elif self.state == self.STATE_WAITING_FOR_HANDSHAKE:
|
|
481
|
-
self.state = self.STATE_ERROR #default to an error state, set correctly if things work out
|
|
482
|
-
if data_len >= 2 and ord(data[0]) == 0x05:
|
|
483
|
-
supports_no_auth = False
|
|
484
|
-
auth_count = ord(data[1])
|
|
485
|
-
if data_len == auth_count + 2:
|
|
486
|
-
for i in range(auth_count):
|
|
487
|
-
offset = i + 2
|
|
488
|
-
if ord(data[offset]) == 0:
|
|
489
|
-
supports_no_auth = True
|
|
490
|
-
if supports_no_auth:
|
|
491
|
-
# Respond with a message that "No Authentication" was agreed to
|
|
492
|
-
logging.info('[{0:d}] New Socks5 client'.format(self.client_id))
|
|
493
|
-
response = chr(0x05) + chr(0x00)
|
|
494
|
-
self.state = self.STATE_WAITING_FOR_CONNECT_REQUEST
|
|
495
|
-
self.buffer += response
|
|
496
|
-
self.handle_write()
|
|
497
|
-
elif self.state == self.STATE_WAITING_FOR_CONNECT_REQUEST:
|
|
498
|
-
self.state = self.STATE_ERROR #default to an error state, set correctly if things work out
|
|
499
|
-
if data_len >= 10 and ord(data[0]) == 0x05 and ord(data[2]) == 0x00:
|
|
500
|
-
if ord(data[1]) == 0x01: #TCP connection (only supported method for now)
|
|
501
|
-
connections[self.client_id]['server'] = TCPConnection(self.client_id)
|
|
502
|
-
self.requested_address = data[3:]
|
|
503
|
-
port_offset = 0
|
|
504
|
-
if ord(data[3]) == 0x01:
|
|
505
|
-
port_offset = 8
|
|
506
|
-
self.ip = '{0:d}.{1:d}.{2:d}.{3:d}'.format(ord(data[4]), ord(data[5]), ord(data[6]), ord(data[7]))
|
|
507
|
-
elif ord(data[3]) == 0x03:
|
|
508
|
-
name_len = ord(data[4])
|
|
509
|
-
if data_len >= 6 + name_len:
|
|
510
|
-
port_offset = 5 + name_len
|
|
511
|
-
self.hostname = data[5:5 + name_len]
|
|
512
|
-
elif ord(data[3]) == 0x04 and data_len >= 22:
|
|
513
|
-
port_offset = 20
|
|
514
|
-
self.ip = ''
|
|
515
|
-
for i in range(16):
|
|
516
|
-
self.ip += '{0:02x}'.format(ord(data[4 + i]))
|
|
517
|
-
if i % 2 and i < 15:
|
|
518
|
-
self.ip += ':'
|
|
519
|
-
if port_offset and connections[self.client_id]['server'] is not None:
|
|
520
|
-
self.port = 256 * ord(data[port_offset]) + ord(data[port_offset + 1])
|
|
521
|
-
if self.port:
|
|
522
|
-
if self.ip is None and self.hostname is not None:
|
|
523
|
-
if dns_cache is not None and self.hostname in dns_cache:
|
|
524
|
-
self.state = self.STATE_CONNECTING
|
|
525
|
-
cache_entry = dns_cache[self.hostname]
|
|
526
|
-
self.addresses = cache_entry['addresses']
|
|
527
|
-
self.SendMessage('connect', {'addresses': self.addresses, 'port': self.port, 'localhost': cache_entry['localhost']})
|
|
528
|
-
else:
|
|
529
|
-
self.state = self.STATE_RESOLVING
|
|
530
|
-
self.SendMessage('resolve', {'hostname': self.hostname, 'port': self.port})
|
|
531
|
-
elif self.ip is not None:
|
|
532
|
-
self.state = self.STATE_CONNECTING
|
|
533
|
-
logging.debug('[{0:d}] Socks Connect - calling getaddrinfo for {1}:{2:d}'.format(self.client_id, self.ip, self.port))
|
|
534
|
-
self.addresses = socket.getaddrinfo(self.ip, self.port)
|
|
535
|
-
self.SendMessage('connect', {'addresses': self.addresses, 'port': self.port})
|
|
536
|
-
else:
|
|
537
|
-
return
|
|
538
|
-
except:
|
|
539
|
-
pass
|
|
540
|
-
|
|
541
|
-
def handle_close(self):
|
|
542
|
-
global last_client_disconnected
|
|
543
|
-
logging.info('[{0:d}] Browser Connection Closed by browser'.format(self.client_id))
|
|
544
|
-
self.state = self.STATE_ERROR
|
|
545
|
-
self.close()
|
|
546
|
-
try:
|
|
547
|
-
if self.client_id in connections:
|
|
548
|
-
if 'client' in connections[self.client_id]:
|
|
549
|
-
del connections[self.client_id]['client']
|
|
550
|
-
if 'server' in connections[self.client_id]:
|
|
551
|
-
self.SendMessage('closed', {})
|
|
552
|
-
else:
|
|
553
|
-
del connections[self.client_id]
|
|
554
|
-
if not connections:
|
|
555
|
-
last_client_disconnected = current_time()
|
|
556
|
-
logging.info('[{0:d}] Last Browser disconnected'.format(self.client_id))
|
|
557
|
-
except:
|
|
558
|
-
pass
|
|
559
|
-
|
|
560
|
-
def HandleResolved(self, message):
|
|
561
|
-
global dns_cache
|
|
562
|
-
if self.state == self.STATE_RESOLVING:
|
|
563
|
-
if 'addresses' in message and len(message['addresses']):
|
|
564
|
-
self.state = self.STATE_CONNECTING
|
|
565
|
-
self.addresses = message['addresses']
|
|
566
|
-
if dns_cache is not None:
|
|
567
|
-
dns_cache[self.hostname] = {'addresses': self.addresses, 'localhost': message['localhost']}
|
|
568
|
-
logging.debug('[{0:d}] Resolved {1}, Connecting'.format(self.client_id, self.hostname))
|
|
569
|
-
self.SendMessage('connect', {'addresses': self.addresses, 'port': self.port, 'localhost': message['localhost']})
|
|
570
|
-
else:
|
|
571
|
-
# Send host unreachable error
|
|
572
|
-
self.state = self.STATE_ERROR
|
|
573
|
-
self.buffer += chr(0x05) + chr(0x04) + self.requested_address
|
|
574
|
-
self.handle_write()
|
|
575
|
-
|
|
576
|
-
def HandleConnected(self, message):
|
|
577
|
-
if 'success' in message and self.state == self.STATE_CONNECTING:
|
|
578
|
-
response = chr(0x05)
|
|
579
|
-
if message['success']:
|
|
580
|
-
response += chr(0x00)
|
|
581
|
-
logging.debug('[{0:d}] Connected to {1}'.format(self.client_id, self.hostname))
|
|
582
|
-
self.state = self.STATE_CONNECTED
|
|
583
|
-
else:
|
|
584
|
-
response += chr(0x04)
|
|
585
|
-
self.state = self.STATE_ERROR
|
|
586
|
-
response += chr(0x00)
|
|
587
|
-
response += self.requested_address
|
|
588
|
-
self.buffer += response
|
|
589
|
-
self.handle_write()
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
########################################################################################################################
|
|
593
|
-
# stdin command processor
|
|
594
|
-
########################################################################################################################
|
|
595
|
-
class CommandProcessor():
|
|
596
|
-
def __init__(self):
|
|
597
|
-
thread = threading.Thread(target = self.run, args=())
|
|
598
|
-
thread.daemon = True
|
|
599
|
-
thread.start()
|
|
600
|
-
|
|
601
|
-
def run(self):
|
|
602
|
-
global must_exit
|
|
603
|
-
while not must_exit:
|
|
604
|
-
for line in iter(sys.stdin.readline, ''):
|
|
605
|
-
self.ProcessCommand(line.strip())
|
|
606
|
-
|
|
607
|
-
def ProcessCommand(self, input):
|
|
608
|
-
global in_pipe
|
|
609
|
-
global out_pipe
|
|
610
|
-
global needs_flush
|
|
611
|
-
global REMOVE_TCP_OVERHEAD
|
|
612
|
-
global port_mappings
|
|
613
|
-
global server
|
|
614
|
-
if len(input):
|
|
615
|
-
ok = False
|
|
616
|
-
try:
|
|
617
|
-
command = input.split()
|
|
618
|
-
if len(command) and len(command[0]):
|
|
619
|
-
if command[0].lower() == 'flush':
|
|
620
|
-
ok = True
|
|
621
|
-
elif command[0].lower() == 'set' and len(command) >= 3:
|
|
622
|
-
if command[1].lower() == 'rtt' and len(command[2]):
|
|
623
|
-
rtt = float(command[2])
|
|
624
|
-
latency = rtt / 2000.0
|
|
625
|
-
in_pipe.latency = latency
|
|
626
|
-
out_pipe.latency = latency
|
|
627
|
-
ok = True
|
|
628
|
-
elif command[1].lower() == 'inkbps' and len(command[2]):
|
|
629
|
-
in_pipe.kbps = float(command[2]) * REMOVE_TCP_OVERHEAD
|
|
630
|
-
ok = True
|
|
631
|
-
elif command[1].lower() == 'outkbps' and len(command[2]):
|
|
632
|
-
out_pipe.kbps = float(command[2]) * REMOVE_TCP_OVERHEAD
|
|
633
|
-
ok = True
|
|
634
|
-
elif command[1].lower() == 'mapports' and len(command[2]):
|
|
635
|
-
SetPortMappings(command[2])
|
|
636
|
-
ok = True
|
|
637
|
-
elif command[0].lower() == 'reset' and len(command) >= 2:
|
|
638
|
-
if command[1].lower() == 'rtt' or command[1].lower() == 'all':
|
|
639
|
-
in_pipe.latency = 0
|
|
640
|
-
out_pipe.latency = 0
|
|
641
|
-
ok = True
|
|
642
|
-
if command[1].lower() == 'inkbps' or command[1].lower() == 'all':
|
|
643
|
-
in_pipe.kbps = 0
|
|
644
|
-
ok = True
|
|
645
|
-
if command[1].lower() == 'outkbps' or command[1].lower() == 'all':
|
|
646
|
-
out_pipe.kbps = 0
|
|
647
|
-
ok = True
|
|
648
|
-
if command[1].lower() == 'mapports' or command[1].lower() == 'all':
|
|
649
|
-
port_mappings = {}
|
|
650
|
-
ok = True
|
|
651
|
-
|
|
652
|
-
if ok:
|
|
653
|
-
needs_flush = True
|
|
654
|
-
except:
|
|
655
|
-
pass
|
|
656
|
-
if not ok:
|
|
657
|
-
PrintMessage('ERROR')
|
|
658
|
-
# open and close a local socket which will interrupt the long polling loop to process the flush
|
|
659
|
-
if needs_flush:
|
|
660
|
-
s = socket.socket()
|
|
661
|
-
s.connect((server.ipaddr, server.port))
|
|
662
|
-
s.close()
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
########################################################################################################################
|
|
666
|
-
# Main Entry Point
|
|
667
|
-
########################################################################################################################
|
|
668
|
-
def main():
|
|
669
|
-
global server
|
|
670
|
-
global options
|
|
671
|
-
global in_pipe
|
|
672
|
-
global out_pipe
|
|
673
|
-
global dest_addresses
|
|
674
|
-
global port_mappings
|
|
675
|
-
global map_localhost
|
|
676
|
-
global dns_cache
|
|
677
|
-
import argparse
|
|
678
|
-
global REMOVE_TCP_OVERHEAD
|
|
679
|
-
parser = argparse.ArgumentParser(description='Traffic-shaping socks5 proxy.',
|
|
680
|
-
prog='tsproxy')
|
|
681
|
-
parser.add_argument('-v', '--verbose', action='count', help="Increase verbosity (specify multiple times for more). -vvvv for full debug output.")
|
|
682
|
-
parser.add_argument('--logfile', help="Write log messages to given file instead of stdout.")
|
|
683
|
-
parser.add_argument('-b', '--bind', default='localhost', help="Server interface address (defaults to localhost).")
|
|
684
|
-
parser.add_argument('-p', '--port', type=int, default=1080, help="Server port (defaults to 1080, use 0 for randomly assigned).")
|
|
685
|
-
parser.add_argument('-r', '--rtt', type=float, default=.0, help="Round Trip Time Latency (in ms).")
|
|
686
|
-
parser.add_argument('-i', '--inkbps', type=float, default=.0, help="Download Bandwidth (in 1000 bits/s - Kbps).")
|
|
687
|
-
parser.add_argument('-o', '--outkbps', type=float, default=.0, help="Upload Bandwidth (in 1000 bits/s - Kbps).")
|
|
688
|
-
parser.add_argument('-w', '--window', type=int, default=10, help="Emulated TCP initial congestion window (defaults to 10).")
|
|
689
|
-
parser.add_argument('-d', '--desthost', help="Redirect all outbound connections to the specified host.")
|
|
690
|
-
parser.add_argument('-m', '--mapports', help="Remap outbound ports. Comma-separated list of original:new with * as a wildcard. --mapports '443:8443,*:8080'")
|
|
691
|
-
parser.add_argument('-l', '--localhost', action='store_true', default=False,
|
|
692
|
-
help="Include connections already destined for localhost/127.0.0.1 in the host and port remapping.")
|
|
693
|
-
parser.add_argument('-n', '--nodnscache', action='store_true', default=False, help="Disable internal DNS cache.")
|
|
694
|
-
parser.add_argument('-f', '--flushdnscache', action='store_true', default=False, help="Automatically flush the DNS cache 500ms after the last client disconnects.")
|
|
695
|
-
options = parser.parse_args()
|
|
696
|
-
|
|
697
|
-
# Set up logging
|
|
698
|
-
log_level = logging.CRITICAL
|
|
699
|
-
if options.verbose == 1:
|
|
700
|
-
log_level = logging.ERROR
|
|
701
|
-
elif options.verbose == 2:
|
|
702
|
-
log_level = logging.WARNING
|
|
703
|
-
elif options.verbose == 3:
|
|
704
|
-
log_level = logging.INFO
|
|
705
|
-
elif options.verbose >= 4:
|
|
706
|
-
log_level = logging.DEBUG
|
|
707
|
-
if options.logfile is not None:
|
|
708
|
-
logging.basicConfig(filename=options.logfile, level=log_level,
|
|
709
|
-
format="%(asctime)s.%(msecs)03d - %(message)s", datefmt="%H:%M:%S")
|
|
710
|
-
else:
|
|
711
|
-
logging.basicConfig(level=log_level, format="%(asctime)s.%(msecs)03d - %(message)s", datefmt="%H:%M:%S")
|
|
712
|
-
|
|
713
|
-
# Parse any port mappings
|
|
714
|
-
if options.mapports:
|
|
715
|
-
SetPortMappings(options.mapports)
|
|
716
|
-
|
|
717
|
-
if options.nodnscache:
|
|
718
|
-
dns_cache = None
|
|
719
|
-
|
|
720
|
-
map_localhost = options.localhost
|
|
721
|
-
|
|
722
|
-
# Resolve the address for a rewrite destination host if one was specified
|
|
723
|
-
if options.desthost:
|
|
724
|
-
logging.debug('Startup - calling getaddrinfo for {0}:{1:d}'.format(options.desthost, GetDestPort(80)))
|
|
725
|
-
dest_addresses = socket.getaddrinfo(options.desthost, GetDestPort(80))
|
|
726
|
-
|
|
727
|
-
# Set up the pipes. 1/2 of the latency gets applied in each direction (and /1000 to convert to seconds)
|
|
728
|
-
in_pipe = TSPipe(TSPipe.PIPE_IN, options.rtt / 2000.0, options.inkbps * REMOVE_TCP_OVERHEAD)
|
|
729
|
-
out_pipe = TSPipe(TSPipe.PIPE_OUT, options.rtt / 2000.0, options.outkbps * REMOVE_TCP_OVERHEAD)
|
|
730
|
-
|
|
731
|
-
signal.signal(signal.SIGINT, signal_handler)
|
|
732
|
-
server = Socks5Server(options.bind, options.port)
|
|
733
|
-
command_processor = CommandProcessor()
|
|
734
|
-
PrintMessage('Started Socks5 proxy server on {0}:{1:d}\nHit Ctrl-C to exit.'.format(server.ipaddr, server.port))
|
|
735
|
-
run_loop()
|
|
736
|
-
|
|
737
|
-
def signal_handler(signal, frame):
|
|
738
|
-
global server
|
|
739
|
-
global must_exit
|
|
740
|
-
logging.error('Exiting...')
|
|
741
|
-
must_exit = True
|
|
742
|
-
del server
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
# Wrapper around the asyncore loop that lets us poll the in/out pipes every 1ms
|
|
746
|
-
def run_loop():
|
|
747
|
-
global must_exit
|
|
748
|
-
global in_pipe
|
|
749
|
-
global out_pipe
|
|
750
|
-
global needs_flush
|
|
751
|
-
global flush_pipes
|
|
752
|
-
global last_activity
|
|
753
|
-
global last_client_disconnected
|
|
754
|
-
global dns_cache
|
|
755
|
-
winmm = None
|
|
756
|
-
|
|
757
|
-
# increase the windows timer resolution to 1ms
|
|
758
|
-
if platform.system() == "Windows":
|
|
759
|
-
try:
|
|
760
|
-
import ctypes
|
|
761
|
-
winmm = ctypes.WinDLL('winmm')
|
|
762
|
-
winmm.timeBeginPeriod(1)
|
|
763
|
-
except:
|
|
764
|
-
pass
|
|
765
|
-
|
|
766
|
-
last_activity = current_time()
|
|
767
|
-
last_check = current_time()
|
|
768
|
-
# disable gc to avoid pauses during traffic shaping/proxying
|
|
769
|
-
gc.disable()
|
|
770
|
-
out_interval = None
|
|
771
|
-
in_interval = None
|
|
772
|
-
while not must_exit:
|
|
773
|
-
# Tick every 1ms if traffic-shaping is enabled and we have data or are doing background dns lookups, every 1 second otherwise
|
|
774
|
-
lock.acquire()
|
|
775
|
-
tick_interval = 0.001
|
|
776
|
-
if out_interval is not None:
|
|
777
|
-
tick_interval = max(tick_interval, out_interval)
|
|
778
|
-
if in_interval is not None:
|
|
779
|
-
tick_interval = max(tick_interval, in_interval)
|
|
780
|
-
if background_activity_count == 0:
|
|
781
|
-
if in_pipe.next_message is None and in_pipe.queue.empty() and out_pipe.next_message is None and out_pipe.queue.empty():
|
|
782
|
-
tick_interval = 1.0
|
|
783
|
-
elif in_pipe.kbps == .0 and in_pipe.latency == 0 and out_pipe.kbps == .0 and out_pipe.latency == 0:
|
|
784
|
-
tick_interval = 1.0
|
|
785
|
-
lock.release()
|
|
786
|
-
logging.debug("Tick Time: %0.3f", tick_interval)
|
|
787
|
-
asyncore.poll(tick_interval, asyncore.socket_map)
|
|
788
|
-
if needs_flush:
|
|
789
|
-
flush_pipes = True
|
|
790
|
-
dns_cache = {}
|
|
791
|
-
needs_flush = False
|
|
792
|
-
out_interval = out_pipe.tick()
|
|
793
|
-
in_interval = in_pipe.tick()
|
|
794
|
-
if flush_pipes:
|
|
795
|
-
PrintMessage('OK')
|
|
796
|
-
flush_pipes = False
|
|
797
|
-
now = current_time()
|
|
798
|
-
# Clear the DNS cache 500ms after the last client disconnects
|
|
799
|
-
if options.flushdnscache and last_client_disconnected is not None and dns_cache:
|
|
800
|
-
if now - last_client_disconnected >= 0.5:
|
|
801
|
-
dns_cache = {}
|
|
802
|
-
last_client_disconnected = None
|
|
803
|
-
logging.debug("Flushed DNS cache")
|
|
804
|
-
# Every 500 ms check to see if it is a good time to do a gc
|
|
805
|
-
if now - last_check >= 0.5:
|
|
806
|
-
last_check = now
|
|
807
|
-
# manually gc after 5 seconds of idle
|
|
808
|
-
if now - last_activity >= 5:
|
|
809
|
-
last_activity = now
|
|
810
|
-
logging.debug("Triggering manual GC")
|
|
811
|
-
gc.collect()
|
|
812
|
-
|
|
813
|
-
if winmm is not None:
|
|
814
|
-
winmm.timeEndPeriod(1)
|
|
815
|
-
|
|
816
|
-
def GetDestPort(port):
|
|
817
|
-
global port_mappings
|
|
818
|
-
if port_mappings is not None:
|
|
819
|
-
src_port = str(port)
|
|
820
|
-
if src_port in port_mappings:
|
|
821
|
-
return port_mappings[src_port]
|
|
822
|
-
elif 'default' in port_mappings:
|
|
823
|
-
return port_mappings['default']
|
|
824
|
-
return port
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
def SetPortMappings(map_string):
|
|
828
|
-
global port_mappings
|
|
829
|
-
port_mappings = {}
|
|
830
|
-
map_string = map_string.strip('\'" \t\r\n')
|
|
831
|
-
for pair in map_string.split(','):
|
|
832
|
-
(src, dest) = pair.split(':')
|
|
833
|
-
if src == '*':
|
|
834
|
-
port_mappings['default'] = int(dest)
|
|
835
|
-
logging.debug("Default port mapped to port {0}".format(dest))
|
|
836
|
-
else:
|
|
837
|
-
logging.debug("Port {0} mapped to port {1}".format(src, dest))
|
|
838
|
-
port_mappings[src] = int(dest)
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
if '__main__' == __name__:
|
|
842
|
-
main()
|