forked from realgam3/pymultitor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pymultitor.py
382 lines (314 loc) · 12.6 KB
/
pymultitor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
#!/usr/bin/env python
import re
import sys
import json
import atexit
import socket
import logging
import requests
import platform
import itertools
from os import path
from shutil import rmtree
from tempfile import mkdtemp
from multiprocessing.pool import ThreadPool
from stem.control import Controller, Signal
from requests.exceptions import ConnectionError
from stem.process import launch_tor_with_config
from mitmproxy.proxy import ProxyServer, ProxyConfig
from mitmproxy.options import Options as ProxyOptions
from mitmproxy.controller import handler as proxy_handler
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
# If mitmproxy > 0.18.3
websocket_key = 'websocket'
new_mitmproxy = hasattr(ProxyOptions(), websocket_key)
if new_mitmproxy:
from mitmproxy.http import HTTPResponse
from mitmproxy.master import Master
else:
websocket_key = 'websockets'
from mitmproxy.flow import State, FlowMaster as Master
from mitmproxy.models import HTTPResponse
__version__ = '2.0.0'
logger = logging.getLogger(__name__)
def is_windows():
return platform.system().lower() == 'windows'
class Tor(object):
def __init__(self, cmd='tor'):
self.logger = logging.getLogger(__name__)
self.tor_cmd = cmd
self.socks_port = self.free_port()
self.control_port = self.free_port()
self.data_directory = mkdtemp()
self.id = self.socks_port
self.process = None
self.controller = None
self.__is_shutdown = False
def __del__(self):
self.shutdown()
def __enter__(self):
return self.run()
def __exit__(self, exc_type, exc_val, exc_tb):
self.shutdown()
def run(self):
self.logger.debug("[%05d] Executing Tor Process" % self.id)
self.process = launch_tor_with_config(
config={
"ControlPort": str(self.control_port),
"SOCKSPort": str(self.socks_port),
"DataDirectory": self.data_directory
},
tor_cmd=self.tor_cmd,
init_msg_handler=self.print_bootstrapped_line
)
self.logger.debug("[%05d] Creating Tor Controller" % self.id)
self.controller = Controller.from_port(port=self.control_port)
self.controller.authenticate()
return self
def shutdown(self):
if self.__is_shutdown:
return
self.__is_shutdown = True
self.logger.debug("[%05d] Destroying Tor" % self.id)
self.controller.close()
self.process.terminate()
self.process.wait()
# If Not Closed Properly
if path.exists(self.data_directory):
rmtree(self.data_directory)
def newnym_available(self):
return self.controller.is_newnym_available()
def newnym(self):
if not self.newnym_available():
self.logger.warning("[%05d] Cant Change Tor Identity (Need More Tor Processes)" % self.id)
return False
self.logger.debug("[%05d] Changing Tor Identity" % self.id)
self.controller.signal(Signal.NEWNYM)
return True
def print_bootstrapped_line(self, line):
if "Bootstrapped" in line:
self.logger.debug("[%05d] Tor Bootstrapped Line: %s" % (self.id, line))
if "100%" in line:
self.logger.debug("[%05d] Tor Process Executed Successfully" % self.id)
@staticmethod
def free_port():
"""
Determines a free port using sockets.
Taken from selenium python.
"""
free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
free_socket.bind(('0.0.0.0', 0))
free_socket.listen(5)
port = free_socket.getsockname()[1]
free_socket.close()
return port
class MultiTor(object):
def __init__(self, size=2, cmd='tor'):
self.logger = logging.getLogger(__name__)
self.cmd = cmd
self.size = size
self.list = []
self.cycle = None
self.current = None
def run(self):
self.logger.info("Executing %d Tor Processes" % self.size)
# If OS Platform Is Windows Run Processes Async
if is_windows():
pool = ThreadPool(processes=self.size)
self.list = pool.map(lambda _: Tor(cmd=self.cmd).run(), range(self.size))
else:
self.list = [Tor(cmd=self.cmd).run() for _ in range(self.size)]
self.logger.info("All Tor Processes Executed Successfully")
self.cycle = itertools.cycle(self.list)
self.current = next(self.cycle)
@property
def proxy(self):
proxy_url = 'socks5://127.0.0.1:%d' % self.current.socks_port
return {'http': proxy_url, 'https': proxy_url}
def new_identity(self):
self.current.newnym()
self.current = next(self.cycle)
return self.proxy
def shutdown(self):
for tor in self.list:
tor.shutdown()
class MultiTorProxy(Master):
def __init__(self, listen_host="", listen_port=8080, socks=False, insecure=False,
processes=2, cmd='tor',
on_count=0, on_string=None, on_regex=None, on_rst=False, on_callback=None):
self.logger = logging.getLogger(__name__)
# Change IP Policy (Configuration)
self.counter = itertools.count(0)
self.on_count = on_count
self.on_regex = on_regex
self.on_rst = on_rst
self.on_string = on_string
if self.on_string:
# For Python 3
self.on_string = str.encode(on_string)
self.on_callback = None
if callable(on_callback):
self.on_callback = on_callback
# Create MultiTor (Tor Pool)
self.multitor = MultiTor(size=processes, cmd=cmd)
# Create Proxy Server
self.insecure = insecure
options = ProxyOptions(
listen_host=listen_host,
listen_port=listen_port,
ssl_insecure=self.insecure,
mode="socks5" if socks else "regular",
rawtcp=False
)
setattr(options, websocket_key, False)
config = ProxyConfig(options)
server = ProxyServer(config)
if new_mitmproxy:
super(self.__class__, self).__init__(options, server)
else:
state = State()
super(self.__class__, self).__init__(options, server, state)
def run(self):
try:
self.multitor.run()
super(self.__class__, self).run()
except KeyboardInterrupt:
self.multitor.shutdown()
self.shutdown()
def create_response(self, request):
response = requests.request(
method=request.method,
url=request.url,
data=request.content,
headers=request.headers,
allow_redirects=False,
verify=not self.insecure,
proxies=self.multitor.proxy
)
response_headers = dict(response.headers)
if not new_mitmproxy:
response_headers = response.headers.items()
response.headers.items()
return HTTPResponse.make(
status_code=response.status_code,
content=response.content,
headers=response_headers,
)
@proxy_handler
def request(self, flow):
error = None
ip_changed = False
try:
flow.response = self.create_response(flow.request)
except ConnectionError as error:
# If TCP Rst Configured
if self.on_rst:
self.logger.debug("Got TCP Rst, While TCP Rst Configured")
self.multitor.new_identity()
ip_changed = True
else:
# ReRaise Exception
self.logger.error("Got TCP Rst, While TCP Rst Not Configured")
raise error
except Exception as error:
self.logger.error("Got Unknown Error %s" % error)
else:
# If String Found On Response Content
if self.on_string and self.on_string in flow.response.content:
self.logger.debug("String Found On Response Content")
self.multitor.new_identity()
ip_changed = True
# If Regex Found On Response Content
if self.on_regex and re.search(self.on_regex, flow.response.content, re.IGNORECASE):
self.logger.debug("Regex Found On Response Content")
self.multitor.new_identity()
ip_changed = True
# If Counter Raised To The Configured Number
if self.on_count and next(self.counter) >= self.on_count:
self.logger.debug("Counter Raised To The Configured Number")
self.counter = itertools.count(1)
self.multitor.new_identity()
ip_changed = True
finally:
# CallBack (For Developers)
if self.on_callback:
ip_changed = ip_changed or self.on_callback(self, flow, error)
# Set Response
if ip_changed:
flow.response = self.create_response(flow.request)
def run(listen_host="", listen_port=8080, socks=False, insecure=False,
processes=2, cmd='tor',
on_count=0, on_string=None, on_regex=None, on_rst=False, on_callback=None):
# Warn If No Change IP Configuration:
if on_count == 0 and on_string is None and on_regex is None and not on_rst:
logger.warning("Change IP Configuration Not Set (Acting As Regular Tor Proxy)")
proxy = MultiTorProxy(
listen_host=listen_host, listen_port=listen_port, socks=socks, insecure=insecure,
processes=processes, cmd=cmd,
on_count=on_count, on_string=on_string, on_regex=on_regex, on_rst=on_rst, on_callback=on_callback
)
# Shutdown When Exit (To Be Sure All Cleaned)
atexit.register(proxy.multitor.shutdown)
return proxy.run()
def main(args=None):
if args is None:
args = sys.argv[1:]
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument("-v", "--version", action="version", version="%(prog)s {ver}".format(ver=__version__))
# Proxy Configuration
parser.add_argument("-lh", "--host",
help="Proxy Listen Host.",
dest='listen_host',
default="127.0.0.1")
parser.add_argument("-lp", "--port",
help="Proxy Listen Port.",
dest='listen_port',
type=int,
default=8080)
parser.add_argument("-s", "--socks",
help="Use As Socks Proxy (Not HTTP Proxy).",
action='store_true')
parser.add_argument("-i", "--insecure",
help="Insecure SSL.",
action='store_true')
parser.add_argument("-d", "--debug",
help="Debug Log.",
action='store_true')
# MultiTor Configuration
parser.add_argument("-p", "--tor-processes",
help="Number Of Tor Processes On The Cycle.",
dest='processes',
type=int,
default=2)
parser.add_argument("-c", "--tor-cmd",
help="Tor Cmd (Executable Path + Arguments).",
dest='cmd',
default="tor")
# When To Change IP Address
parser.add_argument("--on-count",
help="Change IP Every x Requests (Resources Also Counted).",
type=int,
default=0)
parser.add_argument("--on-string",
help="Change IP When String Found On The Response Content.",
default=None)
parser.add_argument("--on-regex",
help="Change IP When Regex Found On The Response Content.",
default=None)
parser.add_argument("--on-rst",
help="Change IP When Connection Closed With TCP RST.",
action='store_true')
sys_args = vars(parser.parse_args(args=args))
# Configure Logger
logging.basicConfig(level=logging.DEBUG if sys_args.pop('debug') else logging.INFO,
format='%(asctime)s %(levelname)-8s %(message)s',
datefmt='%d-%m-%y %H:%M:%S')
# Disable Other Loggers
logging.getLogger("stem").disabled = True
logging.getLogger("requests.packages.urllib3.connectionpool").disabled = True
# Log CMD Args If Debug Mode Enabled
logger.debug("Running With CMD Args: %s" % json.dumps(sys_args))
# Run PyMultitor
run(**sys_args)
if __name__ == '__main__':
main()