X Tutup
Skip to content

Commit 02fd0d9

Browse files
authored
Add files via upload
1 parent ee6a710 commit 02fd0d9

File tree

5 files changed

+253
-141
lines changed

5 files changed

+253
-141
lines changed

pproxy/__init__.py

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import argparse, time, re, pickle, asyncio, functools, types, os, urllib.parse
1+
import argparse, time, re, asyncio, functools, types, urllib.parse
22
from pproxy import proto
33

44
__title__ = 'pproxy'
5-
__version__ = "1.0.0"
5+
__version__ = "1.2.0"
66
__description__ = "Proxy server that can tunnel among remote servers by regex rules."
77
__author__ = "Qian Wenjie"
88
__license__ = "MIT License"
@@ -15,12 +15,35 @@
1515
asyncio.StreamReader.read_n = lambda self, n: asyncio.wait_for(self.readexactly(n), timeout=SOCKET_TIMEOUT)
1616
asyncio.StreamReader.read_until = lambda self, s: asyncio.wait_for(self.readuntil(s), timeout=SOCKET_TIMEOUT)
1717

18-
async def proxy_handler(reader, writer, protos, auth, rserver, block, auth_tables, cipher, httpget, unix_path, verbose=DUMMY, modstat=lambda r,h:lambda i:DUMMY, **kwargs):
18+
if not hasattr(asyncio.StreamReader, 'readuntil'): # Python 3.4 and below
19+
@asyncio.coroutine
20+
def readuntil(self, separator):
21+
seplen = len(separator)
22+
offset = 0
23+
while True:
24+
buflen = len(self._buffer)
25+
if buflen - offset >= seplen:
26+
isep = self._buffer.find(separator, offset)
27+
if isep != -1:
28+
break
29+
offset = buflen + 1 - seplen
30+
if self._eof:
31+
chunk = bytes(self._buffer)
32+
self._buffer.clear()
33+
raise asyncio.IncompleteReadError(chunk, None)
34+
yield from self._wait_for_data('readuntil')
35+
chunk = self._buffer[:isep + seplen]
36+
del self._buffer[:isep + seplen]
37+
self._maybe_resume_transport()
38+
return bytes(chunk)
39+
asyncio.StreamReader.readuntil = readuntil
40+
41+
def proxy_handler(reader, writer, protos, auth, rserver, block, auth_tables, cipher, httpget, unix_path, verbose=DUMMY, modstat=lambda r,h:lambda i:DUMMY, **kwargs):
1942
try:
2043
remote_ip = writer.get_extra_info('peername')[0] if not unix_path else None
2144
reader_cipher = 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, httpget=httpget, reader_cipher=reader_cipher)
45+
header = yield from reader.read_n(1)
46+
lproto, host_name, port, initbuf = yield from proto.parse(protos, reader=reader, writer=writer, header=header, auth=auth, auth_tables=auth_tables, remote_ip=remote_ip, httpget=httpget, reader_cipher=reader_cipher)
2447
if host_name is None:
2548
writer.close()
2649
return
@@ -33,36 +56,36 @@ async def proxy_handler(reader, writer, protos, auth, rserver, block, auth_table
3356
break
3457
viaproxy = bool(roption)
3558
if viaproxy:
36-
verbose(f'{lproto.__name__} {host_name}:{port} -> {roption.protos[0].__name__} {roption.bind}')
59+
verbose('{l.__name__} {}:{} -> {r.protos[0].__name__} {r.bind}'.format(host_name, port, l=lproto, r=roption))
3760
connect = roption.connect
3861
else:
39-
verbose(f'{lproto.__name__} {host_name}:{port}')
62+
verbose('{l.__name__} {}:{}'.format(host_name, port, l=lproto))
4063
connect = functools.partial(asyncio.open_connection, host=host_name, port=port)
4164
try:
42-
reader_remote, writer_remote = await asyncio.wait_for(connect(), timeout=SOCKET_TIMEOUT)
65+
reader_remote, writer_remote = yield from asyncio.wait_for(connect(), timeout=SOCKET_TIMEOUT)
4366
except asyncio.TimeoutError:
44-
raise Exception(f'Connection timeout {rserver}')
67+
raise Exception('Connection timeout {}'.format(rserver))
4568
try:
4669
if viaproxy:
4770
writer_cipher_r = 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)
71+
yield from 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)
4972
else:
5073
writer_remote.write(initbuf)
5174
except Exception:
5275
writer_remote.close()
5376
raise Exception('Unknown remote protocol')
5477
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))
78+
asyncio.async(proto.base.channel(reader_remote, writer, m(2+viaproxy), m(4+viaproxy)))
79+
asyncio.async(lproto.channel(reader, writer_remote, m(viaproxy), DUMMY))
5780
except Exception as ex:
5881
if not isinstance(ex, asyncio.TimeoutError):
59-
verbose(f'{str(ex) or "Unsupported protocol"} from {remote_ip}')
82+
verbose('{} from {}'.format(str(ex) or "Unsupported protocol", remote_ip))
6083
try: writer.close()
6184
except Exception: pass
6285

6386
def pattern_compile(filename):
6487
with open(filename) as f:
65-
return re.compile('|'.join(i.strip() for i in f if i.strip() and not i.startswith('#'))).fullmatch
88+
return re.compile('(:?'+''.join('|'.join(i.strip() for i in f if i.strip() and not i.startswith('#')))+')$').match
6689

6790
def uri_compile(uri):
6891
url = urllib.parse.urlparse(uri)
@@ -102,20 +125,16 @@ def main():
102125
parser.add_argument('--ssl', dest='sslfile', help='certfile[,keyfile] if server listen in ssl mode')
103126
parser.add_argument('--pac', dest='pac', help='http PAC path')
104127
parser.add_argument('--get', dest='gets', default=[], action='append', help='http custom path/file')
105-
parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
128+
parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__))
106129
args = parser.parse_args()
107130
if not args.listen:
108131
args.listen.append(uri_compile('http+socks://:8080/'))
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 = {}
132+
args.auth_tables = {}
114133
args.httpget = {}
115134
if args.pac:
116-
pactext = 'function FindProxyForURL(u,h){' + (f'var b=/^(:?{args.block.__self__.pattern})$/i;if(b.test(h))return "";' if args.block else '')
135+
pactext = 'function FindProxyForURL(u,h){' + ('var b=/^(:?{})$/i;if(b.test(h))return "";'.format(args.block.__self__.pattern) if args.block else '')
117136
for i, option in enumerate(args.rserver):
118-
pactext += (f'var m{i}=/^(:?{option.match.__self__.pattern})$/i;if(m{i}.test(h))' if option.match else '') + f'return "PROXY %(host)s";'
137+
pactext += ('var m{1}=/^(:?{0})$/i;if(m{1}.test(h))'.format(option.match.__self__.pattern, i) if option.match else '') + 'return "PROXY %(host)s";'
119138
args.httpget[args.pac] = pactext+'return "DIRECT";}'
120139
args.httpget[args.pac+'/all'] = 'function FindProxyForURL(u,h){return "PROXY %(host)s";}'
121140
args.httpget[args.pac+'/none'] = 'function FindProxyForURL(u,h){return "DIRECT";}'
@@ -130,29 +149,26 @@ def main():
130149
option.sslclient.load_cert_chain(*sslfile)
131150
option.sslserver.load_cert_chain(*sslfile)
132151
elif any(map(lambda o: o.sslclient, args.listen)):
133-
print(f'You must specify --ssl to listen in ssl mode')
152+
print('You must specify --ssl to listen in ssl mode')
134153
return
135154
loop = asyncio.get_event_loop()
136155
if args.v:
137156
from pproxy import verbose
138157
verbose.setup(loop, args)
139158
servers = []
140159
for option in args.listen:
141-
print(f'Serving on {option.bind} by {",".join(i.__name__ for i in option.protos)}', '(SSL)' if option.sslclient else '')
142-
handler = functools.partial(proxy_handler, **vars(args), **vars(option))
160+
print('Serving on', option.bind, 'by', ",".join(i.__name__ for i in option.protos) + ('(SSL)' if option.sslclient else ''), '({})'.format(option.cipher.name) if option.cipher else '')
161+
handler = functools.partial(functools.partial(proxy_handler, **vars(args)), **vars(option))
143162
try:
144163
server = loop.run_until_complete(option.server(handler))
145164
servers.append(server)
146165
except Exception as ex:
147-
print(f'Start server failed.\n\t==> {ex}')
166+
print('Start server failed.\n\t==>', ex)
148167
if servers:
149168
try:
150169
loop.run_forever()
151170
except KeyboardInterrupt:
152171
print('exit')
153-
if args.auth_tables:
154-
with open('.auth_tables', 'wb') as f:
155-
pickle.dump(args.auth_tables, f, pickle.HIGHEST_PROTOCOL)
156172
for task in asyncio.Task.all_tasks():
157173
task.cancel()
158174
for server in servers:

pproxy/cipher.py

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os, hashlib, argparse, hmac
22

33
class BaseCipher(object):
4-
LIBRARY = True
4+
PYTHON = False
55
CACHE = {}
66
def __init__(self, key, ota=False):
77
if self.KEY_LENGTH > 0:
@@ -24,14 +24,15 @@ def encrypt(self, s):
2424
return self.cipher.encrypt(s)
2525
def patch_ota_reader(self, reader):
2626
chunk_id = 0
27-
async def patched_read():
27+
@asyncio.coroutine
28+
def patched_read():
2829
nonlocal chunk_id
2930
try:
30-
data_len = int.from_bytes(await reader.readexactly(2), 'big')
31+
data_len = int.from_bytes((yield from reader.readexactly(2)), 'big')
3132
except Exception:
3233
return None
33-
checksum = await reader.readexactly(10)
34-
data = await reader.readexactly(data_len)
34+
checksum = yield from reader.readexactly(10)
35+
data = yield from reader.readexactly(data_len)
3536
checksum_server = hmac.new(self.iv+chunk_id.to_bytes(4, 'big'), data, 'sha1').digest()
3637
assert checksum_server[:10] == checksum
3738
chunk_id += 1
@@ -47,6 +48,9 @@ def patched_write(data):
4748
chunk_id += 1
4849
return write(len(data).to_bytes(2, 'big') + checksum[:10] + data)
4950
writer.write = patched_write
51+
@classmethod
52+
def name(cls):
53+
return cls.__name__.replace('_Cipher', '').replace('_', '-').lower()
5054

5155
class RC4_Cipher(BaseCipher):
5256
KEY_LENGTH = 16
@@ -98,6 +102,19 @@ class AES_192_CFB8_Cipher(AES_256_CFB8_Cipher):
98102
class AES_128_CFB8_Cipher(AES_256_CFB8_Cipher):
99103
KEY_LENGTH = 16
100104

105+
class AES_256_OFB_Cipher(BaseCipher):
106+
KEY_LENGTH = 32
107+
IV_LENGTH = 16
108+
def setup(self):
109+
from Crypto.Cipher import AES
110+
self.cipher = AES.new(self.key, AES.MODE_OFB, iv=self.iv)
111+
112+
class AES_192_OFB_Cipher(AES_256_OFB_Cipher):
113+
KEY_LENGTH = 24
114+
115+
class AES_128_OFB_Cipher(AES_256_OFB_Cipher):
116+
KEY_LENGTH = 16
117+
101118
class BF_CFB_Cipher(BaseCipher):
102119
KEY_LENGTH = 16
103120
IV_LENGTH = 8
@@ -119,27 +136,27 @@ def setup(self):
119136
from Crypto.Cipher import DES
120137
self.cipher = DES.new(self.key, DES.MODE_CFB, iv=self.iv, segment_size=64)
121138

122-
MAP = {name[:-7].replace('_', '-').lower(): cls for name, cls in globals().items() if name.endswith('_Cipher')}
139+
MAP = {cls.name(): cls for name, cls in globals().items() if name.endswith('_Cipher')}
123140

124141
def get_cipher(cipher_key):
125-
from pproxy.cipherpy import MAP as MAP2
126-
CIPHER_MAP = dict(list(MAP.items())+list(MAP2.items()))
142+
from pproxy.cipherpy import MAP as MAP_PY
127143
cipher, _, key = cipher_key.partition(':')
128144
cipher_name, ota, _ = cipher.partition('!')
129145
if not key:
130146
raise argparse.ArgumentTypeError('empty key')
131-
if cipher_name not in CIPHER_MAP:
132-
raise argparse.ArgumentTypeError(f'existing ciphers: {list(sorted(CIPHER_MAP.keys()))}')
133-
cipher, key, ota = CIPHER_MAP[cipher_name], key.encode(), bool(ota) if ota else False
134-
if cipher.LIBRARY:
147+
if cipher_name not in MAP and cipher_name not in MAP_PY:
148+
raise argparse.ArgumentTypeError('existing ciphers: {}'.format(sorted(set(MAP)|set(MAP_PY))))
149+
key, ota = key.encode(), bool(ota) if ota else False
150+
cipher = MAP.get(cipher_name)
151+
if cipher:
135152
try:
136153
assert __import__('Crypto').version_info >= (3, 4)
137154
except Exception:
138-
if cipher_name+'-py' in CIPHER_MAP:
139-
cipher = CIPHER_MAP[cipher_name+'-py']
140-
print(f'Switch to python cipher [{cipher_name}-py]')
141-
else:
142-
raise argparse.ArgumentTypeError(f'this cipher needs library: "pip3 install pycryptodome"')
155+
cipher = None
156+
if cipher is None:
157+
cipher = MAP_PY.get(cipher_name)
158+
if cipher is None:
159+
raise argparse.ArgumentTypeError('this cipher needs library: "pip3 install pycryptodome"')
143160
def apply_cipher(reader, writer):
144161
reader_cipher, writer_cipher = cipher(key, ota=ota), cipher(key, ota=ota)
145162
reader_cipher._buffer = b''
@@ -162,7 +179,11 @@ def write(s, o=writer.write):
162179
return o(writer_cipher.encrypt(s))
163180
reader.feed_data = feed_data
164181
writer.write = write
182+
if reader._buffer:
183+
reader._buffer, buf = bytearray(), reader._buffer
184+
feed_data(buf)
165185
return reader_cipher, writer_cipher
186+
apply_cipher.name = cipher_name + ('-py' if cipher.PYTHON else '')
166187
apply_cipher.ota = ota
167188
return apply_cipher
168189

0 commit comments

Comments
 (0)
X Tutup