|
| 1 | +import argparse, time, re, pickle, asyncio, functools, types, os, urllib.parse |
| 2 | +from pproxy import proto |
| 3 | + |
| 4 | +__title__ = 'pproxy' |
| 5 | +__version__ = "0.9.2" |
| 6 | +__description__ = "Proxy server that can tunnel among remote servers by regex rules." |
| 7 | +__author__ = "Qian Wenjie" |
| 8 | +__license__ = "MIT License" |
| 9 | + |
| 10 | +SOCKET_TIMEOUT = 300 |
| 11 | +PACKET_SIZE = 65536 |
| 12 | +DUMMY = lambda s: None |
| 13 | + |
| 14 | +asyncio.StreamReader.read_ = lambda self: self.read(PACKET_SIZE) |
| 15 | +asyncio.StreamReader.read_n = lambda self, n: asyncio.wait_for(self.readexactly(n), timeout=SOCKET_TIMEOUT) |
| 16 | +asyncio.StreamReader.read_until = lambda self, s: asyncio.wait_for(self.readuntil(s), timeout=SOCKET_TIMEOUT) |
| 17 | + |
| 18 | +async def proxy_handler(reader, writer, protos, auth, rserver, block, auth_tables, cipher, pac, pactext, unix_path, verbose=DUMMY, modstat=lambda r,h:lambda i:DUMMY, **kwargs): |
| 19 | + try: |
| 20 | + remote_ip = writer.get_extra_info('peername')[0] if not unix_path else None |
| 21 | + reader_cipher = (await cipher(reader, writer))[0] if cipher else None |
| 22 | + header = await reader.read_n(1) |
| 23 | + lproto, host_name, port, initbuf = await proto.parse(protos, reader=reader, writer=writer, header=header, auth=auth, auth_tables=auth_tables, remote_ip=remote_ip, pac=pac, pactext=pactext, reader_cipher=reader_cipher) |
| 24 | + if host_name is None: |
| 25 | + writer.close() |
| 26 | + return |
| 27 | + if block and block(host_name): |
| 28 | + raise Exception('BLOCK ' + host_name) |
| 29 | + roption = None |
| 30 | + for option in rserver: |
| 31 | + if not option.match or option.match(host_name): |
| 32 | + roption = option |
| 33 | + break |
| 34 | + viaproxy = bool(roption) |
| 35 | + if viaproxy: |
| 36 | + verbose(f'{lproto.__name__} {host_name}:{port} -> {roption.protos[0].__name__} {roption.bind}') |
| 37 | + connect = roption.connect |
| 38 | + else: |
| 39 | + verbose(f'{lproto.__name__} {host_name}:{port}') |
| 40 | + connect = functools.partial(asyncio.open_connection, host=host_name, port=port) |
| 41 | + try: |
| 42 | + reader_remote, writer_remote = await asyncio.wait_for(connect(), timeout=SOCKET_TIMEOUT) |
| 43 | + except asyncio.TimeoutError: |
| 44 | + raise Exception(f'Connection timeout {rserver}') |
| 45 | + try: |
| 46 | + if viaproxy: |
| 47 | + writer_cipher_r = (await roption.cipher(reader_remote, writer_remote))[1] if roption.cipher else None |
| 48 | + await roption.protos[0].connect(reader_remote=reader_remote, writer_remote=writer_remote, rauth=roption.auth, host_name=host_name, port=port, initbuf=initbuf, writer_cipher_r=writer_cipher_r) |
| 49 | + else: |
| 50 | + writer_remote.write(initbuf) |
| 51 | + except Exception: |
| 52 | + writer_remote.close() |
| 53 | + raise Exception('Unknown remote protocol') |
| 54 | + m = modstat(remote_ip, host_name) |
| 55 | + asyncio.ensure_future(proto.base.channel(reader_remote, writer, m(2+viaproxy), m(4+viaproxy))) |
| 56 | + asyncio.ensure_future(lproto.channel(reader, writer_remote, m(viaproxy), DUMMY)) |
| 57 | + except Exception as ex: |
| 58 | + if not isinstance(ex, asyncio.TimeoutError): |
| 59 | + verbose(f'{str(ex) or "Unsupported protocol"} from {remote_ip}') |
| 60 | + try: writer.close() |
| 61 | + except Exception: pass |
| 62 | + raise |
| 63 | + |
| 64 | +def pattern_compile(file_name): |
| 65 | + with open(file_name) as f: |
| 66 | + return re.compile('|'.join(i.strip() for i in f if i.strip() and not i.startswith('#'))).fullmatch |
| 67 | + |
| 68 | +def uri_compile(uri): |
| 69 | + url = urllib.parse.urlparse(uri) |
| 70 | + rawprotos = url.scheme.split('+') |
| 71 | + protos = list(set(filter(None, (proto.find(i) for i in rawprotos)))) |
| 72 | + if 'ssl' in rawprotos or 'secure' in rawprotos: |
| 73 | + import ssl |
| 74 | + sslserver = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) |
| 75 | + sslclient = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) |
| 76 | + if 'ssl' in rawprotos: |
| 77 | + sslclient.check_hostname = False |
| 78 | + sslclient.verify_mode = ssl.CERT_NONE |
| 79 | + else: |
| 80 | + sslserver = None |
| 81 | + sslclient = None |
| 82 | + cipher, _, loc = url.netloc.rpartition('@') |
| 83 | + if cipher: |
| 84 | + from pproxy import ciphers |
| 85 | + cipher = ciphers.get_cipher(cipher) |
| 86 | + match = pattern_compile(url.query) if url.query else None |
| 87 | + if loc: |
| 88 | + host, _, port = loc.partition(':') |
| 89 | + port = int(port) if port else 8080 |
| 90 | + connect = functools.partial(asyncio.open_connection, host=host, port=port, ssl=sslclient) |
| 91 | + server = functools.partial(asyncio.start_server, host=host, port=port, ssl=sslserver) |
| 92 | + else: |
| 93 | + connect = functools.partial(asyncio.open_unix_connection, path=url.path, ssl=sslclient, server_hostname='' if sslclient else None) |
| 94 | + server = functools.partial(asyncio.start_unix_server, path=url.path, ssl=sslserver) |
| 95 | + return types.SimpleNamespace(sslclient=sslclient, protos=protos, cipher=cipher, auth=url.fragment.encode(), match=match, server=server, connect=connect, bind=loc or url.path, unix_path=not loc, sslserver=sslserver) |
| 96 | + |
| 97 | +def main(): |
| 98 | + parser = argparse.ArgumentParser(description=__description__+'\nSupported protocols: http,socks,shadowsocks', epilog='Online help: <https://github.com/qwj/python-proxy>') |
| 99 | + parser.add_argument('-i', dest='listen', default=[], action='append', type=uri_compile, help='proxy server setting uri (default: http+socks://:8080/)') |
| 100 | + parser.add_argument('-r', dest='rserver', default=[], action='append', type=uri_compile, help='remote server setting uri (default: direct)') |
| 101 | + parser.add_argument('-b', dest='block', type=pattern_compile, help='block regex rules') |
| 102 | + parser.add_argument('-v', dest='v', action='store_true', help='print verbose output') |
| 103 | + parser.add_argument('--ssl', dest='sslfile', help='certfile[,keyfile] if server listen in ssl mode') |
| 104 | + parser.add_argument('--pac', dest='pac', help='http pac file path') |
| 105 | + parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}') |
| 106 | + args = parser.parse_args() |
| 107 | + if not args.listen: |
| 108 | + args.listen.append(uri_compile('http+socks://:/')) |
| 109 | + if os.path.exists('.auth_tables'): |
| 110 | + with open('.auth_tables', 'rb') as f: |
| 111 | + args.auth_tables = pickle.load(f) |
| 112 | + else: |
| 113 | + args.auth_tables = {} |
| 114 | + if args.pac: |
| 115 | + pactext = 'function FindProxyForURL(u,h){' + (f'var b=/^(:?{args.block.__self__.pattern})$/i;if(b.test(h))return "";' if args.block else '') |
| 116 | + for i, option in enumerate(args.rserver): |
| 117 | + pactext += (f'var m{i}=/^(:?{option.match.__self__.pattern})$/i;if(m{i}.test(h))' if option.match else '') + f'return "PROXY %(host)s";' |
| 118 | + args.pactext = (pactext+'return "DIRECT";}', 'function FindProxyForURL(u,h){{return "PROXY %(host)s";}}', 'function FindProxyForURL(u,h){return "DIRECT";}') |
| 119 | + else: |
| 120 | + args.pactext = None |
| 121 | + if args.sslfile: |
| 122 | + sslfile = args.sslfile.split(',') |
| 123 | + for option in args.listen: |
| 124 | + if option.sslclient: |
| 125 | + option.sslclient.load_cert_chain(*sslfile) |
| 126 | + option.sslserver.load_cert_chain(*sslfile) |
| 127 | + elif any(map(lambda o: o.sslclient, args.listen)): |
| 128 | + print(f'You must specify --ssl when open ssl server mode') |
| 129 | + return |
| 130 | + loop = asyncio.get_event_loop() |
| 131 | + if args.v: |
| 132 | + from pproxy import verbose |
| 133 | + verbose.setup(loop, args) |
| 134 | + servers = [] |
| 135 | + for option in args.listen: |
| 136 | + print(f'Serving on {option.bind} by {",".join(i.__name__ for i in option.protos)}', '(SSL)' if option.sslclient else '') |
| 137 | + handler = functools.partial(proxy_handler, **vars(args), **vars(option)) |
| 138 | + server = loop.run_until_complete(option.server(handler)) |
| 139 | + servers.append(server) |
| 140 | + try: |
| 141 | + loop.run_forever() |
| 142 | + except KeyboardInterrupt: |
| 143 | + print('exit') |
| 144 | + if args.auth_tables: |
| 145 | + with open('.auth_tables', 'wb') as f: |
| 146 | + pickle.dump(args.auth_tables, f, pickle.HIGHEST_PROTOCOL) |
| 147 | + for task in asyncio.Task.all_tasks(): |
| 148 | + task.cancel() |
| 149 | + for server in servers: |
| 150 | + server.close() |
| 151 | + for server in servers: |
| 152 | + loop.run_until_complete(server.wait_closed()) |
| 153 | + loop.close() |
| 154 | + |
0 commit comments