X Tutup
Skip to content

Commit be916b7

Browse files
authored
Add files via upload
1 parent aeca61e commit be916b7

File tree

5 files changed

+562
-0
lines changed

5 files changed

+562
-0
lines changed

pproxy/__init__.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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+

pproxy/__main__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
if __package__ == '':
2+
import os, sys
3+
path = os.path.dirname(os.path.dirname(__file__))
4+
sys.path.insert(0, path)
5+
6+
if __name__ == '__main__':
7+
import pproxy
8+
pproxy.main()

pproxy/ciphers.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import os, hashlib, struct, functools, argparse, hmac
2+
3+
#pip install pycryptodome
4+
from Crypto.Cipher import ARC4, ChaCha20, Salsa20, AES, DES, CAST, Blowfish, ARC2
5+
6+
class BaseCipher(object):
7+
CACHE = {}
8+
def __init__(self, key, iv=None, ota=False):
9+
if self.KEY_LENGTH > 0:
10+
self.key = self.CACHE.get(b'key'+key)
11+
if self.key is None:
12+
keybuf = []
13+
while len(b''.join(keybuf)) < self.KEY_LENGTH:
14+
keybuf.append(hashlib.md5((keybuf[-1] if keybuf else b'') + key).digest())
15+
self.key = self.CACHE[b'key'+key] = b''.join(keybuf)[:self.KEY_LENGTH]
16+
else:
17+
self.key = key
18+
self.iv = os.urandom(self.IV_LENGTH) if iv is None else iv
19+
self.ota = ota
20+
self.setup()
21+
def decrypt(self, s):
22+
return self.cipher.decrypt(s)
23+
def encrypt(self, s):
24+
return self.cipher.encrypt(s)
25+
def patch_ota_reader(self, reader):
26+
chunk_id = 0
27+
async def patched_read():
28+
nonlocal chunk_id
29+
try:
30+
data_len = int.from_bytes(await reader.readexactly(2), 'big')
31+
except Exception:
32+
return None
33+
checksum = await reader.readexactly(10)
34+
data = await reader.readexactly(data_len)
35+
checksum_server = hmac.new(self.iv+chunk_id.to_bytes(4, 'big'), data, 'sha1').digest()
36+
assert checksum_server[:10] == checksum
37+
chunk_id += 1
38+
return data
39+
reader.read_ = patched_read
40+
def patch_ota_writer(self, writer):
41+
chunk_id = 0
42+
write = writer.write
43+
def patched_write(data):
44+
nonlocal chunk_id
45+
if not data: return
46+
checksum = hmac.new(self.iv+chunk_id.to_bytes(4, 'big'), data, 'sha1').digest()
47+
chunk_id += 1
48+
return write(len(data).to_bytes(2, 'big') + checksum[:10] + data)
49+
writer.write = patched_write
50+
51+
class TableCipher(BaseCipher):
52+
KEY_LENGTH = 0
53+
IV_LENGTH = 0
54+
def setup(self):
55+
if self.key in self.CACHE:
56+
self.encrypt_table, self.decrypt_table = self.CACHE[self.key]
57+
else:
58+
a, _ = struct.unpack('<QQ', hashlib.md5(self.key).digest())
59+
table = list(range(256))
60+
for i in range(1, 1024):
61+
table.sort(key = lambda x: a % (x + i))
62+
self.encrypt_table = bytes(table)
63+
self.decrypt_table = bytes.maketrans(self.encrypt_table, bytes(range(256)))
64+
self.CACHE[self.key] = self.encrypt_table, self.decrypt_table
65+
def decrypt(self, s):
66+
return bytes.translate(s, self.decrypt_table)
67+
def encrypt(self, s):
68+
return bytes.translate(s, self.encrypt_table)
69+
70+
class RC4Cipher(BaseCipher):
71+
KEY_LENGTH = 16
72+
IV_LENGTH = 0
73+
def setup(self):
74+
self.cipher = ARC4.new(self.key)
75+
76+
class RC4MD5Cipher(BaseCipher):
77+
KEY_LENGTH = 16
78+
IV_LENGTH = 16
79+
def setup(self):
80+
self.cipher = ARC4.new(hashlib.md5(self.key + self.iv).digest())
81+
82+
class ChaCha20Cipher(BaseCipher):
83+
KEY_LENGTH = 32
84+
IV_LENGTH = 8
85+
def setup(self):
86+
self.cipher = ChaCha20.new(key=self.key, nonce=self.iv)
87+
88+
class Salsa20Cipher(BaseCipher):
89+
KEY_LENGTH = 32
90+
IV_LENGTH = 8
91+
def setup(self):
92+
self.cipher = Salsa20.new(key=self.key, nonce=self.iv)
93+
94+
class AES256CFBCipher(BaseCipher):
95+
KEY_LENGTH = 32
96+
IV_LENGTH = 16
97+
def setup(self):
98+
self.cipher = AES.new(self.key, AES.MODE_CFB, iv=self.iv, segment_size=128)
99+
100+
class AES128CFBCipher(AES256CFBCipher):
101+
KEY_LENGTH = 16
102+
103+
class AES192CFBCipher(AES256CFBCipher):
104+
KEY_LENGTH = 24
105+
106+
class BFCFBCipher(BaseCipher):
107+
KEY_LENGTH = 16
108+
IV_LENGTH = 8
109+
def setup(self):
110+
self.cipher = Blowfish.new(self.key, Blowfish.MODE_CFB, iv=self.iv, segment_size=64)
111+
112+
class CAST5CFBCipher(BaseCipher):
113+
KEY_LENGTH = 16
114+
IV_LENGTH = 8
115+
def setup(self):
116+
self.cipher = CAST.new(self.key, CAST.MODE_CFB, iv=self.iv, segment_size=64)
117+
118+
class DESCFBCipher(BaseCipher):
119+
KEY_LENGTH = 8
120+
IV_LENGTH = 8
121+
def setup(self):
122+
self.cipher = DES.new(self.key, DES.MODE_CFB, iv=self.iv, segment_size=64)
123+
124+
MAPPINGS = {\
125+
'table': TableCipher,
126+
'rc4': RC4Cipher,
127+
'rc4-md5': RC4MD5Cipher,
128+
'chacha20': ChaCha20Cipher,
129+
'salsa20': Salsa20Cipher,
130+
'aes-128-cfb': AES128CFBCipher,
131+
'aes-192-cfb': AES192CFBCipher,
132+
'aes-256-cfb': AES256CFBCipher,
133+
'bf-cfb': BFCFBCipher,
134+
'cast5-cfb': CAST5CFBCipher,
135+
'des-cfb': DESCFBCipher,
136+
}
137+
138+
def get_cipher(cipher_key):
139+
cipher, _, key = cipher_key.partition(':')
140+
cipher, ota, _ = cipher.partition('!')
141+
if not key:
142+
raise argparse.ArgumentTypeError('empty key')
143+
if cipher not in MAPPINGS:
144+
raise argparse.ArgumentTypeError(f'existing ciphers: {list(MAPPINGS.keys())}')
145+
cipher, key, ota = MAPPINGS[cipher], key.encode(), bool(ota) if ota else False
146+
async def apply_cipher(reader, writer):
147+
writer_cipher = cipher(key, ota=ota)
148+
writer.write(writer_cipher.iv)
149+
writer.write = lambda s, o=writer.write, p=writer_cipher.encrypt: o(p(s))
150+
reader_cipher = cipher(key, await reader.read_n(len(writer_cipher.iv)), ota=ota)
151+
reader._buffer = bytearray(reader_cipher.decrypt(bytes(reader._buffer)))
152+
reader.feed_data = lambda s, o=reader.feed_data, p=reader_cipher.decrypt: o(p(s))
153+
return reader_cipher, writer_cipher
154+
apply_cipher.ota = ota
155+
return apply_cipher
156+

0 commit comments

Comments
 (0)
X Tutup