├── LICENSE ├── MANIFEST.in ├── README.rst ├── pproxy ├── __doc__.py ├── __init__.py ├── __main__.py ├── admin.py ├── cipher.py ├── cipherpy.py ├── plugin.py ├── proto.py ├── server.py ├── sysproxy.py └── verbose.py ├── setup.py └── tests ├── api_client.py ├── api_server.py ├── cipher_compare.py ├── cipher_speed.py └── cipher_verify.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 qwj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | graft tests 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | python-proxy 2 | ============ 3 | 4 | |made-with-python| |PyPI-version| |Hit-Count| |Downloads| |Downloads-month| |Downloads-week| 5 | 6 | .. |made-with-python| image:: https://img.shields.io/badge/Made%20with-Python-1f425f.svg 7 | :target: https://www.python.org/ 8 | .. |PyPI-version| image:: https://badge.fury.io/py/pproxy.svg 9 | :target: https://pypi.python.org/pypi/pproxy/ 10 | .. |Hit-Count| image:: http://hits.dwyl.io/qwj/python-proxy.svg 11 | :target: https://pypi.python.org/pypi/pproxy/ 12 | .. |Downloads| image:: https://pepy.tech/badge/pproxy 13 | :target: https://pepy.tech/project/pproxy 14 | .. |Downloads-month| image:: https://pepy.tech/badge/pproxy/month 15 | :target: https://pepy.tech/project/pproxy 16 | .. |Downloads-week| image:: https://pepy.tech/badge/pproxy/week 17 | :target: https://pepy.tech/project/pproxy 18 | 19 | HTTP/HTTP2/HTTP3/Socks4/Socks5/Shadowsocks/SSH/Redirect/Pf/QUIC TCP/UDP asynchronous tunnel proxy implemented in Python3 asyncio. 20 | 21 | QuickStart 22 | ---------- 23 | 24 | .. code:: rst 25 | 26 | $ pip3 install pproxy 27 | Successfully installed pproxy-1.9.5 28 | $ pproxy 29 | Serving on :8080 by http,socks4,socks5 30 | ^C 31 | $ pproxy -l ss://chacha20:abc@:8080 32 | Serving on :8080 by ss (chacha20-py) 33 | 34 | Optional: (better performance with C ciphers) 35 | 36 | .. code:: rst 37 | 38 | $ pip3 install pproxy[accelerated] 39 | Successfully installed pycryptodome-3.6.4 40 | 41 | Apply OS system-wide proxy: (MacOS, Windows) 42 | 43 | .. code:: rst 44 | 45 | $ pproxy -r ss://chacha20:abc@server_ip:8080 --sys -vv 46 | Serving on :8080 by http,socks4,socks5 47 | System proxy setting -> socks5 localhost:8080 48 | socks5 ::1:57345 -> ss server_ip:8080 -> slack.com:443 49 | socks5 ::1:57345 -> ss server_ip:8080 -> www.google.com:443 50 | ..... (all local traffic log) ...... 51 | 52 | Apply CLI proxy: (MacOS, Linux) 53 | 54 | .. code:: rst 55 | 56 | $ export http_proxy=http://localhost:8080 57 | $ export https_proxy=http://localhost:8080 58 | 59 | Run With Docker 60 | --------------- 61 | 62 | `pproxy` Docker container has both python3 (with Cryptodome for performance optimizations) and `pypy` versions available. 63 | 64 | Python3: 65 | 66 | ``docker run -it -p 8080:8080 mosajjal/pproxy:latest -l http://:8080 -vv`` 67 | 68 | Pypy3: 69 | 70 | ``docker run -it -p 8080:8080 mosajjal/pproxy:latest-pypy -l http://:8080 -vv`` 71 | 72 | Features 73 | -------- 74 | 75 | - Lightweight single-thread asynchronous IO. 76 | - Pure python, no additional library required. 77 | - Proxy client/server for TCP/UDP. 78 | - Schedule (load balance) among remote servers. 79 | - Incoming traffic auto-detect. 80 | - Tunnel/jump/backward-jump support. 81 | - Unix domain socket support. 82 | - HTTP v2, HTTP v3 (QUIC) 83 | - User/password authentication support. 84 | - Filter/block hostname by regex patterns. 85 | - SSL/TLS client/server support. 86 | - Shadowsocks OTA (One-Time-Auth_), SSR plugins. 87 | - Statistics by bandwidth and traffic. 88 | - PAC support for javascript configuration. 89 | - Iptables/Pf NAT redirect packet tunnel. 90 | - System proxy auto-setting support. 91 | - Client/Server API provided. 92 | 93 | .. _One-Time-Auth: https://shadowsocks.org/en/spec/one-time-auth.html 94 | 95 | Protocols 96 | --------- 97 | 98 | +-------------------+------------+------------+------------+------------+--------------+ 99 | | Name | TCP server | TCP client | UDP server | UDP client | scheme | 100 | +===================+============+============+============+============+==============+ 101 | | http (connect) | ✔ | ✔ | | | http:// | 102 | +-------------------+ +------------+------------+------------+--------------+ 103 | | http | | ✔ | | | httponly:// | 104 | | (get,post,etc) | | | | | (as client) | 105 | +-------------------+------------+------------+------------+------------+--------------+ 106 | | http v2 (connect) | ✔ | ✔ | | | h2:// | 107 | +-------------------+------------+------------+------------+------------+--------------+ 108 | | http v3 (connect) | ✔ by UDP | ✔ by UDP | | | h3:// | 109 | +-------------------+------------+------------+------------+------------+--------------+ 110 | | https | ✔ | ✔ | | | http+ssl:// | 111 | +-------------------+------------+------------+------------+------------+--------------+ 112 | | socks4 | ✔ | ✔ | | | socks4:// | 113 | +-------------------+------------+------------+------------+------------+--------------+ 114 | | socks5 | ✔ | ✔ | ✔ udp-only | ✔ udp-only | socks5:// | 115 | +-------------------+------------+------------+------------+------------+--------------+ 116 | | socks5 over TLS | ✔ | ✔ | | | socks5+ssl://| 117 | +-------------------+------------+------------+------------+------------+--------------+ 118 | | shadowsocks | ✔ | ✔ | ✔ | ✔ | ss:// | 119 | +-------------------+------------+------------+------------+------------+--------------+ 120 | | shadowsocks aead | ✔ | ✔ | | | ss:// | 121 | +-------------------+------------+------------+------------+------------+--------------+ 122 | | shadowsocksR | ✔ | ✔ | | | ssr:// | 123 | +-------------------+------------+------------+------------+------------+--------------+ 124 | | trojan | ✔ | ✔ | | | trojan:// | 125 | +-------------------+------------+------------+------------+------------+--------------+ 126 | | ssh tunnel | | ✔ | | | ssh:// | 127 | +-------------------+------------+------------+------------+------------+--------------+ 128 | | quic | ✔ by UDP | ✔ by UDP | ✔ | ✔ | http+quic:// | 129 | +-------------------+------------+------------+------------+------------+--------------+ 130 | | iptables nat | ✔ | | | | redir:// | 131 | +-------------------+------------+------------+------------+------------+--------------+ 132 | | pfctl nat (macos) | ✔ | | | | pf:// | 133 | +-------------------+------------+------------+------------+------------+--------------+ 134 | | echo | ✔ | | ✔ | | echo:// | 135 | +-------------------+------------+------------+------------+------------+--------------+ 136 | | tunnel | ✔ | ✔ | ✔ | ✔ | tunnel:// | 137 | | (raw socket) | | | | | tunnel{ip}://| 138 | +-------------------+------------+------------+------------+------------+--------------+ 139 | | websocket | ✔ | ✔ | | | ws:// | 140 | | (simple tunnel) | | | | | ws{dst_ip}://| 141 | +-------------------+------------+------------+------------+------------+--------------+ 142 | | xxx over TLS | ✔ | ✔ | | | xxx+ssl:// | 143 | +-------------------+------------+------------+------------+------------+--------------+ 144 | | AUTO DETECT | ✔ | | ✔ | | a+b+c+d:// | 145 | +-------------------+------------+------------+------------+------------+--------------+ 146 | 147 | Scheduling Algorithms 148 | --------------------- 149 | 150 | +-------------------+------------+------------+------------+------------+ 151 | | Name | TCP | UDP | Parameter | Default | 152 | +===================+============+============+============+============+ 153 | | first_available | ✔ | ✔ | -s fa | ✔ | 154 | +-------------------+------------+------------+------------+------------+ 155 | | round_robin | ✔ | ✔ | -s rr | | 156 | +-------------------+------------+------------+------------+------------+ 157 | | random_choice | ✔ | ✔ | -s rc | | 158 | +-------------------+------------+------------+------------+------------+ 159 | | least_connection | ✔ | | -s lc | | 160 | +-------------------+------------+------------+------------+------------+ 161 | 162 | Requirement 163 | ----------- 164 | 165 | pycryptodome_ is an optional library to enable faster (C version) cipher. **pproxy** has many built-in pure python ciphers. They are lightweight and stable, but slower than C ciphers. After speedup with PyPy_, pure python ciphers can get similar performance as C version. If the performance is important and don't have PyPy_, install pycryptodome_ instead. 166 | 167 | asyncssh_ is an optional library to enable ssh tunnel client support. 168 | 169 | These are some performance benchmarks between Python and C ciphers (dataset: 8M): 170 | 171 | +---------------------+----------------+ 172 | | chacha20-c | 0.64 secs | 173 | +---------------------+----------------+ 174 | | chacha20-py (pypy3) | 1.32 secs | 175 | +---------------------+----------------+ 176 | | chacha20-py | 48.86 secs | 177 | +---------------------+----------------+ 178 | 179 | PyPy3 Quickstart: 180 | 181 | .. code:: rst 182 | 183 | $ pypy3 -m ensurepip 184 | $ pypy3 -m pip install asyncio pproxy 185 | 186 | .. _pycryptodome: https://pycryptodome.readthedocs.io/en/latest/src/introduction.html 187 | .. _asyncssh: https://asyncssh.readthedocs.io/en/latest/ 188 | .. _PyPy: http://pypy.org 189 | 190 | Usage 191 | ----- 192 | 193 | .. code:: rst 194 | 195 | $ pproxy -h 196 | usage: pproxy [-h] [-l LISTEN] [-r RSERVER] [-ul ULISTEN] [-ur URSERVER] 197 | [-b BLOCK] [-a ALIVED] [-v] [--ssl SSLFILE] [--pac PAC] 198 | [--get GETS] [--sys] [--test TESTURL] [--version] 199 | 200 | Proxy server that can tunnel among remote servers by regex rules. Supported 201 | protocols: http,socks4,socks5,shadowsocks,shadowsocksr,redirect,pf,tunnel 202 | 203 | optional arguments: 204 | -h, --help show this help message and exit 205 | -l LISTEN tcp server uri (default: http+socks4+socks5://:8080/) 206 | -r RSERVER tcp remote server uri (default: direct) 207 | -ul ULISTEN udp server setting uri (default: none) 208 | -ur URSERVER udp remote server uri (default: direct) 209 | -b BLOCK block regex rules 210 | -a ALIVED interval to check remote alive (default: no check) 211 | -s {fa,rr,rc,lc} scheduling algorithm (default: first_available) 212 | -v print verbose output 213 | --ssl SSLFILE certfile[,keyfile] if server listen in ssl mode 214 | --pac PAC http PAC path 215 | --get GETS http custom {path,file} 216 | --sys change system proxy setting (mac, windows) 217 | --test TEST test this url for all remote proxies and exit 218 | --version show program's version number and exit 219 | 220 | Online help: 221 | 222 | URI Syntax 223 | ---------- 224 | 225 | .. code:: rst 226 | 227 | {scheme}://[{cipher}@]{netloc}/[@{localbind}][,{plugins}][?{rules}][#{auth}] 228 | 229 | - scheme 230 | 231 | - Currently supported scheme: http, socks, ss, ssl, secure. You can use + to link multiple protocols together. 232 | 233 | +----------+-----------------------------+ 234 | | http | http protocol (CONNECT) | 235 | +----------+-----------------------------+ 236 | | httponly | http protocol (GET/POST) | 237 | +----------+-----------------------------+ 238 | | socks4 | socks4 protocol | 239 | +----------+-----------------------------+ 240 | | socks5 | socks5 protocol | 241 | +----------+-----------------------------+ 242 | | ss | shadowsocks protocol | 243 | +----------+-----------------------------+ 244 | | ssr | shadowsocksr (SSR) protocol | 245 | +----------+-----------------------------+ 246 | | trojan | trojan_ protocol | 247 | +----------+-----------------------------+ 248 | | ssh | ssh client tunnel | 249 | +----------+-----------------------------+ 250 | | redir | redirect (iptables nat) | 251 | +----------+-----------------------------+ 252 | | pf | pfctl (macos pf nat) | 253 | +----------+-----------------------------+ 254 | | ssl | unsecured ssl/tls (no cert) | 255 | +----------+-----------------------------+ 256 | | secure | secured ssl/tls (cert) | 257 | +----------+-----------------------------+ 258 | | tunnel | raw connection | 259 | +----------+-----------------------------+ 260 | | ws | websocket connection | 261 | +----------+-----------------------------+ 262 | | echo | echo-back service | 263 | +----------+-----------------------------+ 264 | | direct | direct connection | 265 | +----------+-----------------------------+ 266 | 267 | .. _trojan: https://trojan-gfw.github.io/trojan/protocol 268 | 269 | - "http://" accepts GET/POST/CONNECT as server, sends CONNECT as client. "httponly://" sends "GET/POST" as client, works only on http traffic. 270 | 271 | - Valid schemes: http://, http+socks4+socks5://, http+ssl://, ss+secure://, http+socks5+ss:// 272 | 273 | - Invalid schemes: ssl://, secure:// 274 | 275 | - cipher 276 | 277 | - Cipher's format: "cipher_name:cipher_key". Cipher can be base64-encoded. So cipher string with "YWVzLTEyOC1nY206dGVzdA==" is equal to "aes-128-gcm:test". 278 | 279 | - Full cipher support list: 280 | 281 | +-----------------+------------+-----------+-------------+ 282 | | Cipher | Key Length | IV Length | Score (0-5) | 283 | +=================+============+===========+=============+ 284 | | table-py | any | 0 | 0 (lowest) | 285 | +-----------------+------------+-----------+-------------+ 286 | | rc4 | 16 | 0 | 0 (lowest) | 287 | +-----------------+------------+-----------+-------------+ 288 | | rc4-md5 | 16 | 16 | 0.5 | 289 | +-----------------+------------+-----------+-------------+ 290 | | chacha20 | 32 | 8 | 5 (highest) | 291 | +-----------------+------------+-----------+-------------+ 292 | | chacha20-ietf | 32 | 12 | 5 | 293 | +-----------------+------------+-----------+-------------+ 294 | | chacha20-ietf- | | | | 295 | | poly1305-py | 32 | 32 | AEAD | 296 | +-----------------+------------+-----------+-------------+ 297 | | salsa20 | 32 | 8 | 4.5 | 298 | +-----------------+------------+-----------+-------------+ 299 | | aes-128-cfb | 16 | 16 | 3 | 300 | | | | | | 301 | | aes-128-cfb8 | | | | 302 | | | | | | 303 | | aes-128-cfb1-py | | | slow | 304 | +-----------------+------------+-----------+-------------+ 305 | | aes-192-cfb | 24 | 16 | 3.5 | 306 | | | | | | 307 | | aes-192-cfb8 | | | | 308 | | | | | | 309 | | aes-192-cfb1-py | | | slow | 310 | +-----------------+------------+-----------+-------------+ 311 | | aes-256-cfb | 32 | 16 | 4.5 | 312 | | | | | | 313 | | aes-256-ctr | | | | 314 | | | | | | 315 | | aes-256-ofb | | | | 316 | | | | | | 317 | | aes-256-cfb8 | | | | 318 | | | | | | 319 | | aes-256-cfb1-py | | | slow | 320 | +-----------------+------------+-----------+-------------+ 321 | | aes-256-gcm | 32 | 32 | AEAD | 322 | | | | | | 323 | | aes-192-gcm | 24 | 24 | AEAD | 324 | | | | | | 325 | | aes-128-gcm | 16 | 16 | AEAD | 326 | +-----------------+------------+-----------+-------------+ 327 | | camellia-256-cfb| 32 | 16 | 4 | 328 | | | | | | 329 | | camellia-192-cfb| 24 | 16 | 4 | 330 | | | | | | 331 | | camellia-128-cfb| 16 | 16 | 4 | 332 | +-----------------+------------+-----------+-------------+ 333 | | bf-cfb | 16 | 8 | 1 | 334 | +-----------------+------------+-----------+-------------+ 335 | | cast5-cfb | 16 | 8 | 2.5 | 336 | +-----------------+------------+-----------+-------------+ 337 | | des-cfb | 8 | 8 | 1.5 | 338 | +-----------------+------------+-----------+-------------+ 339 | | rc2-cfb-py | 16 | 8 | 2 | 340 | +-----------------+------------+-----------+-------------+ 341 | | idea-cfb-py | 16 | 8 | 2.5 | 342 | +-----------------+------------+-----------+-------------+ 343 | | seed-cfb-py | 16 | 16 | 2 | 344 | +-----------------+------------+-----------+-------------+ 345 | 346 | - *pproxy* ciphers have pure python implementations. Program will switch to C cipher if there is C implementation available within pycryptodome_. Otherwise, use pure python cipher. 347 | 348 | - AEAD ciphers use additional payload after each packet. The underlying protocol is different. Specifications: AEAD_. 349 | 350 | - Some pure python ciphers (aes-256-cfb1-py) is quite slow, and is not recommended to use without PyPy speedup. Try install pycryptodome_ and use C version cipher instead. 351 | 352 | - To enable OTA encryption with shadowsocks, add '!' immediately after cipher name. 353 | 354 | - netloc 355 | 356 | - It can be "hostname:port" or "/unix_domain_socket". If the hostname is empty, server will listen on all interfaces. 357 | 358 | - Valid netloc: localhost:8080, 0.0.0.0:8123, /tmp/domain_socket, :8123 359 | 360 | - localbind 361 | 362 | - It can be "@in" or @ipv4_address or @ipv6_address 363 | 364 | - Valid localbind: @in, @192.168.1.15, @::1 365 | 366 | - plugins 367 | 368 | - It can be multiple plugins joined by ",". Supported plugins: plain, origin, http_simple, tls1.2_ticket_auth, verify_simple, verify_deflate 369 | 370 | - Valid plugins: /,tls1.2_ticket_auth,verify_simple 371 | 372 | - rules 373 | 374 | - The filename that contains regex rules 375 | 376 | - auth 377 | 378 | - The username, colon ':', and the password 379 | 380 | URIs can be joined by "__" to indicate tunneling by jump. For example, ss://1.2.3.4:1324__http://4.5.6.7:4321 make remote connection to the first shadowsocks proxy server, and then jump to the second http proxy server. 381 | 382 | .. _AEAD: http://shadowsocks.org/en/spec/AEAD-Ciphers.html 383 | 384 | Client API 385 | ---------- 386 | 387 | - TCP Client API 388 | 389 | .. code:: rst 390 | 391 | import asyncio, pproxy 392 | 393 | async def test_tcp(proxy_uri): 394 | conn = pproxy.Connection(proxy_uri) 395 | reader, writer = await conn.tcp_connect('google.com', 80) 396 | writer.write(b'GET / HTTP/1.1\r\n\r\n') 397 | data = await reader.read(1024*16) 398 | print(data.decode()) 399 | 400 | asyncio.run(test_tcp('ss://aes-256-cfb:password@remote_host:remote_port')) 401 | 402 | - UDP Client API 403 | 404 | .. code:: rst 405 | 406 | import asyncio, pproxy 407 | 408 | async def test_udp(proxy_uri): 409 | conn = pproxy.Connection(proxy_uri) 410 | answer = asyncio.Future() 411 | await conn.udp_sendto('8.8.8.8', 53, b'hello the world', answer.set_result) 412 | await answer 413 | print(answer.result()) 414 | 415 | asyncio.run(test_udp('ss://chacha20:password@remote_host:remote_port')) 416 | 417 | Server API 418 | ---------- 419 | 420 | - Server API example: 421 | 422 | .. code:: rst 423 | 424 | import asyncio 425 | import pproxy 426 | 427 | server = pproxy.Server('ss://0.0.0.0:1234') 428 | remote = pproxy.Connection('ss://1.2.3.4:5678') 429 | args = dict( rserver = [remote], 430 | verbose = print ) 431 | 432 | loop = asyncio.get_event_loop() 433 | handler = loop.run_until_complete(server.start_server(args)) 434 | try: 435 | loop.run_forever() 436 | except KeyboardInterrupt: 437 | print('exit!') 438 | 439 | handler.close() 440 | loop.run_until_complete(handler.wait_closed()) 441 | loop.run_until_complete(loop.shutdown_asyncgens()) 442 | loop.close() 443 | 444 | 445 | Examples 446 | -------- 447 | 448 | - Regex rule 449 | 450 | Define regex file "rules" as follow: 451 | 452 | .. code:: rst 453 | 454 | #google domains 455 | (?:.+\.)?google.*\.com 456 | (?:.+\.)?gstatic\.com 457 | (?:.+\.)?gmail\.com 458 | (?:.+\.)?ntp\.org 459 | (?:.+\.)?glpals\.com 460 | (?:.+\.)?akamai.*\.net 461 | (?:.+\.)?ggpht\.com 462 | (?:.+\.)?android\.com 463 | (?:.+\.)?gvt1\.com 464 | (?:.+\.)?youtube.*\.com 465 | (?:.+\.)?ytimg\.com 466 | (?:.+\.)?goo\.gl 467 | (?:.+\.)?youtu\.be 468 | (?:.+\.)?google\..+ 469 | 470 | Then start *pproxy* 471 | 472 | .. code:: rst 473 | 474 | $ pproxy -r http://aa.bb.cc.dd:8080?rules -vv 475 | Serving on :8080 by http,socks4,socks5 476 | http ::1:57768 -> http aa.bb.cc.dd:8080 -> www.googleapis.com:443 477 | http ::1:57772 -> www.yahoo.com:80 478 | socks4 ::1:57770 -> http aa.bb.cc.dd:8080 -> www.youtube.com:443 479 | 480 | *pproxy* will serve incoming traffic by http/socks4/socks5 auto-detect protocol, redirect all google traffic to http proxy aa.bb.cc.dd:8080, and visit all other traffic directly from local. 481 | 482 | - Use cipher 483 | 484 | Add cipher encryption to make sure data can't be intercepted. Run *pproxy* locally as: 485 | 486 | .. code:: rst 487 | 488 | $ pproxy -l ss://:8888 -r ss://chacha20:cipher_key@aa.bb.cc.dd:12345 -vv 489 | 490 | Next, run pproxy.py remotely on server "aa.bb.cc.dd". The base64 encoded string of "chacha20:cipher_key" is also supported: 491 | 492 | .. code:: rst 493 | 494 | $ pproxy -l ss://chacha20:cipher_key@:12345 495 | 496 | The same as: 497 | 498 | .. code:: rst 499 | 500 | $ pproxy -l ss://Y2hhY2hhMjA6Y2lwaGVyX2tleQ==@:12345 501 | 502 | The traffic between local and aa.bb.cc.dd is encrypted by stream cipher Chacha20 with secret key "cipher_key". 503 | 504 | - Unix domain socket 505 | 506 | A more complex example: 507 | 508 | .. code:: rst 509 | 510 | $ pproxy -l ss://salsa20!:complex_cipher_key@/tmp/pproxy_socket -r http+ssl://domain1.com:443#username:password 511 | 512 | *pproxy* listen on the unix domain socket "/tmp/pproxy_socket" with cipher "salsa20" and key "complex_cipher_key". OTA packet protocol is enabled by adding ! after cipher name. The traffic is tunneled to remote https proxy with simple http authentication. 513 | 514 | - SSL/TLS server 515 | 516 | If you want to listen in SSL/TLS, you must specify ssl certificate and private key files by parameter "--ssl": 517 | 518 | .. code:: rst 519 | 520 | $ pproxy -l http+ssl://0.0.0.0:443 -l http://0.0.0.0:80 --ssl server.crt,server.key --pac /autopac 521 | 522 | *pproxy* listen on both 80 HTTP and 443 HTTPS ports, use the specified SSL/TLS certificate and private key files. The "--pac" enable PAC feature, so you can put "https://yourdomain.com/autopac" path in your device's auto-configure url. 523 | 524 | Simple guide for generating self-signed ssl certificates: 525 | 526 | .. code:: rst 527 | 528 | $ openssl genrsa -des3 -out server.key 1024 529 | $ openssl req -new -key server.key -out server.csr 530 | $ cp server.key server.key.org 531 | $ openssl rsa -in server.key.org -out server.key 532 | $ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt 533 | 534 | - SSR plugins 535 | 536 | ShadowsocksR example with plugin "tls1.2_ticket_auth" to emulate common tls traffic: 537 | 538 | .. code:: rst 539 | 540 | $ pproxy -l ssr://chacha20:mypass@0.0.0.0:443/,tls1.2_ticket_auth,verify_simple 541 | 542 | - Local bind ip 543 | 544 | If you want to route the traffic by different local bind, use the @localbind URI syntax. For example, server has three ip interfaces: 192.168.1.15, 111.0.0.1, 112.0.0.1. You want to route traffic matched by "rule1" to 111.0.0.2 and traffic matched by "rule2" to 222.0.0.2, and the remaining traffic directly: 545 | 546 | .. code:: rst 547 | 548 | $ pproxy -l ss://:8000/@in -r ss://111.0.0.2:8000/@111.0.0.1?rule1 -r ss://222.0.0.2:8000/@222.0.0.1?rule2 549 | 550 | - Redirect/Pf protocol 551 | 552 | IPTable NAT redirect example (Ubuntu): 553 | 554 | .. code:: rst 555 | 556 | $ sudo iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-ports 5555 557 | $ pproxy -l redir://:5555 -r http://remote_http_server:3128 -vv 558 | 559 | The above example illustrates how to redirect all local output tcp traffic with destination port 80 to localhost port 5555 listened by **pproxy**, and then tunnel the traffic to remote http proxy. 560 | 561 | PF redirect example (MacOS): 562 | 563 | .. code:: rst 564 | 565 | $ sudo pfctl -ef /dev/stdin 566 | rdr pass on lo0 inet proto tcp from any to any port 80 -> 127.0.0.1 port 8080 567 | pass out on en0 route-to lo0 inet proto tcp from any to any port 80 keep state 568 | ^D 569 | $ sudo pproxy -l pf://:8080 -r socks5://remote_socks5_server:1324 -vv 570 | 571 | Make sure **pproxy** runs in root mode (sudo), otherwise it cannot redirect pf packet. 572 | 573 | - Multiple jumps example 574 | 575 | .. code:: rst 576 | 577 | $ pproxy -r http://server1__ss://server2__socks://server3 578 | 579 | *pproxy* will connect to server1 first, tell server1 connect to server2, and tell server2 connect to server3, and make real traffic by server3. 580 | 581 | - Raw connection tunnel 582 | 583 | TCP raw connection tunnel example: 584 | 585 | .. code:: rst 586 | 587 | $ pproxy -l tunnel{google.com}://:80 588 | $ curl -H "Host: google.com" http://localhost 589 | 590 | UDP dns tunnel example: 591 | 592 | .. code:: rst 593 | 594 | $ pproxy -ul tunnel{8.8.8.8}://:53 595 | $ nslookup google.com localhost 596 | 597 | - UDP more complicated example 598 | 599 | Run the shadowsocks udp proxy on remote machine: 600 | 601 | .. code:: rst 602 | 603 | $ pproxy -ul ss://remote_server:13245 604 | 605 | Run the commands on local machine: 606 | 607 | .. code:: rst 608 | 609 | $ pproxy -ul tunnel{8.8.8.8}://:53 -ur ss://remote_server:13245 -vv 610 | UDP tunnel 127.0.0.1:60573 -> ss remote_server:13245 -> 8.8.8.8:53 611 | UDP tunnel 127.0.0.1:60574 -> ss remote_server:13245 -> 8.8.8.8:53 612 | ... 613 | $ nslookup google.com localhost 614 | 615 | - Load balance example 616 | 617 | Specify multiple -r server, and a scheduling algorithm (rr = round_robin, rc = random_choice, lc = least_connection): 618 | 619 | .. code:: rst 620 | 621 | $ pproxy -r http://server1 -r ss://server2 -r socks5://server3 -s rr -vv 622 | http ::1:42356 -> http server1 -> google.com:443 623 | http ::1:42357 -> ss server2 -> google.com:443 624 | http ::1:42358 -> socks5 server3 -> google.com:443 625 | http ::1:42359 -> http server1 -> google.com:443 626 | ... 627 | $ pproxy -ul tunnel://:53 -ur tunnel://8.8.8.8:53 -ur tunnel://8.8.4.4:53 -s rc -vv 628 | UDP tunnel ::1:35378 -> tunnel 8.8.8.8:53 629 | UDP tunnel ::1:35378 -> tunnel 8.8.4.4:53 630 | ... 631 | 632 | - WebSocket example 633 | 634 | WebSocket protocol is similar to Tunnel protocol. It is raw and doesn't support any proxy function. It can connect to other proxy like Tunnel protocol. 635 | 636 | First run pproxy on remote machine: 637 | 638 | .. code:: rst 639 | 640 | $ pproxy -l ws://:80 -r tunnel:///tmp/myproxy -v 641 | $ pproxy -l ss://chacha20:abc@/tmp/myproxy -v 642 | 643 | Run pproxy on local machine: 644 | 645 | .. code:: rst 646 | 647 | $ pproxy -l tunnel://:1234 -r ws://remote_ip:80 -vv 648 | 649 | Then port :1234 on local machine is connected to the /tmp/myproxy on remote machine by WebSocket tunnel. You can specify any proxy protocol details on /tmp/myproxy. 650 | 651 | It is a good practice to use some CDN in the middle of local/remote machines. CDN with WebSocket support can hide remote machine's real IP from public. 652 | 653 | - Backward proxy 654 | 655 | Sometimes, the proxy server hides behind an NAT router and doesn't have a public ip. The client side has a public ip "client_ip". Backward proxy feature enables the server to connect backward to client and wait for proxy requests. 656 | 657 | Run **pproxy** client as follows: 658 | 659 | .. code:: rst 660 | 661 | $ pproxy -l http://:8080 -r http+in://:8081 -v 662 | 663 | Run **pproxy** server as follows: 664 | 665 | .. code:: rst 666 | 667 | $ pproxy -l http+in://client_ip:8081 668 | 669 | Server connects to client_ip:8081 and waits for client proxy requests. The protocol http specified is just an example. It can be any protocol and cipher **pproxy** supports. The scheme "**in**" should exist in URI to inform **pproxy** that it is a backward proxy. 670 | 671 | .. code:: rst 672 | 673 | $ pproxy -l http+in://jumpserver__http://client_ip:8081 674 | 675 | It is a complicated example. Server connects to client_ip:8081 by jump http://jumpserver. The backward proxy works through jumps. 676 | 677 | - SSH client tunnel 678 | 679 | SSH client tunnel support is enabled by installing additional library asyncssh_. After "pip3 install asyncssh", you can specify "**ssh**" as scheme to proxy via ssh client tunnel. 680 | 681 | .. code:: rst 682 | 683 | $ pproxy -l http://:8080 -r ssh://remote_server.com/#login:password 684 | 685 | If a client private key is used to authenticate, put double colon "::" between login and private key path. 686 | 687 | .. code:: rst 688 | 689 | $ pproxy -l http://:8080 -r ssh://remote_server.com/#login::private_key_path 690 | 691 | SSH connection known_hosts feature is disabled by default. 692 | 693 | - SSH jump 694 | 695 | SSH jump is supported by using "__" concatenation 696 | 697 | .. code:: rst 698 | 699 | $ pproxy -r ssh://server1__ssh://server2__ssh://server3 700 | 701 | First connection to server1 is made. Second, ssh connection to server2 is made from server1. Finally, connect to server3, and use server3 for proxying traffic. 702 | 703 | - SSH remote forward 704 | 705 | .. code:: rst 706 | 707 | $ pproxy -l ssh://server__tunnel://0.0.0.0:1234 -r tunnel://127.0.0.1:1234 708 | 709 | TCP :1234 on remote server is forwarded to 127.0.0.1:1234 on local server 710 | 711 | .. code:: rst 712 | 713 | $ pproxy -l ssh://server1__ssh://server2__ss://0.0.0.0:1234 -r ss://server3:1234 714 | 715 | It is a complicated example. SSH server2 is jumped from SSH server1, and ss://0.0.0.0:1234 on server2 is listened. Traffic is forwarded to ss://server3:1234. 716 | 717 | - Trojan protocol example 718 | 719 | Normally trojan:// should be used together with ssl://. You should specify the SSL crt/key file for ssl usage. A typical trojan server would be: 720 | 721 | .. code:: rst 722 | 723 | $ pproxy --ssl ssl.crt,ssl.key -l trojan+tunnel{localhost:80}+ssl://:443#yourpassword -vv 724 | 725 | If trojan password doesn't match, the tunnal{localhost:80} will be switched to. It looks exactly the same as a common HTTPS website. 726 | 727 | - QUIC protocol example 728 | 729 | QUIC is a UDP stream protocol used in HTTP/3. Library **aioquic** is required if you want to proxy via QUIC. 730 | QUIC is listened on UDP port, but can handle TCP or UDP traffic. If you want to handle TCP traffic, you should use "-l quic+http" instead of "-ul quic+http". 731 | 732 | .. code:: rst 733 | 734 | $ pip3 install aioquic 735 | $ pproxy --ssl ssl.crt,ssl.key -l quic+http://:1234 736 | 737 | On the client: 738 | 739 | $ pproxy -r quic+http://server:1234 740 | 741 | QUIC protocol can transfer a lot of TCP streams on one single UDP stream. If the connection number is hugh, QUIC can benefit by reducing TCP handshake time. 742 | 743 | - VPN Server Example 744 | 745 | You can run VPN server simply by installing pvpn (python vpn), a lightweight VPN server with pproxy tunnel feature. 746 | 747 | .. code:: rst 748 | 749 | $ pip3 install pvpn 750 | Successfully installed pvpn-0.2.1 751 | $ pvpn -wg 9999 -r http://remote_server:remote_port 752 | Serving on UDP :500 :4500... 753 | Serving on UDP :9000 (WIREGUARD)... 754 | TCP xx.xx.xx.xx:xx -> HTTP xx.xx.xx.xx:xx -> xx.xx.xx.xx:xx 755 | 756 | 757 | Projects 758 | -------- 759 | 760 | + `python-vpn `_ - VPN Server (IPSec,IKE,IKEv2,L2TP,WireGuard) in pure python 761 | + `shadowproxy `_ - Awesome python proxy implementation by guyingbo 762 | 763 | -------------------------------------------------------------------------------- /pproxy/__doc__.py: -------------------------------------------------------------------------------- 1 | __title__ = "pproxy" 2 | __license__ = "MIT" 3 | __description__ = "Proxy server that can tunnel among remote servers by regex rules." 4 | __keywords__ = "proxy socks http shadowsocks shadowsocksr ssr redirect pf tunnel cipher ssl udp" 5 | __author__ = "Qian Wenjie" 6 | __email__ = "qianwenjie@gmail.com" 7 | __url__ = "https://github.com/qwj/python-proxy" 8 | 9 | try: 10 | from setuptools_scm import get_version 11 | __version__ = get_version() 12 | except Exception: 13 | try: 14 | from pkg_resources import get_distribution 15 | __version__ = get_distribution('pproxy').version 16 | except Exception: 17 | __version__ = 'unknown' 18 | 19 | __all__ = ['__version__', '__description__', '__url__'] 20 | -------------------------------------------------------------------------------- /pproxy/__init__.py: -------------------------------------------------------------------------------- 1 | from . import server 2 | 3 | Connection = server.proxies_by_uri 4 | Server = server.proxies_by_uri 5 | Rule = server.compile_rule 6 | DIRECT = server.DIRECT 7 | -------------------------------------------------------------------------------- /pproxy/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == '__main__': 2 | from .server import main 3 | main() 4 | -------------------------------------------------------------------------------- /pproxy/admin.py: -------------------------------------------------------------------------------- 1 | import json 2 | import asyncio 3 | config = {} 4 | 5 | 6 | async def reply_http(reply, ver, code, content): 7 | await reply(code, f'{ver} {code}\r\nConnection: close\r\nContent-Type: text/plain\r\nCache-Control: max-age=900\r\nContent-Length: {len(content)}\r\n\r\n'.encode(), content, True) 8 | 9 | 10 | async def status_handler(reply, **kwarg): 11 | method = kwarg.get('method') 12 | if method == 'GET': 13 | data = {"status": "ok"} 14 | value = json.dumps(data).encode() 15 | ver = kwarg.get('ver') 16 | await reply_http(reply, ver, '200 OK', value) 17 | 18 | 19 | async def configs_handler(reply, **kwarg): 20 | method = kwarg.get('method') 21 | ver = kwarg.get('ver') 22 | 23 | if method == 'GET': 24 | data = {"argv": config['argv']} 25 | value = json.dumps(data).encode() 26 | await reply_http(reply, ver, '200 OK', value) 27 | elif method == 'POST': 28 | config['argv'] = kwarg.get('content').decode().split(' ') 29 | config['reload'] = True 30 | data = {"result": 'ok'} 31 | value = json.dumps(data).encode() 32 | await reply_http(reply, ver, '200 OK', value) 33 | raise KeyboardInterrupt 34 | 35 | 36 | httpget = { 37 | '/status': status_handler, 38 | '/configs': configs_handler, 39 | } 40 | -------------------------------------------------------------------------------- /pproxy/cipher.py: -------------------------------------------------------------------------------- 1 | import os, hashlib, hmac 2 | 3 | class BaseCipher(object): 4 | PYTHON = False 5 | CACHE = {} 6 | def __init__(self, key, ota=False, setup_key=True): 7 | if self.KEY_LENGTH > 0 and setup_key: 8 | self.key = self.CACHE.get(b'key'+key) 9 | if self.key is None: 10 | keybuf = [] 11 | while len(b''.join(keybuf)) < self.KEY_LENGTH: 12 | keybuf.append(hashlib.md5((keybuf[-1] if keybuf else b'') + key).digest()) 13 | self.key = self.CACHE[b'key'+key] = b''.join(keybuf)[:self.KEY_LENGTH] 14 | else: 15 | self.key = key 16 | self.ota = ota 17 | self.iv = None 18 | def setup_iv(self, iv=None): 19 | self.iv = os.urandom(self.IV_LENGTH) if iv is None else iv 20 | self.setup() 21 | return self 22 | def decrypt(self, s): 23 | return self.cipher.decrypt(s) 24 | def encrypt(self, s): 25 | return self.cipher.encrypt(s) 26 | @classmethod 27 | def name(cls): 28 | return cls.__name__.replace('_Cipher', '').replace('_', '-').lower() 29 | 30 | class AEADCipher(BaseCipher): 31 | PACKET_LIMIT = 16*1024-1 32 | def setup_iv(self, iv=None): 33 | self.iv = os.urandom(self.IV_LENGTH) if iv is None else iv 34 | randkey = hmac.new(self.iv, self.key, hashlib.sha1).digest() 35 | blocks_needed = (self.KEY_LENGTH + len(randkey) - 1) // len(randkey) 36 | okm = bytearray() 37 | output_block = b'' 38 | for counter in range(blocks_needed): 39 | output_block = hmac.new(randkey, output_block + b'ss-subkey' + bytes([counter+1]), hashlib.sha1).digest() 40 | okm.extend(output_block) 41 | self.key = bytes(okm[:self.KEY_LENGTH]) 42 | self._nonce = 0 43 | self._buffer = bytearray() 44 | self._declen = None 45 | self.setup() 46 | return self 47 | @property 48 | def nonce(self): 49 | ret = self._nonce.to_bytes(self.NONCE_LENGTH, 'little') 50 | self._nonce = (self._nonce+1) & ((1<= (3, 4) 226 | except Exception: 227 | cipher = None 228 | if cipher is None: 229 | cipher = MAP_PY.get(cipher_name) 230 | if cipher is None and cipher_name.endswith('-py'): 231 | cipher_name = cipher_name[:-3] 232 | cipher = MAP_PY.get(cipher_name) 233 | if cipher is None: 234 | return 'this cipher needs library: "pip3 install pycryptodome"', None 235 | cipher_name += ('-py' if cipher.PYTHON else '') 236 | def apply_cipher(reader, writer, pdecrypt, pdecrypt2, pencrypt, pencrypt2): 237 | reader_cipher, writer_cipher = cipher(key, ota=ota), cipher(key, ota=ota) 238 | reader_cipher._buffer = b'' 239 | def decrypt(s): 240 | s = pdecrypt2(s) 241 | if not reader_cipher.iv: 242 | s = reader_cipher._buffer + s 243 | if len(s) >= reader_cipher.IV_LENGTH: 244 | reader_cipher.setup_iv(s[:reader_cipher.IV_LENGTH]) 245 | return pdecrypt(reader_cipher.decrypt(s[reader_cipher.IV_LENGTH:])) 246 | else: 247 | reader_cipher._buffer = s 248 | return b'' 249 | else: 250 | return pdecrypt(reader_cipher.decrypt(s)) 251 | if hasattr(reader, 'decrypts'): 252 | reader.decrypts.append(decrypt) 253 | else: 254 | reader.decrypts = [decrypt] 255 | def feed_data(s, o=reader.feed_data, p=reader.decrypts): 256 | for decrypt in p: 257 | s = decrypt(s) 258 | if not s: 259 | return 260 | o(s) 261 | reader.feed_data = feed_data 262 | if reader._buffer: 263 | reader._buffer, buf = bytearray(), reader._buffer 264 | feed_data(buf) 265 | def write(s, o=writer.write): 266 | if not writer_cipher.iv: 267 | writer_cipher.setup_iv() 268 | o(pencrypt2(writer_cipher.iv)) 269 | if not s: 270 | return 271 | return o(pencrypt2(writer_cipher.encrypt(pencrypt(s)))) 272 | writer.write = write 273 | return reader_cipher, writer_cipher 274 | apply_cipher.cipher = cipher 275 | apply_cipher.key = key 276 | apply_cipher.name = cipher_name 277 | apply_cipher.ota = ota 278 | apply_cipher.plugins = [] 279 | apply_cipher.datagram = PacketCipher(cipher, key, cipher_name) 280 | return None, apply_cipher 281 | 282 | -------------------------------------------------------------------------------- /pproxy/cipherpy.py: -------------------------------------------------------------------------------- 1 | import hashlib, struct, base64 2 | 3 | from .cipher import BaseCipher, AEADCipher 4 | 5 | # Pure Python Ciphers 6 | 7 | class Table_Cipher(BaseCipher): 8 | PYTHON = True 9 | KEY_LENGTH = 0 10 | IV_LENGTH = 0 11 | def setup(self): 12 | if self.key in self.CACHE: 13 | self.encrypt_table, self.decrypt_table = self.CACHE[self.key] 14 | else: 15 | a, _ = struct.unpack('>32-b 62 | ORDERS_CHACHA20 = ((0,4,8,12),(1,5,9,13),(2,6,10,14),(3,7,11,15),(0,5,10,15),(1,6,11,12),(2,7,8,13),(3,4,9,14)) * 10 63 | ORDERS_SALSA20 = ((4,0,12,8),(9,5,1,13),(14,10,6,2),(3,15,11,7),(1,0,3,2),(6,5,4,7),(11,10,9,8),(12,15,14,13)) * 10 64 | def ChaCha20_round(H): 65 | for a, b, c, d in ORDERS_CHACHA20: 66 | H[a] += H[b] 67 | H[d] = ROL(H[d]^H[a], 16) 68 | H[c] += H[d] 69 | H[b] = ROL(H[b]^H[c], 12) 70 | H[a] += H[b] 71 | H[d] = ROL(H[d]^H[a], 8) 72 | H[c] += H[d] 73 | H[b] = ROL(H[b]^H[c], 7) 74 | return H 75 | 76 | class ChaCha20_Cipher(StreamCipher): 77 | KEY_LENGTH = 32 78 | IV_LENGTH = 8 79 | def __init__(self, key, ota=False, setup_key=True, *, counter=0): 80 | super().__init__(key, ota, setup_key) 81 | self.counter = counter 82 | def core(self): 83 | data = list(struct.unpack('<16I', b'expand 32-byte k' + self.key + self.counter.to_bytes(4, 'little') + self.iv.rjust(12, b'\x00'))) 84 | while 1: 85 | yield from struct.pack('<16I', *(a+b&0xffffffff for a, b in zip(ChaCha20_round(data[:]), data))) 86 | data[12:14] = (0, data[13]+1) if data[12]==0xffffffff else (data[12]+1, data[13]) 87 | 88 | class ChaCha20_IETF_Cipher(ChaCha20_Cipher): 89 | IV_LENGTH = 12 90 | 91 | class XChaCha20_Cipher(ChaCha20_Cipher): 92 | IV_LENGTH = 16+8 93 | def core(self): 94 | H = ChaCha20_round(list(struct.unpack('<16I', b'expand 32-byte k' + self.key + self.iv[:16]))) 95 | key = struct.pack('<8I', *(i&0xffffffff for i in (H[:4]+H[12:]))) 96 | data = list(struct.unpack('<16I', b'expand 32-byte k' + key + self.counter.to_bytes(4, 'little') + self.iv[16:].rjust(12, b'\x00'))) 97 | while 1: 98 | yield from struct.pack('<16I', *(a+b&0xffffffff for a, b in zip(ChaCha20_round(data[:]), data))) 99 | data[12:14] = (0, data[13]+1) if data[12]==0xffffffff else (data[12]+1, data[13]) 100 | 101 | class XChaCha20_IETF_Cipher(XChaCha20_Cipher): 102 | IV_LENGTH = 16+12 103 | 104 | def poly1305(cipher_encrypt, nonce, ciphertext): 105 | otk = cipher_encrypt(nonce, bytes(32)) 106 | mac_data = ciphertext + bytes((-len(ciphertext))%16 + 8) + len(ciphertext).to_bytes(8, 'little') 107 | acc, r, s = 0, int.from_bytes(otk[:16], 'little') & 0x0ffffffc0ffffffc0ffffffc0fffffff, int.from_bytes(otk[16:], 'little') 108 | for i in range(0, len(mac_data), 16): 109 | acc = (r * (acc+int.from_bytes(mac_data[i:i+16]+b'\x01', 'little'))) % ((1<<130)-5) 110 | return ((acc + s) & ((1<<128)-1)).to_bytes(16, 'little') 111 | 112 | class ChaCha20_IETF_POLY1305_Cipher(AEADCipher): 113 | PYTHON = True 114 | KEY_LENGTH = 32 115 | IV_LENGTH = 32 116 | NONCE_LENGTH = 12 117 | TAG_LENGTH = 16 118 | def process(self, s, tag=None): 119 | nonce = self.nonce 120 | if tag is not None: 121 | assert tag == poly1305(self.cipher_encrypt, nonce, s) 122 | data = self.cipher_encrypt(nonce, s, counter=1) 123 | if tag is None: 124 | return data, poly1305(self.cipher_encrypt, nonce, data) 125 | else: 126 | return data 127 | encrypt_and_digest = decrypt_and_verify = process 128 | def setup(self): 129 | self.cipher_encrypt = lambda nonce, s, counter=0: ChaCha20_IETF_Cipher(self.key, setup_key=False, counter=counter).setup_iv(nonce).encrypt(s) 130 | 131 | class XChaCha20_IETF_POLY1305_Cipher(ChaCha20_IETF_POLY1305_Cipher): 132 | NONCE_LENGTH = 16+12 133 | def setup(self): 134 | self.cipher_encrypt = lambda nonce, s, counter=0: XChaCha20_IETF_Cipher(self.key, setup_key=False, counter=counter).setup_iv(nonce).encrypt(s) 135 | 136 | class Salsa20_Cipher(StreamCipher): 137 | KEY_LENGTH = 32 138 | IV_LENGTH = 8 139 | def core(self): 140 | data = list(struct.unpack('<16I', b'expa' + self.key[:16] + b'nd 3' + self.iv.ljust(16, b'\x00') + b'2-by' + self.key[16:] + b'te k')) 141 | while 1: 142 | H = data[:] 143 | for a, b, c, d in ORDERS_SALSA20: 144 | H[a] ^= ROL(H[b]+H[c], 7) 145 | H[d] ^= ROL(H[a]+H[b], 9) 146 | H[c] ^= ROL(H[d]+H[a], 13) 147 | H[b] ^= ROL(H[c]+H[d], 18) 148 | yield from struct.pack('<16I', *(a+b&0xffffffff for a, b in zip(H, data))) 149 | data[8:10] = (0, data[9]+1) if data[8]==0xffffffff else (data[8]+1, data[9]) 150 | 151 | class CFBCipher(StreamCipher): 152 | def setup(self): 153 | segment_bit = getattr(self, 'SEGMENT_SIZE', self.IV_LENGTH*8) 154 | self.bit_mode = segment_bit % 8 != 0 155 | self.stream = self.core_bit(segment_bit) if self.bit_mode else self.core(segment_bit//8) 156 | self.last = None 157 | self.cipher = self.CIPHER.new(self.key) 158 | def process(self, s, inv=False): 159 | r = bytearray() 160 | for i in s: 161 | if self.bit_mode: 162 | j = 0 163 | for k in range(7,-1,-1): 164 | ibit = i>>k & 1 165 | jbit = ibit^self.stream.send(self.last) 166 | j |= jbit<>(7-i%8)&1)<<(segment_bit-1-i) 192 | 193 | class CFB8Cipher(CFBCipher): 194 | SEGMENT_SIZE = 8 195 | 196 | class CFB1Cipher(CFBCipher): 197 | SEGMENT_SIZE = 1 198 | 199 | class CTRCipher(StreamCipher): 200 | def setup(self): 201 | self.stream = self.core() 202 | self.cipher = self.CIPHER.new(self.key) 203 | def core(self): 204 | next_iv = int.from_bytes(self.iv, 'big') 205 | while 1: 206 | yield from self.cipher.encrypt(next_iv) 207 | next_iv = 0 if next_iv >= (1<<(self.IV_LENGTH*8))-1 else next_iv+1 208 | 209 | class OFBCipher(CTRCipher): 210 | def core(self): 211 | data = self.iv 212 | while 1: 213 | data = self.cipher.encrypt(data) 214 | yield from data 215 | 216 | class GCMCipher(AEADCipher): 217 | PYTHON = True 218 | NONCE_LENGTH = 12 219 | TAG_LENGTH = 16 220 | def setup(self): 221 | self.cipher = self.CIPHER.new(self.key) 222 | self.hkey = [] 223 | x = int.from_bytes(self.cipher.encrypt(0), 'big') 224 | for i in range(128): 225 | self.hkey.insert(0, x) 226 | x = (x>>1)^(0xe1<<120) if x&1 else x>>1 227 | def process(self, s, tag=None): 228 | def multh(y): 229 | z = 0 230 | for i in range(128): 231 | if y & (1<>2)+(j&3))*4&12,j+3&3|((j>>2)+(j+3&3))*4&12,j+2&3|((j>>2)+(j+2&3))*4&12,j+1&3|((j>>2)+(j+1&3))*4&12) for j in range(16)) 266 | def __init__(self, key): 267 | size, ekey = len(key), bytearray(key) 268 | nbr = {16:10, 24:12, 32:14}[size] 269 | while len(ekey) < 16*(nbr+1): 270 | t = ekey[-4:] 271 | if len(ekey) % size == 0: 272 | t = [self.g1[i] for i in t[1:]+t[:1]] 273 | t[0] ^= self.Rcon[len(ekey)//size%51] 274 | if size == 32 and len(ekey) % size == 16: 275 | t = [self.g1[i] for i in t] 276 | ekey.extend(m^ekey[i-size] for i, m in enumerate(t)) 277 | self.ekey = tuple(ekey[i*16:i*16+16] for i in range(nbr+1)) 278 | def encrypt(self, data): 279 | data = data.to_bytes(16, 'big') if isinstance(data, int) else data 280 | s = [data[j]^self.ekey[0][j] for j in range(16)] 281 | for key in self.ekey[1:-1]: 282 | s = [self.g2[s[a]]^self.g1[s[b]]^self.g1[s[c]]^self.g3[s[d]]^key[j] for j,a,b,c,d in self.shifts] 283 | return bytes([self.g1[s[self.shifts[j][1]]]^self.ekey[-1][j] for j in range(16)]) 284 | 285 | for method in (CFBCipher, CFB8Cipher, CFB1Cipher, CTRCipher, OFBCipher, GCMCipher): 286 | for key in (32, 24, 16): 287 | name = f'AES_{key*8}_{method.__name__[:-6]}_Cipher' 288 | globals()[name] = type(name, (method,), dict(KEY_LENGTH=key, IV_LENGTH=key if method is GCMCipher else 16, CIPHER=AES)) 289 | 290 | class Blowfish(RAW): 291 | P = None 292 | @staticmethod 293 | def hex_pi(): 294 | n, d = -3, 1 295 | for xn, xd in ((120*N**2+151*N+47, 512*N**4+1024*N**3+712*N**2+194*N+15) for N in range(1<<32)): 296 | n, d = n * xd + d * xn, d * xd 297 | o, n = divmod(16 * n, d) 298 | yield '%x' % o 299 | def __init__(self, key): 300 | if not self.P: 301 | pi = self.hex_pi() 302 | self.__class__.P = [int(''.join(next(pi) for j in range(8)), 16) for i in range(18+1024)] 303 | self.p = [a^b for a, b in zip(self.P[:18], struct.unpack('>18I', (key*(72//len(key)+1))[:72]))]+self.P[18:] 304 | buf = b'\x00'*8 305 | for i in range(0, 1042, 2): 306 | buf = self.encrypt(buf) 307 | self.p[i:i+2] = struct.unpack('>II', buf) 308 | def encrypt(self, s): 309 | s = data.to_bytes(8, 'big') if isinstance(s, int) else s 310 | sl, sr = struct.unpack('>II', s) 311 | sl ^= self.p[0] 312 | for i in self.p[1:17]: 313 | sl, sr = sr ^ i ^ (self.p[18+(sl>>24)]+self.p[274+(sl>>16&0xff)]^self.p[530+(sl>>8&0xff)])+self.p[786+(sl&0xff)] & 0xffffffff, sl 314 | return struct.pack('>II', sr^self.p[17], sl) 315 | 316 | class BF_CFB_Cipher(CFBCipher): 317 | KEY_LENGTH = 16 318 | IV_LENGTH = 8 319 | CIPHER = Blowfish 320 | 321 | class Camellia(RAW): 322 | S1 = base64.b64decode(b'cIIs7LMnwOXkhVc16gyuQSPva5NFGaUh7Q5PTh1lkr2GuK+PfOsfzj4w3F9exQsapuE5ytVHXT3ZAVrWUVZsTYsNmmb7zLAtdBIrIPCxhJnfTMvCNH52BW23qTHRFwTXFFg6Yd4bERwyD5wWUxjyIv5Ez7LDtXqRJAjoqGD8aVCq0KB9oYlil1RbHpXg/2TSEMQASKP3dduKA+baCT/dlIdcgwLNSpAzc2f2851/v+JSm9gmyDfGO4GWb0sTvmMu6XmnjJ9uvI4p9fm2L/20WXiYBmrnRnG61CWrQoiijfpyB7lV+O6sCjZJKmg8OPGkQCjTe7vJQ8EV4630d8eAng==') 323 | S2, S3, S4 = bytes(i>>7|(i&0x7f)<<1 for i in S1), bytes(i>>1|(i&1)<<7 for i in S1), S1[::2]+S1[1::2] 324 | S = (S1, S4, S3, S2, S4, S3, S2, S1) 325 | KS = base64.b64decode(b'AAIICiAiKCpIShETGTM5OxIQQkBKSCMhKykAAgwOJCYoKkRGTE4RExkbMTM1Nz0/EhAaGEZESkgjIS8t') 326 | KS = tuple((i%16//4, i%4*32, (64,51,49,36,34)[i>>4]) for i in KS) 327 | def R(self, s, t): 328 | t = sum(S[(t^s>>64)>>(i*8)&0xff]<<(i*8+32)%64 for i,S in enumerate(self.S)) 329 | t = t^t>>32<<8&0xffffff00^t>>56^((t>>32<<56|t>>8)^t<<48)&0xffff<<48^(t>>8^t<<16)&0xffff<<32 330 | return (t^t>>8&0xff<<24^t>>40^t<<16&0xff<<56^t<<56^(t>>32<<48^t>>16^t<<24)&0xffffff<<32^s)<<64&(1<<128)-1|s>>64 331 | def __init__(self, key): 332 | q, R = [int.from_bytes(key[:16], 'big'), int.from_bytes(key[16:], 'big'), 0, 0], self.R 333 | q[1] = q[1]<<64|q[1]^((1<<64)-1) if len(key)==24 else q[1] 334 | q[2] = R(R(R(R(q[0]^q[1],0xa09e667f3bcc908b),0xb67ae8584caa73b2)^q[0],0xc6ef372fe94f82be),0x54ff53a5f1d36f1c) 335 | q[3] = R(R(q[2]^q[1],0x10e527fade682d1d),0xb05688c2b3e6c1fd) 336 | nr, ks = (22, self.KS[:26]) if len(key)==16 else (29, self.KS[26:]) 337 | e = [(q[n]<>o|q[n]>>128-m+o)&(1<<64)-1 for n, m, o in ks] 338 | self.e = [e[i+i//7]<<64|e[i+i//7+1] if i%7==0 else e[i+i//7+1] for i in range(nr)] 339 | def encrypt(self, s): 340 | s = (s if isinstance(s, int) else int.from_bytes(s, 'big'))^self.e[0] 341 | for idx, k in enumerate(self.e[1:-1]): 342 | s = s^((s&k)>>95&0xfffffffe|(s&k)>>127)<<64^((s&k)<<1&~1<<96^(s&k)>>31^s<<32|k<<32)&0xffffffff<<96 ^ ((s|k)&0xffffffff)<<32^((s|k)<<1^s>>31)&k>>31&0xfffffffe^((s|k)>>31^s>>63)&k>>63&1 if (idx+1)%7==0 else self.R(s, k) 343 | return (s>>64^(s&(1<<64)-1)<<64^self.e[-1]).to_bytes(16, 'big') 344 | 345 | class Camellia_256_CFB_Cipher(CFBCipher): 346 | KEY_LENGTH = 32 347 | IV_LENGTH = 16 348 | CIPHER = Camellia 349 | 350 | class Camellia_192_CFB_Cipher(Camellia_256_CFB_Cipher): 351 | KEY_LENGTH = 24 352 | 353 | class Camellia_128_CFB_Cipher(Camellia_256_CFB_Cipher): 354 | KEY_LENGTH = 16 355 | 356 | class IDEA(RAW): 357 | def __init__(self, key): 358 | e = list(struct.unpack('>8H', key)) 359 | for i in range(8, 52): 360 | e.append((e[i-8&0xf8|i+1&0x7]&0x7f)<<9|e[i-8&0xf8|i+2&0x7]>>7) 361 | self.e = [e[i*6:i*6+6] for i in range(9)] 362 | def encrypt(self, s): 363 | s = data.to_bytes(8, 'big') if isinstance(s, int) else s 364 | M = lambda a,b: (a*b-(a*b>>16)+(a*b&0xffff>16) if a else 1-b if b else 1-a)&0xffff 365 | s0, s1, s2, s3 = struct.unpack('>4H', s) 366 | for e in self.e[:-1]: 367 | s0, o1, o2, s3 = M(s0, e[0]), s1+e[1], s2+e[2]&0xffff, M(s3, e[3]) 368 | s2 = M(o2^s0, e[4]) 369 | s1 = M((o1^s3)+s2&0xffff, e[5]) 370 | s0, s1, s2, s3 = s0^s1, s1^o2, s2+s1^o1, s3^s2+s1&0xffff 371 | e = self.e[-1] 372 | return struct.pack('>4H', M(s0, e[0]), s2+e[1]&0xffff, s1+e[2]&0xffff, M(s3, e[3])) 373 | 374 | class IDEA_CFB_Cipher(CFBCipher): 375 | KEY_LENGTH = 16 376 | IV_LENGTH = 8 377 | CIPHER = IDEA 378 | 379 | class SEED(RAW): 380 | S0 = base64.b64decode(b'qYXW01QdrCVdQxgeUfzKYyhEIJ3g4sgXpY8De7sT0u5wjD+oMt32dOyVC1dcW70BJBxzmBDM8tks53KDm9GGyWBQo+sNtp5Pt1rGeKYSr9Vhw7RBUn2NCB+ZABkEU/fh/XYvJ7CLDquibpNNaXwJCr/v88WHFP5k3i5LGgYha2YC9ZKKDLN+0HpHluUmgK3foTA3rjYVIjj0p0VMgemElzXLzjxxEceJdfva+JRZgsT/STlnwM/XuA+OQiORbNukNPFIwm89LUC+PrzBqrpOVTvcaH+c2EpWd6DtRrUrZfrjubGfXvnmsjHqbV/k8M2IFjpY1GIpBzPoGwV5kGoqmg==') 381 | S1 = base64.b64decode(b'OOgtps/es7ivYFXHRG9rW8NiM7UpoOKn05ERBhy8NkvviGyoF8QW9MJF4dY/PY6YKE72PqX5Dd/YK2Z6Jy/xckLUQcBzZ6yL962AH8osqjTSC+7pXZQY+FeuCMUTzYa5/33BMfWKarHRINcCIgRocQfbnZlhvuZZ3VGQ3Jqjq9CBD0ca4+yNv5Z7XKKhYyNNyJ6cOgwuum6fWvKS80l4zBX7cHV/NRADZG3GdNW06gl2Gf5AEuC9BfoB8CpeqVZDhRSJm7DlSHmX/B6CIYwbX3dUsh0lTwBG7VhS637ayf0wlWU8tuS7fA5QOSYyhGmTN+ckpMtTCofZTIOPzjtKtw==') 382 | G = lambda self, x, M=b'\xfc\xf3\xcf\x3f': sum((self.S0[x&0xff]&M[i]^self.S1[x>>8&0xff]&M[i+1&3]^self.S0[x>>16&0xff]&M[i+2&3]^self.S1[x>>24&0xff]&M[i+3&3])<QQ', key) 385 | for i, kc in enumerate((0x9e3779b9, 0x3c6ef373, 0x78dde6e6, 0xf1bbcdcc, 0xe3779b99, 0xc6ef3733, 0x8dde6e67, 0x1bbcdccf, 0x3779b99e, 0x6ef3733c, 0xdde6e678, 0xbbcdccf1, 0x779b99e3, 0xef3733c6, 0xde6e678d, 0xbcdccf1b)): 386 | self.e.append((self.G((key0>>32)+(key1>>32)-kc), self.G(key0-key1+kc))) 387 | key0, key1 = (key0, (key1<<8|key1>>56)&(1<<64)-1) if i&1 else ((key0<<56|key0>>8)&(1<<64)-1, key1) 388 | def encrypt(self, s): 389 | s = data.to_bytes(16, 'big') if isinstance(s, int) else s 390 | s0, s1, s2, s3 = struct.unpack('>4I', s) 391 | for k0, k1 in self.e: 392 | t0 = self.G(s2^k0^s3^k1) 393 | t1 = self.G(t0+(s2^k0)) 394 | t0 = self.G(t1+t0) 395 | s0, s1, s2, s3 = s2, s3, s0^t0+t1, s1^t0 396 | return struct.pack('>4I', s2&0xffffffff, s3, s0&0xffffffff, s1) 397 | 398 | class SEED_CFB_Cipher(CFBCipher): 399 | KEY_LENGTH = 16 400 | IV_LENGTH = 16 401 | CIPHER = SEED 402 | 403 | class RC2(RAW): 404 | S = base64.b64decode(b'2Xj5xBndte0o6f15SqDYncZ+N4MrdlOOYkxkiESL+6IXmln1h7NPE2FFbY0JgX0yvY9A64a3ewvwlSEiXGtOglTWZZPOYLIcc1bAFKeM8dwSdcofO77k0UI91DCjPLYmb78O2kZpB1cn8h2bvJRDA/gRx/aQ7z7nBsPVL8hmHtcI6OregFLu94Sqcqw1TWoqlhrScVoVSXRLn9BeBBik7MLgQW4PUcvMJJGvUKH0cDmZfDqFI7i0evwCNlslVZcxLV36mOOKkq4F3ykQZ2y6ydMA5s/hnqgsYxYBP1jiiakNODQbqzP/sLtIDF+5sc0uxfPbR+WlnHcKpiBo/n/BrQ==') 405 | B = list(range(20))+[-4,-3,-2,-1]+list(range(20,44))+[-4,-3,-2,-1]+list(range(44,64)) 406 | def __init__(self, key): 407 | e = bytearray(key) 408 | for i in range(128-len(key)): 409 | e.append(self.S[e[-1]+e[-len(key)]&0xff]) 410 | e[-len(key)] = self.S[e[-len(key)]] 411 | for i in range(127-len(key), -1, -1): 412 | e[i] = self.S[e[i+1]^e[i+len(key)]] 413 | self.e = struct.unpack('<64H', e) 414 | def encrypt(self, s): 415 | s = data.to_bytes(8, 'big') if isinstance(s, int) else s 416 | s = list(struct.unpack('<4H', s)) 417 | for j in self.B: 418 | s[j&3] = s[j&3]+self.e[j]+(s[j+3&3]&s[j+2&3])+(~s[j+3&3]&s[j+1&3])<>15-j%4*4//3 if j>=0 else s[j]+self.e[s[j+3]&0x3f] 419 | return struct.pack('<4H', *s) 420 | 421 | class RC2_CFB_Cipher(CFBCipher): 422 | KEY_LENGTH = 16 423 | IV_LENGTH = 8 424 | CIPHER = RC2 425 | 426 | MAP = {cls.name(): cls for name, cls in globals().items() if name.endswith('_Cipher')} 427 | 428 | -------------------------------------------------------------------------------- /pproxy/plugin.py: -------------------------------------------------------------------------------- 1 | import datetime, zlib, os, binascii, hmac, hashlib, time, random, collections 2 | 3 | packstr = lambda s, n=2: len(s).to_bytes(n, 'big') + s 4 | toint = lambda s, o='big': int.from_bytes(s, o) 5 | 6 | class BasePlugin(object): 7 | async def init_client_data(self, reader, writer, cipher): 8 | pass 9 | async def init_server_data(self, reader, writer, cipher, raddr): 10 | pass 11 | def add_cipher(self, cipher): 12 | pass 13 | @classmethod 14 | def name(cls): 15 | return cls.__name__.replace('_Plugin', '').replace('__', '.').lower() 16 | 17 | class Plain_Plugin(BasePlugin): 18 | pass 19 | 20 | class Origin_Plugin(BasePlugin): 21 | pass 22 | 23 | class Http_Simple_Plugin(BasePlugin): 24 | async def init_client_data(self, reader, writer, cipher): 25 | buf = await reader.read_until(b'\r\n\r\n') 26 | data = buf.split(b' ')[:2] 27 | data = bytes.fromhex(data[1][1:].replace(b'%',b'').decode()) 28 | reader._buffer[0:0] = data 29 | writer.write(b'HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Encoding: gzip\r\nContent-Type: text/html\r\nDate: ' + datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT').encode() + b'\r\nServer: nginx\r\nVary: Accept-Encoding\r\n\r\n') 30 | async def init_server_data(self, reader, writer, cipher, raddr): 31 | writer.write(f'GET / HTTP/1.1\r\nHost: {raddr}\r\nUser-Agent: curl\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\n\r\n'.encode()) 32 | await reader.read_until(b'\r\n\r\n') 33 | 34 | TIMESTAMP_TOLERANCE = 5 * 60 35 | 36 | class Tls1__2_Ticket_Auth_Plugin(BasePlugin): 37 | CACHE = collections.deque(maxlen = 100) 38 | async def init_client_data(self, reader, writer, cipher): 39 | key = cipher.cipher(cipher.key).key 40 | assert await reader.read_n(3) == b'\x16\x03\x01' 41 | header = await reader.read_n(toint(await reader.read_n(2))) 42 | assert header[:2] == b'\x01\x00' 43 | assert header[4:6] == b'\x03\x03' 44 | cacheid = header[6:28] 45 | sessionid = header[39:39+header[38]] 46 | assert cacheid not in self.CACHE 47 | self.CACHE.append(cacheid) 48 | utc_time = int(time.time()) 49 | assert hmac.new(key+sessionid, cacheid, hashlib.sha1).digest()[:10] == header[28:38] 50 | assert abs(toint(header[6:10]) - utc_time) < TIMESTAMP_TOLERANCE 51 | addhmac = lambda s: s + hmac.new(key+sessionid, s, hashlib.sha1).digest()[:10] 52 | writer.write(addhmac((b"\x16\x03\x03" + packstr(b"\x02\x00" + packstr(b'\x03\x03' + addhmac(utc_time.to_bytes(4, 'big') + os.urandom(18)) + b'\x20' + sessionid + b'\xc0\x2f\x00\x00\x05\xff\x01\x00\x01\x00')) + (b"\x16\x03\x03" + packstr(b"\x04\x00" + packstr(os.urandom(random.randrange(164)*2+64))) if random.randint(0, 8) < 1 else b'') + b"\x14\x03\x03\x00\x01\x01\x16\x03\x03" + packstr(os.urandom(random.choice((32, 40)))))[:-10])) 53 | 54 | async def init_server_data(self, reader, writer, cipher, raddr): 55 | key = cipher.cipher(cipher.key).key 56 | sessionid = os.urandom(32) 57 | addhmac = lambda s: s + hmac.new(key+sessionid, s, hashlib.sha1).digest()[:10] 58 | writer.write(b"\x16\x03\x01" + packstr(b"\x01\x00" + packstr(b'\x03\x03' + addhmac(int(time.time()).to_bytes(4, 'big') + os.urandom(18)) + b"\x20" + sessionid + b"\x00\x1c\xc0\x2b\xc0\x2f\xcc\xa9\xcc\xa8\xcc\x14\xcc\x13\xc0\x0a\xc0\x14\xc0\x09\xc0\x13\x00\x9c\x00\x35\x00\x2f\x00\x0a\x01\x00" + packstr(b"\xff\x01\x00\x01\x00\x00\x00" + packstr(packstr(b"\x00" + packstr(raddr.encode()))) + b"\x00\x17\x00\x00\x00\x23" + packstr(os.urandom((random.randrange(17)+8)*16)) + b"\x00\x0d\x00\x16\x00\x14\x06\x01\x06\x03\x05\x01\x05\x03\x04\x01\x04\x03\x03\x01\x03\x03\x02\x01\x02\x03\x00\x05\x00\x05\x01\x00\x00\x00\x00\x00\x12\x00\x00\x75\x50\x00\x00\x00\x0b\x00\x02\x01\x00\x00\x0a\x00\x06\x00\x04\x00\x17\x00\x18")))) 59 | writer.write(addhmac(b'\x14\x03\x03\x00\x01\x01\x16\x03\x03\x00\x20' + os.urandom(22))) 60 | 61 | def add_cipher(self, cipher): 62 | self.buf = bytearray() 63 | def decrypt(s): 64 | self.buf.extend(s) 65 | ret = b'' 66 | while len(self.buf) >= 5: 67 | l = int.from_bytes(self.buf[3:5], 'big') 68 | if len(self.buf) < l: 69 | break 70 | if self.buf[:3] in (b'\x16\x03\x03', b'\x14\x03\x03'): 71 | del self.buf[:5+l] 72 | continue 73 | assert self.buf[:3] == b'\x17\x03\x03' 74 | data = self.buf[5:5+l] 75 | ret += data 76 | del self.buf[:5+l] 77 | return ret 78 | def pack(s): 79 | return b'\x17\x03\x03' + packstr(s) 80 | def encrypt(s): 81 | ret = b'' 82 | while len(s) > 2048: 83 | size = min(random.randrange(4096)+100, len(s)) 84 | ret += pack(s[:size]) 85 | s = s[size:] 86 | if s: 87 | ret += pack(s) 88 | return ret 89 | cipher.pdecrypt2 = decrypt 90 | cipher.pencrypt2 = encrypt 91 | 92 | class Verify_Simple_Plugin(BasePlugin): 93 | def add_cipher(self, cipher): 94 | self.buf = bytearray() 95 | def decrypt(s): 96 | self.buf.extend(s) 97 | ret = b'' 98 | while len(self.buf) >= 2: 99 | l = int.from_bytes(self.buf[:2], 'big') 100 | if len(self.buf) < l: 101 | break 102 | data = self.buf[2+self.buf[2]:l-4] 103 | crc = (-1 - binascii.crc32(self.buf[:l-4])) & 0xffffffff 104 | assert int.from_bytes(self.buf[l-4:l], 'little') == crc 105 | ret += data 106 | del self.buf[:l] 107 | return ret 108 | def pack(s): 109 | rnd_data = os.urandom(os.urandom(1)[0] % 16) 110 | data = bytes([len(rnd_data)+1]) + rnd_data + s 111 | data = (len(data)+6).to_bytes(2, 'big') + data 112 | crc = (-1 - binascii.crc32(data)) & 0xffffffff 113 | return data + crc.to_bytes(4, 'little') 114 | def encrypt(s): 115 | ret = b'' 116 | while len(s) > 8100: 117 | ret += pack(s[:8100]) 118 | s = s[8100:] 119 | if s: 120 | ret += pack(s) 121 | return ret 122 | cipher.pdecrypt = decrypt 123 | cipher.pencrypt = encrypt 124 | 125 | class Verify_Deflate_Plugin(BasePlugin): 126 | def add_cipher(self, cipher): 127 | self.buf = bytearray() 128 | def decrypt(s): 129 | self.buf.extend(s) 130 | ret = b'' 131 | while len(self.buf) >= 2: 132 | l = int.from_bytes(self.buf[:2], 'big') 133 | if len(self.buf) < l: 134 | break 135 | ret += zlib.decompress(b'x\x9c' + self.buf[2:l]) 136 | del self.buf[:l] 137 | return ret 138 | def pack(s): 139 | packed = zlib.compress(s) 140 | return len(packed).to_bytes(2, 'big') + packed[2:] 141 | def encrypt(s): 142 | ret = b'' 143 | while len(s) > 32700: 144 | ret += pack(s[:32700]) 145 | s = s[32700:] 146 | if s: 147 | ret += pack(s) 148 | return ret 149 | cipher.pdecrypt = decrypt 150 | cipher.pencrypt = encrypt 151 | 152 | PLUGIN = {cls.name(): cls for name, cls in globals().items() if name.endswith('_Plugin')} 153 | 154 | def get_plugin(plugin_name): 155 | if plugin_name not in PLUGIN: 156 | return f'existing plugins: {sorted(PLUGIN.keys())}', None 157 | return None, PLUGIN[plugin_name]() 158 | 159 | -------------------------------------------------------------------------------- /pproxy/proto.py: -------------------------------------------------------------------------------- 1 | import asyncio, socket, urllib.parse, time, re, base64, hmac, struct, hashlib, io, os 2 | from . import admin 3 | HTTP_LINE = re.compile('([^ ]+) +(.+?) +(HTTP/[^ ]+)$') 4 | packstr = lambda s, n=1: len(s).to_bytes(n, 'big') + s 5 | 6 | def netloc_split(loc, default_host=None, default_port=None): 7 | ipv6 = re.fullmatch(r'\[([0-9a-fA-F:]*)\](?::(\d+)?)?', loc) 8 | if ipv6: 9 | host_name, port = ipv6.groups() 10 | elif ':' in loc: 11 | host_name, port = loc.rsplit(':', 1) 12 | else: 13 | host_name, port = loc, None 14 | return host_name or default_host, int(port) if port else default_port 15 | 16 | async def socks_address_stream(reader, n): 17 | if n in (1, 17): 18 | data = await reader.read_n(4) 19 | host_name = socket.inet_ntoa(data) 20 | elif n in (3, 19): 21 | data = await reader.read_n(1) 22 | data += await reader.read_n(data[0]) 23 | host_name = data[1:].decode() 24 | elif n in (4, 20): 25 | data = await reader.read_n(16) 26 | host_name = socket.inet_ntop(socket.AF_INET6, data) 27 | else: 28 | raise Exception(f'Unknown address header {n}') 29 | data_port = await reader.read_n(2) 30 | return host_name, int.from_bytes(data_port, 'big'), data+data_port 31 | 32 | def socks_address(reader, n): 33 | return socket.inet_ntoa(reader.read(4)) if n == 1 else \ 34 | reader.read(reader.read(1)[0]).decode() if n == 3 else \ 35 | socket.inet_ntop(socket.AF_INET6, reader.read(16)), \ 36 | int.from_bytes(reader.read(2), 'big') 37 | 38 | class BaseProtocol: 39 | def __init__(self, param): 40 | self.param = param 41 | @property 42 | def name(self): 43 | return self.__class__.__name__.lower() 44 | def reuse(self): 45 | return False 46 | def udp_accept(self, data, **kw): 47 | raise Exception(f'{self.name} don\'t support UDP server') 48 | def udp_connect(self, rauth, host_name, port, data, **kw): 49 | raise Exception(f'{self.name} don\'t support UDP client') 50 | def udp_unpack(self, data): 51 | return data 52 | def udp_pack(self, host_name, port, data): 53 | return data 54 | async def connect(self, reader_remote, writer_remote, rauth, host_name, port, **kw): 55 | raise Exception(f'{self.name} don\'t support client') 56 | async def channel(self, reader, writer, stat_bytes, stat_conn): 57 | try: 58 | stat_conn(1) 59 | while not reader.at_eof() and not writer.is_closing(): 60 | data = await reader.read(65536) 61 | if not data: 62 | break 63 | if stat_bytes is None: 64 | continue 65 | stat_bytes(len(data)) 66 | writer.write(data) 67 | await writer.drain() 68 | except Exception: 69 | pass 70 | finally: 71 | stat_conn(-1) 72 | writer.close() 73 | 74 | class Direct(BaseProtocol): 75 | pass 76 | 77 | class Trojan(BaseProtocol): 78 | async def guess(self, reader, users, **kw): 79 | header = await reader.read_w(56) 80 | if users: 81 | for user in users: 82 | if hashlib.sha224(user).hexdigest().encode() == header: 83 | return user 84 | else: 85 | if hashlib.sha224(b'').hexdigest().encode() == header: 86 | return True 87 | reader.rollback(header) 88 | async def accept(self, reader, user, **kw): 89 | assert await reader.read_n(2) == b'\x0d\x0a' 90 | if (await reader.read_n(1))[0] != 1: 91 | raise Exception('Connection closed') 92 | host_name, port, _ = await socks_address_stream(reader, (await reader.read_n(1))[0]) 93 | assert await reader.read_n(2) == b'\x0d\x0a' 94 | return user, host_name, port 95 | async def connect(self, reader_remote, writer_remote, rauth, host_name, port, **kw): 96 | toauth = hashlib.sha224(rauth or b'').hexdigest().encode() 97 | writer_remote.write(toauth + b'\x0d\x0a\x01\x03' + packstr(host_name.encode()) + port.to_bytes(2, 'big') + b'\x0d\x0a') 98 | 99 | class SSR(BaseProtocol): 100 | async def guess(self, reader, users, **kw): 101 | if users: 102 | header = await reader.read_w(max(len(i) for i in users)) 103 | reader.rollback(header) 104 | user = next(filter(lambda x: x == header[:len(x)], users), None) 105 | if user is None: 106 | return 107 | await reader.read_n(len(user)) 108 | return user 109 | header = await reader.read_w(1) 110 | reader.rollback(header) 111 | return header[0] in (1, 3, 4, 17, 19, 20) 112 | async def accept(self, reader, user, **kw): 113 | host_name, port, data = await socks_address_stream(reader, (await reader.read_n(1))[0]) 114 | return user, host_name, port 115 | async def connect(self, reader_remote, writer_remote, rauth, host_name, port, **kw): 116 | writer_remote.write(rauth + b'\x03' + packstr(host_name.encode()) + port.to_bytes(2, 'big')) 117 | 118 | class SS(SSR): 119 | def patch_ota_reader(self, cipher, reader): 120 | chunk_id, data_len, _buffer = 0, None, bytearray() 121 | def decrypt(s): 122 | nonlocal chunk_id, data_len 123 | _buffer.extend(s) 124 | ret = bytearray() 125 | while 1: 126 | if data_len is None: 127 | if len(_buffer) < 2: 128 | break 129 | data_len = int.from_bytes(_buffer[:2], 'big') 130 | del _buffer[:2] 131 | else: 132 | if len(_buffer) < 10+data_len: 133 | break 134 | data = _buffer[10:10+data_len] 135 | assert _buffer[:10] == hmac.new(cipher.iv+chunk_id.to_bytes(4, 'big'), data, hashlib.sha1).digest()[:10] 136 | del _buffer[:10+data_len] 137 | data_len = None 138 | chunk_id += 1 139 | ret.extend(data) 140 | return bytes(ret) 141 | reader.decrypts.append(decrypt) 142 | if reader._buffer: 143 | reader._buffer = bytearray(decrypt(reader._buffer)) 144 | def patch_ota_writer(self, cipher, writer): 145 | chunk_id = 0 146 | def write(data, o=writer.write): 147 | nonlocal chunk_id 148 | if not data: return 149 | checksum = hmac.new(cipher.iv+chunk_id.to_bytes(4, 'big'), data, hashlib.sha1).digest() 150 | chunk_id += 1 151 | return o(len(data).to_bytes(2, 'big') + checksum[:10] + data) 152 | writer.write = write 153 | async def accept(self, reader, user, reader_cipher, **kw): 154 | header = await reader.read_n(1) 155 | ota = (header[0] & 0x10 == 0x10) 156 | host_name, port, data = await socks_address_stream(reader, header[0]) 157 | assert ota or not reader_cipher or not reader_cipher.ota, 'SS client must support OTA' 158 | if ota and reader_cipher: 159 | checksum = hmac.new(reader_cipher.iv+reader_cipher.key, header+data, hashlib.sha1).digest() 160 | assert checksum[:10] == await reader.read_n(10), 'Unknown OTA checksum' 161 | self.patch_ota_reader(reader_cipher, reader) 162 | return user, host_name, port 163 | async def connect(self, reader_remote, writer_remote, rauth, host_name, port, writer_cipher_r, **kw): 164 | writer_remote.write(rauth) 165 | if writer_cipher_r and writer_cipher_r.ota: 166 | rdata = b'\x13' + packstr(host_name.encode()) + port.to_bytes(2, 'big') 167 | checksum = hmac.new(writer_cipher_r.iv+writer_cipher_r.key, rdata, hashlib.sha1).digest() 168 | writer_remote.write(rdata + checksum[:10]) 169 | self.patch_ota_writer(writer_cipher_r, writer_remote) 170 | else: 171 | writer_remote.write(b'\x03' + packstr(host_name.encode()) + port.to_bytes(2, 'big')) 172 | def udp_accept(self, data, users, **kw): 173 | reader = io.BytesIO(data) 174 | user = True 175 | if users: 176 | user = next(filter(lambda i: data[:len(i)]==i, users), None) 177 | if user is None: 178 | return 179 | reader.read(len(user)) 180 | n = reader.read(1)[0] 181 | if n not in (1, 3, 4): 182 | return 183 | host_name, port = socks_address(reader, n) 184 | return user, host_name, port, reader.read() 185 | def udp_unpack(self, data): 186 | reader = io.BytesIO(data) 187 | n = reader.read(1)[0] 188 | host_name, port = socks_address(reader, n) 189 | return reader.read() 190 | def udp_pack(self, host_name, port, data): 191 | try: 192 | return b'\x01' + socket.inet_aton(host_name) + port.to_bytes(2, 'big') + data 193 | except Exception: 194 | pass 195 | return b'\x03' + packstr(host_name.encode()) + port.to_bytes(2, 'big') + data 196 | def udp_connect(self, rauth, host_name, port, data, **kw): 197 | return rauth + b'\x03' + packstr(host_name.encode()) + port.to_bytes(2, 'big') + data 198 | 199 | class Socks4(BaseProtocol): 200 | async def guess(self, reader, **kw): 201 | header = await reader.read_w(1) 202 | if header == b'\x04': 203 | return True 204 | reader.rollback(header) 205 | async def accept(self, reader, user, writer, users, authtable, **kw): 206 | assert await reader.read_n(1) == b'\x01' 207 | port = int.from_bytes(await reader.read_n(2), 'big') 208 | ip = await reader.read_n(4) 209 | userid = (await reader.read_until(b'\x00'))[:-1] 210 | user = authtable.authed() 211 | if users: 212 | if userid in users: 213 | user = userid 214 | elif not user: 215 | raise Exception(f'Unauthorized SOCKS {userid}') 216 | authtable.set_authed(user) 217 | writer.write(b'\x00\x5a' + port.to_bytes(2, 'big') + ip) 218 | return user, socket.inet_ntoa(ip), port 219 | async def connect(self, reader_remote, writer_remote, rauth, host_name, port, **kw): 220 | ip = socket.inet_aton((await asyncio.get_event_loop().getaddrinfo(host_name, port, family=socket.AF_INET))[0][4][0]) 221 | writer_remote.write(b'\x04\x01' + port.to_bytes(2, 'big') + ip + rauth + b'\x00') 222 | assert await reader_remote.read_n(2) == b'\x00\x5a' 223 | await reader_remote.read_n(6) 224 | 225 | class Socks5(BaseProtocol): 226 | async def guess(self, reader, **kw): 227 | header = await reader.read_w(1) 228 | if header == b'\x05': 229 | return True 230 | reader.rollback(header) 231 | async def accept(self, reader, user, writer, users, authtable, **kw): 232 | methods = await reader.read_n((await reader.read_n(1))[0]) 233 | user = authtable.authed() 234 | if users and (not user or b'\x00' not in methods): 235 | if b'\x02' not in methods: 236 | raise Exception(f'Unauthorized SOCKS') 237 | writer.write(b'\x05\x02') 238 | assert (await reader.read_n(1))[0] == 1, 'Unknown SOCKS auth' 239 | u = await reader.read_n((await reader.read_n(1))[0]) 240 | p = await reader.read_n((await reader.read_n(1))[0]) 241 | user = u+b':'+p 242 | if user not in users: 243 | raise Exception(f'Unauthorized SOCKS {u}:{p}') 244 | writer.write(b'\x01\x00') 245 | elif users and not user: 246 | raise Exception(f'Unauthorized SOCKS') 247 | else: 248 | writer.write(b'\x05\x00') 249 | if users: 250 | authtable.set_authed(user) 251 | assert await reader.read_n(3) == b'\x05\x01\x00', 'Unknown SOCKS protocol' 252 | header = await reader.read_n(1) 253 | host_name, port, data = await socks_address_stream(reader, header[0]) 254 | writer.write(b'\x05\x00\x00' + header + data) 255 | return user, host_name, port 256 | async def connect(self, reader_remote, writer_remote, rauth, host_name, port, **kw): 257 | if rauth: 258 | writer_remote.write(b'\x05\x01\x02') 259 | assert await reader_remote.read_n(2) == b'\x05\x02' 260 | writer_remote.write(b'\x01' + b''.join(packstr(i) for i in rauth.split(b':', 1))) 261 | assert await reader_remote.read_n(2) == b'\x01\x00', 'Unknown SOCKS auth' 262 | else: 263 | writer_remote.write(b'\x05\x01\x00') 264 | assert await reader_remote.read_n(2) == b'\x05\x00' 265 | writer_remote.write(b'\x05\x01\x00\x03' + packstr(host_name.encode()) + port.to_bytes(2, 'big')) 266 | assert await reader_remote.read_n(3) == b'\x05\x00\x00' 267 | header = (await reader_remote.read_n(1))[0] 268 | await reader_remote.read_n(6 if header == 1 else (18 if header == 4 else (await reader_remote.read_n(1))[0]+2)) 269 | def udp_accept(self, data, **kw): 270 | reader = io.BytesIO(data) 271 | if reader.read(3) != b'\x00\x00\x00': 272 | return 273 | n = reader.read(1)[0] 274 | if n not in (1, 3, 4): 275 | return 276 | host_name, port = socks_address(reader, n) 277 | return True, host_name, port, reader.read() 278 | def udp_connect(self, rauth, host_name, port, data, **kw): 279 | return b'\x00\x00\x00\x03' + packstr(host_name.encode()) + port.to_bytes(2, 'big') + data 280 | 281 | class HTTP(BaseProtocol): 282 | async def guess(self, reader, **kw): 283 | header = await reader.read_w(4) 284 | reader.rollback(header) 285 | return header in (b'GET ', b'HEAD', b'POST', b'PUT ', b'DELE', b'CONN', b'OPTI', b'TRAC', b'PATC') 286 | async def accept(self, reader, user, writer, **kw): 287 | lines = await reader.read_until(b'\r\n\r\n') 288 | headers = lines[:-4].decode().split('\r\n') 289 | method, path, ver = HTTP_LINE.match(headers.pop(0)).groups() 290 | lines = '\r\n'.join(i for i in headers if not i.startswith('Proxy-')) 291 | headers = dict(i.split(': ', 1) for i in headers if ': ' in i) 292 | async def reply(code, message, body=None, wait=False): 293 | writer.write(message) 294 | if body: 295 | writer.write(body) 296 | if wait: 297 | await writer.drain() 298 | return await self.http_accept(user, method, path, None, ver, lines, headers.get('Host', ''), headers.get('Proxy-Authorization'), reply, **kw) 299 | async def http_accept(self, user, method, path, authority, ver, lines, host, pauth, reply, authtable, users, httpget=None, **kw): 300 | url = urllib.parse.urlparse(path) 301 | if method == 'GET' and not url.hostname: 302 | for path, text in (httpget.items() if httpget else ()): 303 | if path == url.path: 304 | user = next(filter(lambda x: x.decode()==url.query, users), None) if users else True 305 | if user: 306 | if users: 307 | authtable.set_authed(user) 308 | if type(text) is str: 309 | text = (text % dict(host=host)).encode() 310 | await reply(200, f'{ver} 200 OK\r\nConnection: close\r\nContent-Type: text/plain\r\nCache-Control: max-age=900\r\nContent-Length: {len(text)}\r\n\r\n'.encode(), text, True) 311 | raise Exception('Connection closed') 312 | raise Exception(f'404 {method} {url.path}') 313 | if users: 314 | user = authtable.authed() 315 | if not user: 316 | user = next(filter(lambda i: ('Basic '+base64.b64encode(i).decode()) == pauth, users), None) 317 | if user is None: 318 | await reply(407, f'{ver} 407 Proxy Authentication Required\r\nConnection: close\r\nProxy-Authenticate: Basic realm="simple"\r\n\r\n'.encode(), wait=True) 319 | raise Exception('Unauthorized HTTP') 320 | authtable.set_authed(user) 321 | if method == 'CONNECT': 322 | host_name, port = netloc_split(authority or path) 323 | return user, host_name, port, lambda writer: reply(200, f'{ver} 200 Connection established\r\nConnection: close\r\n\r\n'.encode()) 324 | else: 325 | host_name, port = netloc_split(url.netloc or host, default_port=80) 326 | newpath = url._replace(netloc='', scheme='').geturl() 327 | async def connected(writer): 328 | writer.write(f'{method} {newpath} {ver}\r\n{lines}\r\n\r\n'.encode()) 329 | return True 330 | return user, host_name, port, connected 331 | async def connect(self, reader_remote, writer_remote, rauth, host_name, port, **kw): 332 | writer_remote.write(f'CONNECT {host_name}:{port} HTTP/1.1\r\nHost: {host_name}:{port}'.encode() + (b'\r\nProxy-Authorization: Basic '+base64.b64encode(rauth) if rauth else b'') + b'\r\n\r\n') 333 | await reader_remote.read_until(b'\r\n\r\n') 334 | async def http_channel(self, reader, writer, stat_bytes, stat_conn): 335 | try: 336 | stat_conn(1) 337 | while not reader.at_eof() and not writer.is_closing(): 338 | data = await reader.read(65536) 339 | if not data: 340 | break 341 | if b'\r\n' in data and HTTP_LINE.match(data.split(b'\r\n', 1)[0].decode()): 342 | if b'\r\n\r\n' not in data: 343 | data += await reader.readuntil(b'\r\n\r\n') 344 | lines, data = data.split(b'\r\n\r\n', 1) 345 | headers = lines.decode().split('\r\n') 346 | method, path, ver = HTTP_LINE.match(headers.pop(0)).groups() 347 | lines = '\r\n'.join(i for i in headers if not i.startswith('Proxy-')) 348 | headers = dict(i.split(': ', 1) for i in headers if ': ' in i) 349 | newpath = urllib.parse.urlparse(path)._replace(netloc='', scheme='').geturl() 350 | data = f'{method} {newpath} {ver}\r\n{lines}\r\n\r\n'.encode() + data 351 | stat_bytes(len(data)) 352 | writer.write(data) 353 | await writer.drain() 354 | except Exception: 355 | pass 356 | finally: 357 | stat_conn(-1) 358 | writer.close() 359 | 360 | class HTTPOnly(HTTP): 361 | async def connect(self, reader_remote, writer_remote, rauth, host_name, port, myhost, **kw): 362 | buffer = bytearray() 363 | HOST_NAME = re.compile('\r\nHost: ([^\r\n]+)\r\n', re.I) 364 | def write(data, o=writer_remote.write): 365 | if not data: return 366 | buffer.extend(data) 367 | pos = buffer.find(b'\r\n\r\n') 368 | if pos != -1: 369 | text = buffer[:pos].decode() 370 | header = HTTP_LINE.match(text.split('\r\n', 1)[0]) 371 | host = HOST_NAME.search(text) 372 | if not header or not host: 373 | writer_remote.close() 374 | raise Exception('Unknown HTTP header for protocol HTTPOnly') 375 | method, path, ver = header.groups() 376 | data = f'{method} http://{host.group(1)}{path} {ver}\r\nHost: {host.group(1)}'.encode() + (b'\r\nProxy-Authorization: Basic '+base64.b64encode(rauth) if rauth else b'') + b'\r\n\r\n' + buffer[pos+4:] 377 | buffer.clear() 378 | return o(data) 379 | writer_remote.write = write 380 | 381 | class H2(HTTP): 382 | async def guess(self, reader, **kw): 383 | return True 384 | async def accept(self, reader, user, writer, **kw): 385 | if not writer.headers.done(): 386 | await writer.headers 387 | headers = writer.headers.result() 388 | headers = {i.decode().lower():j.decode() for i,j in headers} 389 | lines = '\r\n'.join(i for i in headers if not i.startswith('proxy-') and not i.startswith(':')) 390 | async def reply(code, message, body=None, wait=False): 391 | writer.send_headers(((':status', str(code)),)) 392 | if body: 393 | writer.write(body) 394 | if wait: 395 | await writer.drain() 396 | return await self.http_accept(user, headers[':method'], headers[':path'], headers[':authority'], '2.0', lines, '', headers.get('proxy-authorization'), reply, **kw) 397 | async def connect(self, reader_remote, writer_remote, rauth, host_name, port, myhost, **kw): 398 | headers = [(':method', 'CONNECT'), (':scheme', 'https'), (':path', '/'), 399 | (':authority', f'{host_name}:{port}')] 400 | if rauth: 401 | headers.append(('proxy-authorization', 'Basic '+base64.b64encode(rauth))) 402 | writer_remote.send_headers(headers) 403 | 404 | class H3(H2): 405 | pass 406 | 407 | 408 | class HTTPAdmin(HTTP): 409 | async def accept(self, reader, user, writer, **kw): 410 | lines = await reader.read_until(b'\r\n\r\n') 411 | headers = lines[:-4].decode().split('\r\n') 412 | method, path, ver = HTTP_LINE.match(headers.pop(0)).groups() 413 | lines = '\r\n'.join(i for i in headers if not i.startswith('Proxy-')) 414 | headers = dict(i.split(': ', 1) for i in headers if ': ' in i) 415 | async def reply(code, message, body=None, wait=False): 416 | writer.write(message) 417 | if body: 418 | writer.write(body) 419 | if wait: 420 | await writer.drain() 421 | 422 | content_length = int(headers.get('Content-Length','0')) 423 | content = '' 424 | if content_length > 0: 425 | content = await reader.read_n(content_length) 426 | 427 | url = urllib.parse.urlparse(path) 428 | if url.hostname is not None: 429 | raise Exception(f'HTTP Admin Unsupported hostname') 430 | if method in ["GET", "POST", "PUT", "PATCH", "DELETE"]: 431 | for path, handler in admin.httpget.items(): 432 | if path == url.path: 433 | await handler(reply=reply, ver=ver, method=method, headers=headers, lines=lines, content=content) 434 | raise Exception('Connection closed') 435 | raise Exception(f'404 {method} {url.path}') 436 | raise Exception(f'405 {method} not allowed') 437 | 438 | 439 | class SSH(BaseProtocol): 440 | async def connect(self, reader_remote, writer_remote, rauth, host_name, port, myhost, **kw): 441 | pass 442 | 443 | class Transparent(BaseProtocol): 444 | async def guess(self, reader, sock, **kw): 445 | remote = self.query_remote(sock) 446 | return remote is not None and (sock is None or sock.getsockname() != remote) 447 | async def accept(self, reader, user, sock, **kw): 448 | remote = self.query_remote(sock) 449 | return user, remote[0], remote[1] 450 | def udp_accept(self, data, sock, **kw): 451 | remote = self.query_remote(sock) 452 | return True, remote[0], remote[1], data 453 | 454 | SO_ORIGINAL_DST = 80 455 | SOL_IPV6 = 41 456 | class Redir(Transparent): 457 | def query_remote(self, sock): 458 | try: 459 | #if sock.family == socket.AF_INET: 460 | if "." in sock.getsockname()[0]: 461 | buf = sock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, 16) 462 | assert len(buf) == 16 463 | return socket.inet_ntoa(buf[4:8]), int.from_bytes(buf[2:4], 'big') 464 | else: 465 | buf = sock.getsockopt(SOL_IPV6, SO_ORIGINAL_DST, 28) 466 | assert len(buf) == 28 467 | return socket.inet_ntop(socket.AF_INET6, buf[8:24]), int.from_bytes(buf[2:4], 'big') 468 | except Exception: 469 | pass 470 | 471 | class Pf(Transparent): 472 | def query_remote(self, sock): 473 | try: 474 | import fcntl 475 | src = sock.getpeername() 476 | dst = sock.getsockname() 477 | src_ip = socket.inet_pton(sock.family, src[0]) 478 | dst_ip = socket.inet_pton(sock.family, dst[0]) 479 | pnl = bytearray(struct.pack('!16s16s32xHxxHxx8xBBxB', src_ip, dst_ip, src[1], dst[1], sock.family, socket.IPPROTO_TCP, 2)) 480 | if not hasattr(self, 'pf'): 481 | self.pf = open('/dev/pf', 'a+b') 482 | fcntl.ioctl(self.pf.fileno(), 0xc0544417, pnl) 483 | return socket.inet_ntop(sock.family, pnl[48:48+len(src_ip)]), int.from_bytes(pnl[76:78], 'big') 484 | except Exception: 485 | pass 486 | 487 | class Tunnel(Transparent): 488 | def query_remote(self, sock): 489 | if not self.param: 490 | return 'tunnel', 0 491 | dst = sock.getsockname() if sock else (None, None) 492 | return netloc_split(self.param, dst[0], dst[1]) 493 | async def connect(self, reader_remote, writer_remote, rauth, host_name, port, **kw): 494 | pass 495 | def udp_connect(self, rauth, host_name, port, data, **kw): 496 | return data 497 | 498 | class WS(BaseProtocol): 499 | async def guess(self, reader, **kw): 500 | header = await reader.read_w(4) 501 | reader.rollback(header) 502 | return reader == b'GET ' 503 | def patch_ws_stream(self, reader, writer, masked=False): 504 | data_len, mask_key, _buffer = None, None, bytearray() 505 | def feed_data(s, o=reader.feed_data): 506 | nonlocal data_len, mask_key 507 | _buffer.extend(s) 508 | while 1: 509 | if data_len is None: 510 | if len(_buffer) < 2: 511 | break 512 | required = 2 + (4 if _buffer[1]&128 else 0) 513 | p = _buffer[1] & 127 514 | required += 2 if p == 126 else 4 if p == 127 else 0 515 | if len(_buffer) < required: 516 | break 517 | data_len = int.from_bytes(_buffer[2:4], 'big') if p == 126 else int.from_bytes(_buffer[2:6], 'big') if p == 127 else p 518 | mask_key = _buffer[required-4:required] if _buffer[1]&128 else None 519 | del _buffer[:required] 520 | else: 521 | if len(_buffer) < data_len: 522 | break 523 | data = _buffer[:data_len] 524 | if mask_key: 525 | data = bytes(data[i]^mask_key[i%4] for i in range(data_len)) 526 | del _buffer[:data_len] 527 | data_len = None 528 | o(data) 529 | reader.feed_data = feed_data 530 | if reader._buffer: 531 | reader._buffer, buf = bytearray(), reader._buffer 532 | feed_data(buf) 533 | def write(data, o=writer.write): 534 | if not data: return 535 | data_len = len(data) 536 | if masked: 537 | mask_key = os.urandom(4) 538 | data = bytes(data[i]^mask_key[i%4] for i in range(data_len)) 539 | return o(b'\x02' + (bytes([data_len|0x80]) if data_len < 126 else b'\xfe'+data_len.to_bytes(2, 'big') if data_len < 65536 else b'\xff'+data_len.to_bytes(4, 'big')) + mask_key + data) 540 | else: 541 | return o(b'\x02' + (bytes([data_len]) if data_len < 126 else b'\x7e'+data_len.to_bytes(2, 'big') if data_len < 65536 else b'\x7f'+data_len.to_bytes(4, 'big')) + data) 542 | writer.write = write 543 | async def accept(self, reader, user, writer, users, authtable, sock, **kw): 544 | lines = await reader.read_until(b'\r\n\r\n') 545 | headers = lines[:-4].decode().split('\r\n') 546 | method, path, ver = HTTP_LINE.match(headers.pop(0)).groups() 547 | lines = '\r\n'.join(i for i in headers if not i.startswith('Proxy-')) 548 | headers = dict(i.split(': ', 1) for i in headers if ': ' in i) 549 | url = urllib.parse.urlparse(path) 550 | if users: 551 | pauth = headers.get('Proxy-Authorization', None) 552 | user = authtable.authed() 553 | if not user: 554 | user = next(filter(lambda i: ('Basic '+base64.b64encode(i).decode()) == pauth, users), None) 555 | if user is None: 556 | writer.write(f'{ver} 407 Proxy Authentication Required\r\nConnection: close\r\nProxy-Authenticate: Basic realm="simple"\r\n\r\n'.encode()) 557 | raise Exception('Unauthorized WebSocket') 558 | authtable.set_authed(user) 559 | if method != 'GET': 560 | raise Exception(f'Unsupported method {method}') 561 | if headers.get('Sec-WebSocket-Key', None) is None: 562 | raise Exception(f'Unsupported headers {headers}') 563 | seckey = base64.b64decode(headers.get('Sec-WebSocket-Key')) 564 | rseckey = base64.b64encode(hashlib.sha1(seckey+b'amtf').digest()[:16]).decode() 565 | writer.write(f'{ver} 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {rseckey}\r\nSec-WebSocket-Protocol: chat\r\n\r\n'.encode()) 566 | self.patch_ws_stream(reader, writer, False) 567 | if not self.param: 568 | return 'tunnel', 0 569 | dst = sock.getsockname() 570 | host, port = netloc_split(self.param, dst[0], dst[1]) 571 | return user, host, port 572 | async def connect(self, reader_remote, writer_remote, rauth, host_name, port, myhost, **kw): 573 | seckey = base64.b64encode(os.urandom(16)).decode() 574 | writer_remote.write(f'GET / HTTP/1.1\r\nHost: {myhost}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {seckey}\r\nSec-WebSocket-Protocol: chat\r\nSec-WebSocket-Version: 13'.encode() + (b'\r\nProxy-Authorization: Basic '+base64.b64encode(rauth) if rauth else b'') + b'\r\n\r\n') 575 | await reader_remote.read_until(b'\r\n\r\n') 576 | self.patch_ws_stream(reader_remote, writer_remote, True) 577 | 578 | class Echo(Transparent): 579 | def query_remote(self, sock): 580 | return 'echo', 0 581 | 582 | async def accept(protos, reader, **kw): 583 | for proto in protos: 584 | try: 585 | user = await proto.guess(reader, **kw) 586 | except Exception: 587 | raise Exception('Connection closed') 588 | if user: 589 | ret = await proto.accept(reader, user, **kw) 590 | while len(ret) < 4: 591 | ret += (None,) 592 | return (proto,) + ret 593 | raise Exception(f'Unsupported protocol') 594 | 595 | def udp_accept(protos, data, **kw): 596 | for proto in protos: 597 | ret = proto.udp_accept(data, **kw) 598 | if ret: 599 | return (proto,) + ret 600 | raise Exception(f'Unsupported protocol {data[:10]}') 601 | 602 | MAPPINGS = dict(direct=Direct, http=HTTP, httponly=HTTPOnly, httpadmin=HTTPAdmin, ssh=SSH, socks5=Socks5, socks4=Socks4, socks=Socks5, ss=SS, ssr=SSR, redir=Redir, pf=Pf, tunnel=Tunnel, echo=Echo, ws=WS, trojan=Trojan, h2=H2, h3=H3, ssl='', secure='', quic='') 603 | MAPPINGS['in'] = '' 604 | 605 | def get_protos(rawprotos): 606 | protos = [] 607 | for s in rawprotos: 608 | s, _, param = s.partition('{') 609 | param = param[:-1] if param else None 610 | p = MAPPINGS.get(s) 611 | if p is None: 612 | return f'existing protocols: {list(MAPPINGS.keys())}', None 613 | if p and p not in protos: 614 | protos.append(p(param)) 615 | if not protos: 616 | return 'no protocol specified', None 617 | return None, protos 618 | 619 | def sslwrap(reader, writer, sslcontext, server_side=False, server_hostname=None, verbose=None): 620 | if sslcontext is None: 621 | return reader, writer 622 | ssl_reader = asyncio.StreamReader() 623 | class Protocol(asyncio.Protocol): 624 | def data_received(self, data): 625 | ssl_reader.feed_data(data) 626 | def eof_received(self): 627 | ssl_reader.feed_eof() 628 | def connection_lost(self, exc): 629 | ssl_reader.feed_eof() 630 | ssl = asyncio.sslproto.SSLProtocol(asyncio.get_event_loop(), Protocol(), sslcontext, None, server_side, server_hostname, False) 631 | class Transport(asyncio.Transport): 632 | _paused = False 633 | def __init__(self, extra={}): 634 | self._extra = extra 635 | self.closed = False 636 | def write(self, data): 637 | if data and not self.closed: 638 | writer.write(data) 639 | def close(self): 640 | self.closed = True 641 | writer.close() 642 | def _force_close(self, exc): 643 | if not self.closed: 644 | (verbose or print)(f'{exc} from {writer.get_extra_info("peername")[0]}') 645 | ssl._app_transport._closed = True 646 | self.close() 647 | def abort(self): 648 | self.close() 649 | ssl.connection_made(Transport()) 650 | async def channel(): 651 | read_size=65536 652 | buffer=None 653 | if hasattr(ssl,'get_buffer'): 654 | buffer=ssl.get_buffer(read_size) 655 | try: 656 | while not reader.at_eof() and not ssl._app_transport._closed: 657 | data = await reader.read(read_size) 658 | if not data: 659 | break 660 | if buffer!=None: 661 | data_len=len(data) 662 | buffer[:data_len]=data 663 | ssl.buffer_updated(data_len) 664 | else: 665 | ssl.data_received(data) 666 | except Exception: 667 | pass 668 | finally: 669 | ssl.eof_received() 670 | asyncio.ensure_future(channel()) 671 | class Writer(): 672 | def get_extra_info(self, key): 673 | return writer.get_extra_info(key) 674 | def write(self, data): 675 | ssl._app_transport.write(data) 676 | def drain(self): 677 | return writer.drain() 678 | def is_closing(self): 679 | return ssl._app_transport._closed 680 | def close(self): 681 | if not ssl._app_transport._closed: 682 | ssl._app_transport.close() 683 | return ssl_reader, Writer() 684 | -------------------------------------------------------------------------------- /pproxy/server.py: -------------------------------------------------------------------------------- 1 | import argparse, time, re, asyncio, functools, base64, random, urllib.parse, socket, sys 2 | from . import proto 3 | from . import admin 4 | 5 | from .__doc__ import * 6 | 7 | SOCKET_TIMEOUT = 60 8 | UDP_LIMIT = 30 9 | DUMMY = lambda s: s 10 | 11 | def patch_StreamReader(c=asyncio.StreamReader): 12 | c.read_w = lambda self, n: asyncio.wait_for(self.read(n), timeout=SOCKET_TIMEOUT) 13 | c.read_n = lambda self, n: asyncio.wait_for(self.readexactly(n), timeout=SOCKET_TIMEOUT) 14 | c.read_until = lambda self, s: asyncio.wait_for(self.readuntil(s), timeout=SOCKET_TIMEOUT) 15 | c.rollback = lambda self, s: self._buffer.__setitem__(slice(0, 0), s) 16 | def patch_StreamWriter(c=asyncio.StreamWriter): 17 | c.is_closing = lambda self: self._transport.is_closing() # Python 3.6 fix 18 | patch_StreamReader() 19 | patch_StreamWriter() 20 | 21 | class AuthTable(object): 22 | _auth = {} 23 | _user = {} 24 | def __init__(self, remote_ip, authtime): 25 | self.remote_ip = remote_ip 26 | self.authtime = authtime 27 | def authed(self): 28 | if time.time() - self._auth.get(self.remote_ip, 0) <= self.authtime: 29 | return self._user[self.remote_ip] 30 | def set_authed(self, user): 31 | self._auth[self.remote_ip] = time.time() 32 | self._user[self.remote_ip] = user 33 | 34 | async def prepare_ciphers(cipher, reader, writer, bind=None, server_side=True): 35 | if cipher: 36 | cipher.pdecrypt = cipher.pdecrypt2 = cipher.pencrypt = cipher.pencrypt2 = DUMMY 37 | for plugin in cipher.plugins: 38 | if server_side: 39 | await plugin.init_server_data(reader, writer, cipher, bind) 40 | else: 41 | await plugin.init_client_data(reader, writer, cipher) 42 | plugin.add_cipher(cipher) 43 | return cipher(reader, writer, cipher.pdecrypt, cipher.pdecrypt2, cipher.pencrypt, cipher.pencrypt2) 44 | else: 45 | return None, None 46 | 47 | def schedule(rserver, salgorithm, host_name, port): 48 | filter_cond = lambda o: o.alive and o.match_rule(host_name, port) 49 | if salgorithm == 'fa': 50 | return next(filter(filter_cond, rserver), None) 51 | elif salgorithm == 'rr': 52 | for i, roption in enumerate(rserver): 53 | if filter_cond(roption): 54 | rserver.append(rserver.pop(i)) 55 | return roption 56 | elif salgorithm == 'rc': 57 | filters = [i for i in rserver if filter_cond(i)] 58 | return random.choice(filters) if filters else None 59 | elif salgorithm == 'lc': 60 | return min(filter(filter_cond, rserver), default=None, key=lambda i: i.connections) 61 | else: 62 | raise Exception('Unknown scheduling algorithm') #Unreachable 63 | 64 | async def stream_handler(reader, writer, unix, lbind, protos, rserver, cipher, sslserver, debug=0, authtime=86400*30, block=None, salgorithm='fa', verbose=DUMMY, modstat=lambda u,r,h:lambda i:DUMMY, **kwargs): 65 | try: 66 | reader, writer = proto.sslwrap(reader, writer, sslserver, True, None, verbose) 67 | if unix: 68 | remote_ip, server_ip, remote_text = 'local', None, 'unix_local' 69 | else: 70 | peername = writer.get_extra_info('peername') 71 | remote_ip, remote_port, *_ = peername if peername else ('unknow_remote_ip','unknow_remote_port') 72 | server_ip = writer.get_extra_info('sockname')[0] 73 | remote_text = f'{remote_ip}:{remote_port}' 74 | local_addr = None if server_ip in ('127.0.0.1', '::1', None) else (server_ip, 0) 75 | reader_cipher, _ = await prepare_ciphers(cipher, reader, writer, server_side=False) 76 | lproto, user, host_name, port, client_connected = await proto.accept(protos, reader=reader, writer=writer, authtable=AuthTable(remote_ip, authtime), reader_cipher=reader_cipher, sock=writer.get_extra_info('socket'), **kwargs) 77 | if host_name == 'echo': 78 | asyncio.ensure_future(lproto.channel(reader, writer, DUMMY, DUMMY)) 79 | elif host_name == 'empty': 80 | asyncio.ensure_future(lproto.channel(reader, writer, None, DUMMY)) 81 | elif block and block(host_name): 82 | raise Exception('BLOCK ' + host_name) 83 | else: 84 | roption = schedule(rserver, salgorithm, host_name, port) or DIRECT 85 | verbose(f'{lproto.name} {remote_text}{roption.logtext(host_name, port)}') 86 | try: 87 | reader_remote, writer_remote = await roption.open_connection(host_name, port, local_addr, lbind) 88 | except asyncio.TimeoutError: 89 | raise Exception(f'Connection timeout {roption.bind}') 90 | try: 91 | reader_remote, writer_remote = await roption.prepare_connection(reader_remote, writer_remote, host_name, port) 92 | use_http = (await client_connected(writer_remote)) if client_connected else None 93 | except Exception: 94 | writer_remote.close() 95 | raise Exception('Unknown remote protocol') 96 | m = modstat(user, remote_ip, host_name) 97 | lchannel = lproto.http_channel if use_http else lproto.channel 98 | asyncio.ensure_future(lproto.channel(reader_remote, writer, m(2+roption.direct), m(4+roption.direct))) 99 | asyncio.ensure_future(lchannel(reader, writer_remote, m(roption.direct), roption.connection_change)) 100 | except Exception as ex: 101 | if not isinstance(ex, asyncio.TimeoutError) and not str(ex).startswith('Connection closed'): 102 | verbose(f'{str(ex) or "Unsupported protocol"} from {remote_ip}') 103 | try: writer.close() 104 | except Exception: pass 105 | if debug: 106 | raise 107 | 108 | async def datagram_handler(writer, data, addr, protos, urserver, block, cipher, salgorithm, verbose=DUMMY, **kwargs): 109 | try: 110 | remote_ip, remote_port, *_ = addr 111 | remote_text = f'{remote_ip}:{remote_port}' 112 | data = cipher.datagram.decrypt(data) if cipher else data 113 | lproto, user, host_name, port, data = proto.udp_accept(protos, data, sock=writer.get_extra_info('socket'), **kwargs) 114 | if host_name == 'echo': 115 | writer.sendto(data, addr) 116 | elif host_name == 'empty': 117 | pass 118 | elif block and block(host_name): 119 | raise Exception('BLOCK ' + host_name) 120 | else: 121 | roption = schedule(urserver, salgorithm, host_name, port) or DIRECT 122 | verbose(f'UDP {lproto.name} {remote_text}{roption.logtext(host_name, port)}') 123 | data = roption.udp_prepare_connection(host_name, port, data) 124 | def reply(rdata): 125 | rdata = lproto.udp_pack(host_name, port, rdata) 126 | writer.sendto(cipher.datagram.encrypt(rdata) if cipher else rdata, addr) 127 | await roption.udp_open_connection(host_name, port, data, addr, reply) 128 | except Exception as ex: 129 | if not str(ex).startswith('Connection closed'): 130 | verbose(f'{str(ex) or "Unsupported protocol"} from {remote_ip}') 131 | 132 | async def check_server_alive(interval, rserver, verbose): 133 | while True: 134 | await asyncio.sleep(interval) 135 | for remote in rserver: 136 | if type(remote) is ProxyDirect: 137 | continue 138 | try: 139 | _, writer = await remote.open_connection(None, None, None, None, timeout=3) 140 | except asyncio.CancelledError as ex: 141 | return 142 | except Exception as ex: 143 | if remote.alive: 144 | verbose(f'{remote.rproto.name} {remote.bind} -> OFFLINE') 145 | remote.alive = False 146 | continue 147 | if not remote.alive: 148 | verbose(f'{remote.rproto.name} {remote.bind} -> ONLINE') 149 | remote.alive = True 150 | try: 151 | if isinstance(remote, ProxyBackward): 152 | writer.write(b'\x00') 153 | writer.close() 154 | except Exception: 155 | pass 156 | 157 | class ProxyDirect(object): 158 | def __init__(self, lbind=None): 159 | self.bind = 'DIRECT' 160 | self.lbind = lbind 161 | self.unix = False 162 | self.alive = True 163 | self.connections = 0 164 | self.udpmap = {} 165 | @property 166 | def direct(self): 167 | return type(self) is ProxyDirect 168 | def logtext(self, host, port): 169 | return '' if host == 'tunnel' else f' -> {host}:{port}' 170 | def match_rule(self, host, port): 171 | return True 172 | def connection_change(self, delta): 173 | self.connections += delta 174 | def udp_packet_unpack(self, data): 175 | return data 176 | def destination(self, host, port): 177 | return host, port 178 | async def udp_open_connection(self, host, port, data, addr, reply): 179 | class Protocol(asyncio.DatagramProtocol): 180 | def __init__(prot, data): 181 | self.udpmap[addr] = prot 182 | prot.databuf = [data] 183 | prot.transport = None 184 | prot.update = 0 185 | def connection_made(prot, transport): 186 | prot.transport = transport 187 | for data in prot.databuf: 188 | transport.sendto(data) 189 | prot.databuf.clear() 190 | prot.update = time.perf_counter() 191 | def new_data_arrived(prot, data): 192 | if prot.transport: 193 | prot.transport.sendto(data) 194 | else: 195 | prot.databuf.append(data) 196 | prot.update = time.perf_counter() 197 | def datagram_received(prot, data, addr): 198 | data = self.udp_packet_unpack(data) 199 | reply(data) 200 | prot.update = time.perf_counter() 201 | def connection_lost(prot, exc): 202 | self.udpmap.pop(addr, None) 203 | if addr in self.udpmap: 204 | self.udpmap[addr].new_data_arrived(data) 205 | else: 206 | self.connection_change(1) 207 | if len(self.udpmap) > UDP_LIMIT: 208 | min_addr = min(self.udpmap, key=lambda x: self.udpmap[x].update) 209 | prot = self.udpmap.pop(min_addr) 210 | if prot.transport: 211 | prot.transport.close() 212 | prot = lambda: Protocol(data) 213 | remote = self.destination(host, port) 214 | await asyncio.get_event_loop().create_datagram_endpoint(prot, remote_addr=remote) 215 | def udp_prepare_connection(self, host, port, data): 216 | return data 217 | def wait_open_connection(self, host, port, local_addr, family): 218 | return asyncio.open_connection(host=host, port=port, local_addr=local_addr, family=family) 219 | async def open_connection(self, host, port, local_addr, lbind, timeout=SOCKET_TIMEOUT): 220 | try: 221 | local_addr = local_addr if self.lbind == 'in' else (self.lbind, 0) if self.lbind else \ 222 | local_addr if lbind == 'in' else (lbind, 0) if lbind else None 223 | family = 0 if local_addr is None else socket.AF_INET6 if ':' in local_addr[0] else socket.AF_INET 224 | wait = self.wait_open_connection(host, port, local_addr, family) 225 | reader, writer = await asyncio.wait_for(wait, timeout=timeout) 226 | except Exception as ex: 227 | raise 228 | return reader, writer 229 | async def prepare_connection(self, reader_remote, writer_remote, host, port): 230 | return reader_remote, writer_remote 231 | async def tcp_connect(self, host, port, local_addr=None, lbind=None): 232 | reader, writer = await self.open_connection(host, port, local_addr, lbind) 233 | try: 234 | reader, writer = await self.prepare_connection(reader, writer, host, port) 235 | except Exception: 236 | writer.close() 237 | raise 238 | return reader, writer 239 | async def udp_sendto(self, host, port, data, answer_cb, local_addr=None): 240 | if local_addr is None: 241 | local_addr = random.randrange(2**32) 242 | data = self.udp_prepare_connection(host, port, data) 243 | await self.udp_open_connection(host, port, data, local_addr, answer_cb) 244 | DIRECT = ProxyDirect() 245 | 246 | class ProxySimple(ProxyDirect): 247 | def __init__(self, jump, protos, cipher, users, rule, bind, 248 | host_name, port, unix, lbind, sslclient, sslserver): 249 | super().__init__(lbind) 250 | self.protos = protos 251 | self.cipher = cipher 252 | self.users = users 253 | self.rule = compile_rule(rule) if rule else None 254 | self.bind = bind 255 | self.host_name = host_name 256 | self.port = port 257 | self.unix = unix 258 | self.sslclient = sslclient 259 | self.sslserver = sslserver 260 | self.jump = jump 261 | def logtext(self, host, port): 262 | return f' -> {self.rproto.name+("+ssl" if self.sslclient else "")} {self.bind}' + self.jump.logtext(host, port) 263 | def match_rule(self, host, port): 264 | return (self.rule is None) or self.rule(host) or self.rule(str(port)) 265 | @property 266 | def rproto(self): 267 | return self.protos[0] 268 | @property 269 | def auth(self): 270 | return self.users[0] if self.users else b'' 271 | def udp_packet_unpack(self, data): 272 | data = self.cipher.datagram.decrypt(data) if self.cipher else data 273 | return self.jump.udp_packet_unpack(self.rproto.udp_unpack(data)) 274 | def destination(self, host, port): 275 | return self.host_name, self.port 276 | def udp_prepare_connection(self, host, port, data): 277 | data = self.jump.udp_prepare_connection(host, port, data) 278 | whost, wport = self.jump.destination(host, port) 279 | data = self.rproto.udp_connect(rauth=self.auth, host_name=whost, port=wport, data=data) 280 | if self.cipher: 281 | data = self.cipher.datagram.encrypt(data) 282 | return data 283 | def udp_start_server(self, args): 284 | class Protocol(asyncio.DatagramProtocol): 285 | def connection_made(prot, transport): 286 | prot.transport = transport 287 | def datagram_received(prot, data, addr): 288 | asyncio.ensure_future(datagram_handler(prot.transport, data, addr, **vars(self), **args)) 289 | return asyncio.get_event_loop().create_datagram_endpoint(Protocol, local_addr=(self.host_name, self.port)) 290 | def wait_open_connection(self, host, port, local_addr, family): 291 | if self.unix: 292 | return asyncio.open_unix_connection(path=self.bind) 293 | else: 294 | return asyncio.open_connection(host=self.host_name, port=self.port, local_addr=local_addr, family=family) 295 | async def prepare_connection(self, reader_remote, writer_remote, host, port): 296 | reader_remote, writer_remote = proto.sslwrap(reader_remote, writer_remote, self.sslclient, False, self.host_name) 297 | _, writer_cipher_r = await prepare_ciphers(self.cipher, reader_remote, writer_remote, self.bind) 298 | whost, wport = self.jump.destination(host, port) 299 | await self.rproto.connect(reader_remote=reader_remote, writer_remote=writer_remote, rauth=self.auth, host_name=whost, port=wport, writer_cipher_r=writer_cipher_r, myhost=self.host_name, sock=writer_remote.get_extra_info('socket')) 300 | return await self.jump.prepare_connection(reader_remote, writer_remote, host, port) 301 | def start_server(self, args, stream_handler=stream_handler): 302 | handler = functools.partial(stream_handler, **vars(self), **args) 303 | if self.unix: 304 | return asyncio.start_unix_server(handler, path=self.bind) 305 | else: 306 | return asyncio.start_server(handler, host=self.host_name, port=self.port, reuse_port=args.get('ruport')) 307 | 308 | class ProxyH2(ProxySimple): 309 | def __init__(self, sslserver, sslclient, **kw): 310 | super().__init__(sslserver=None, sslclient=None, **kw) 311 | self.handshake = None 312 | self.h2sslserver = sslserver 313 | self.h2sslclient = sslclient 314 | async def handler(self, reader, writer, client_side=True, stream_handler=None, **kw): 315 | import h2.connection, h2.config, h2.events 316 | reader, writer = proto.sslwrap(reader, writer, self.h2sslclient if client_side else self.h2sslserver, not client_side, None) 317 | config = h2.config.H2Configuration(client_side=client_side) 318 | conn = h2.connection.H2Connection(config=config) 319 | streams = {} 320 | conn.initiate_connection() 321 | writer.write(conn.data_to_send()) 322 | while not reader.at_eof() and not writer.is_closing(): 323 | try: 324 | data = await reader.read(65636) 325 | if not data: 326 | break 327 | events = conn.receive_data(data) 328 | except Exception: 329 | pass 330 | writer.write(conn.data_to_send()) 331 | for event in events: 332 | if isinstance(event, h2.events.RequestReceived) and not client_side: 333 | if event.stream_id not in streams: 334 | stream_reader, stream_writer = self.get_stream(conn, writer, event.stream_id) 335 | streams[event.stream_id] = (stream_reader, stream_writer) 336 | asyncio.ensure_future(stream_handler(stream_reader, stream_writer)) 337 | else: 338 | stream_reader, stream_writer = streams[event.stream_id] 339 | stream_writer.headers.set_result(event.headers) 340 | elif isinstance(event, h2.events.SettingsAcknowledged) and client_side: 341 | self.handshake.set_result((conn, streams, writer)) 342 | elif isinstance(event, h2.events.DataReceived): 343 | stream_reader, stream_writer = streams[event.stream_id] 344 | stream_reader.feed_data(event.data) 345 | conn.acknowledge_received_data(len(event.data), event.stream_id) 346 | writer.write(conn.data_to_send()) 347 | elif isinstance(event, h2.events.StreamEnded) or isinstance(event, h2.events.StreamReset): 348 | stream_reader, stream_writer = streams[event.stream_id] 349 | stream_reader.feed_eof() 350 | if not stream_writer.closed: 351 | stream_writer.close() 352 | elif isinstance(event, h2.events.ConnectionTerminated): 353 | break 354 | elif isinstance(event, h2.events.WindowUpdated): 355 | if event.stream_id in streams: 356 | stream_reader, stream_writer = streams[event.stream_id] 357 | stream_writer.window_update() 358 | writer.write(conn.data_to_send()) 359 | writer.close() 360 | def get_stream(self, conn, writer, stream_id): 361 | reader = asyncio.StreamReader() 362 | write_buffer = bytearray() 363 | write_wait = asyncio.Event() 364 | write_full = asyncio.Event() 365 | class StreamWriter(): 366 | def __init__(self): 367 | self.closed = False 368 | self.headers = asyncio.get_event_loop().create_future() 369 | def get_extra_info(self, key): 370 | return writer.get_extra_info(key) 371 | def write(self, data): 372 | write_buffer.extend(data) 373 | write_wait.set() 374 | def drain(self): 375 | writer.write(conn.data_to_send()) 376 | return writer.drain() 377 | def is_closing(self): 378 | return self.closed 379 | def close(self): 380 | self.closed = True 381 | write_wait.set() 382 | def window_update(self): 383 | write_full.set() 384 | def send_headers(self, headers): 385 | conn.send_headers(stream_id, headers) 386 | writer.write(conn.data_to_send()) 387 | stream_writer = StreamWriter() 388 | async def write_job(): 389 | while not stream_writer.closed: 390 | while len(write_buffer) > 0: 391 | while conn.local_flow_control_window(stream_id) <= 0: 392 | write_full.clear() 393 | await write_full.wait() 394 | if stream_writer.closed: 395 | break 396 | chunk_size = min(conn.local_flow_control_window(stream_id), len(write_buffer), conn.max_outbound_frame_size) 397 | conn.send_data(stream_id, write_buffer[:chunk_size]) 398 | writer.write(conn.data_to_send()) 399 | del write_buffer[:chunk_size] 400 | if not stream_writer.closed: 401 | write_wait.clear() 402 | await write_wait.wait() 403 | conn.send_data(stream_id, b'', end_stream=True) 404 | writer.write(conn.data_to_send()) 405 | asyncio.ensure_future(write_job()) 406 | return reader, stream_writer 407 | async def wait_h2_connection(self, local_addr, family): 408 | if self.handshake is not None: 409 | if not self.handshake.done(): 410 | await self.handshake 411 | else: 412 | self.handshake = asyncio.get_event_loop().create_future() 413 | reader, writer = await super().wait_open_connection(None, None, local_addr, family) 414 | asyncio.ensure_future(self.handler(reader, writer)) 415 | await self.handshake 416 | return self.handshake.result() 417 | async def wait_open_connection(self, host, port, local_addr, family): 418 | conn, streams, writer = await self.wait_h2_connection(local_addr, family) 419 | stream_id = conn.get_next_available_stream_id() 420 | conn._begin_new_stream(stream_id, stream_id%2) 421 | stream_reader, stream_writer = self.get_stream(conn, writer, stream_id) 422 | streams[stream_id] = (stream_reader, stream_writer) 423 | return stream_reader, stream_writer 424 | def start_server(self, args, stream_handler=stream_handler): 425 | handler = functools.partial(stream_handler, **vars(self), **args) 426 | return super().start_server(args, functools.partial(self.handler, client_side=False, stream_handler=handler)) 427 | 428 | class ProxyQUIC(ProxySimple): 429 | def __init__(self, quicserver, quicclient, **kw): 430 | super().__init__(**kw) 431 | self.quicserver = quicserver 432 | self.quicclient = quicclient 433 | self.handshake = None 434 | def patch_writer(self, writer): 435 | async def drain(): 436 | writer._transport.protocol.transmit() 437 | #print('stream_id', writer.get_extra_info("stream_id")) 438 | remote_addr = writer._transport.protocol._quic._network_paths[0].addr 439 | writer.get_extra_info = dict(peername=remote_addr, sockname=remote_addr).get 440 | writer.drain = drain 441 | closed = False 442 | writer.is_closing = lambda: closed 443 | def close(): 444 | nonlocal closed 445 | closed = True 446 | try: 447 | writer.write_eof() 448 | except Exception: 449 | pass 450 | writer.close = close 451 | async def wait_quic_connection(self): 452 | if self.handshake is not None: 453 | if not self.handshake.done(): 454 | await self.handshake 455 | else: 456 | self.handshake = asyncio.get_event_loop().create_future() 457 | import aioquic.asyncio, aioquic.quic.events 458 | class Protocol(aioquic.asyncio.QuicConnectionProtocol): 459 | def quic_event_received(s, event): 460 | if isinstance(event, aioquic.quic.events.HandshakeCompleted): 461 | self.handshake.set_result(s) 462 | elif isinstance(event, aioquic.quic.events.ConnectionTerminated): 463 | self.handshake = None 464 | self.quic_egress_acm = None 465 | elif isinstance(event, aioquic.quic.events.StreamDataReceived): 466 | if event.stream_id in self.udpmap: 467 | self.udpmap[event.stream_id](self.udp_packet_unpack(event.data)) 468 | return 469 | super().quic_event_received(event) 470 | self.quic_egress_acm = aioquic.asyncio.connect(self.host_name, self.port, create_protocol=Protocol, configuration=self.quicclient) 471 | conn = await self.quic_egress_acm.__aenter__() 472 | await self.handshake 473 | async def udp_open_connection(self, host, port, data, addr, reply): 474 | await self.wait_quic_connection() 475 | conn = self.handshake.result() 476 | if addr in self.udpmap: 477 | stream_id = self.udpmap[addr] 478 | else: 479 | stream_id = conn._quic.get_next_available_stream_id(False) 480 | self.udpmap[addr] = stream_id 481 | self.udpmap[stream_id] = reply 482 | conn._quic._get_or_create_stream_for_send(stream_id) 483 | conn._quic.send_stream_data(stream_id, data, False) 484 | conn.transmit() 485 | async def wait_open_connection(self, *args): 486 | await self.wait_quic_connection() 487 | conn = self.handshake.result() 488 | stream_id = conn._quic.get_next_available_stream_id(False) 489 | conn._quic._get_or_create_stream_for_send(stream_id) 490 | reader, writer = conn._create_stream(stream_id) 491 | self.patch_writer(writer) 492 | return reader, writer 493 | async def udp_start_server(self, args): 494 | import aioquic.asyncio, aioquic.quic.events 495 | class Protocol(aioquic.asyncio.QuicConnectionProtocol): 496 | def quic_event_received(s, event): 497 | if isinstance(event, aioquic.quic.events.StreamDataReceived): 498 | stream_id = event.stream_id 499 | addr = ('quic '+self.bind, stream_id) 500 | event.sendto = lambda data, addr: (s._quic.send_stream_data(stream_id, data, False), s.transmit()) 501 | event.get_extra_info = {}.get 502 | asyncio.ensure_future(datagram_handler(event, event.data, addr, **vars(self), **args)) 503 | return 504 | super().quic_event_received(event) 505 | return await aioquic.asyncio.serve(self.host_name, self.port, configuration=self.quicserver, create_protocol=Protocol), None 506 | def start_server(self, args, stream_handler=stream_handler): 507 | import aioquic.asyncio 508 | def handler(reader, writer): 509 | self.patch_writer(writer) 510 | asyncio.ensure_future(stream_handler(reader, writer, **vars(self), **args)) 511 | return aioquic.asyncio.serve(self.host_name, self.port, configuration=self.quicserver, stream_handler=handler) 512 | 513 | class ProxyH3(ProxyQUIC): 514 | def get_stream(self, conn, stream_id): 515 | remote_addr = conn._quic._network_paths[0].addr 516 | reader = asyncio.StreamReader() 517 | class StreamWriter(): 518 | def __init__(self): 519 | self.closed = False 520 | self.headers = asyncio.get_event_loop().create_future() 521 | def get_extra_info(self, key): 522 | return dict(peername=remote_addr, sockname=remote_addr).get(key) 523 | def write(self, data): 524 | conn.http.send_data(stream_id, data, False) 525 | conn.transmit() 526 | async def drain(self): 527 | conn.transmit() 528 | def is_closing(self): 529 | return self.closed 530 | def close(self): 531 | if not self.closed: 532 | conn.http.send_data(stream_id, b'', True) 533 | conn.transmit() 534 | conn.close_stream(stream_id) 535 | self.closed = True 536 | def send_headers(self, headers): 537 | conn.http.send_headers(stream_id, [(i.encode(), j.encode()) for i, j in headers]) 538 | conn.transmit() 539 | return reader, StreamWriter() 540 | def get_protocol(self, server_side=False, handler=None): 541 | import aioquic.asyncio, aioquic.quic.events, aioquic.h3.connection, aioquic.h3.events 542 | class Protocol(aioquic.asyncio.QuicConnectionProtocol): 543 | def __init__(s, *args, **kw): 544 | super().__init__(*args, **kw) 545 | s.http = aioquic.h3.connection.H3Connection(s._quic) 546 | s.streams = {} 547 | def quic_event_received(s, event): 548 | if not server_side: 549 | if isinstance(event, aioquic.quic.events.HandshakeCompleted): 550 | self.handshake.set_result(s) 551 | elif isinstance(event, aioquic.quic.events.ConnectionTerminated): 552 | self.handshake = None 553 | self.quic_egress_acm = None 554 | if s.http is not None: 555 | for http_event in s.http.handle_event(event): 556 | s.http_event_received(http_event) 557 | def http_event_received(s, event): 558 | if isinstance(event, aioquic.h3.events.HeadersReceived): 559 | if event.stream_id not in s.streams and server_side: 560 | reader, writer = s.create_stream(event.stream_id) 561 | writer.headers.set_result(event.headers) 562 | asyncio.ensure_future(handler(reader, writer)) 563 | elif isinstance(event, aioquic.h3.events.DataReceived) and event.stream_id in s.streams: 564 | reader, writer = s.streams[event.stream_id] 565 | if event.data: 566 | reader.feed_data(event.data) 567 | if event.stream_ended: 568 | reader.feed_eof() 569 | s.close_stream(event.stream_id) 570 | def create_stream(s, stream_id=None): 571 | if stream_id is None: 572 | stream_id = s._quic.get_next_available_stream_id(False) 573 | s._quic._get_or_create_stream_for_send(stream_id) 574 | reader, writer = self.get_stream(s, stream_id) 575 | s.streams[stream_id] = (reader, writer) 576 | return reader, writer 577 | def close_stream(s, stream_id): 578 | if stream_id in s.streams: 579 | reader, writer = s.streams[stream_id] 580 | if reader.at_eof() and writer.is_closing(): 581 | s.streams.pop(stream_id) 582 | return Protocol 583 | async def wait_h3_connection(self): 584 | if self.handshake is not None: 585 | if not self.handshake.done(): 586 | await self.handshake 587 | else: 588 | import aioquic.asyncio 589 | self.handshake = asyncio.get_event_loop().create_future() 590 | self.quic_egress_acm = aioquic.asyncio.connect(self.host_name, self.port, create_protocol=self.get_protocol(), configuration=self.quicclient) 591 | conn = await self.quic_egress_acm.__aenter__() 592 | await self.handshake 593 | async def wait_open_connection(self, *args): 594 | await self.wait_h3_connection() 595 | return self.handshake.result().create_stream() 596 | def start_server(self, args, stream_handler=stream_handler): 597 | import aioquic.asyncio 598 | return aioquic.asyncio.serve(self.host_name, self.port, configuration=self.quicserver, create_protocol=self.get_protocol(True, functools.partial(stream_handler, **vars(self), **args))) 599 | 600 | class ProxySSH(ProxySimple): 601 | def __init__(self, **kw): 602 | super().__init__(**kw) 603 | self.sshconn = None 604 | def logtext(self, host, port): 605 | return f' -> sshtunnel {self.bind}' + self.jump.logtext(host, port) 606 | def patch_stream(self, ssh_reader, writer, host, port): 607 | reader = asyncio.StreamReader() 608 | async def channel(): 609 | while not ssh_reader.at_eof() and not writer.is_closing(): 610 | buf = await ssh_reader.read(65536) 611 | if not buf: 612 | break 613 | reader.feed_data(buf) 614 | reader.feed_eof() 615 | asyncio.ensure_future(channel()) 616 | remote_addr = ('ssh:'+str(host), port) 617 | writer.get_extra_info = dict(peername=remote_addr, sockname=remote_addr).get 618 | return reader, writer 619 | async def wait_ssh_connection(self, local_addr=None, family=0, tunnel=None): 620 | if self.sshconn is not None and not self.sshconn.cancelled(): 621 | if not self.sshconn.done(): 622 | await self.sshconn 623 | else: 624 | self.sshconn = asyncio.get_event_loop().create_future() 625 | try: 626 | import asyncssh 627 | except Exception: 628 | raise Exception('Missing library: "pip3 install asyncssh"') 629 | username, password = self.auth.decode().split(':', 1) 630 | if password.startswith(':'): 631 | client_keys = [password[1:]] 632 | password = None 633 | else: 634 | client_keys = None 635 | conn = await asyncssh.connect(host=self.host_name, port=self.port, local_addr=local_addr, family=family, x509_trusted_certs=None, known_hosts=None, username=username, password=password, client_keys=client_keys, keepalive_interval=60, tunnel=tunnel) 636 | self.sshconn.set_result(conn) 637 | async def wait_open_connection(self, host, port, local_addr, family, tunnel=None): 638 | try: 639 | await self.wait_ssh_connection(local_addr, family, tunnel) 640 | conn = self.sshconn.result() 641 | if isinstance(self.jump, ProxySSH): 642 | reader, writer = await self.jump.wait_open_connection(host, port, None, None, conn) 643 | else: 644 | host, port = self.jump.destination(host, port) 645 | if self.jump.unix: 646 | reader, writer = await conn.open_unix_connection(self.jump.bind) 647 | else: 648 | reader, writer = await conn.open_connection(host, port) 649 | reader, writer = self.patch_stream(reader, writer, host, port) 650 | return reader, writer 651 | except Exception as ex: 652 | if not self.sshconn.done(): 653 | self.sshconn.set_exception(ex) 654 | self.sshconn = None 655 | raise 656 | async def start_server(self, args, stream_handler=stream_handler, tunnel=None): 657 | if type(self.jump) is ProxyDirect: 658 | raise Exception('ssh server mode unsupported') 659 | await self.wait_ssh_connection(tunnel=tunnel) 660 | conn = self.sshconn.result() 661 | if isinstance(self.jump, ProxySSH): 662 | return await self.jump.start_server(args, stream_handler, conn) 663 | else: 664 | def handler(host, port): 665 | def handler_stream(reader, writer): 666 | reader, writer = self.patch_stream(reader, writer, host, port) 667 | return stream_handler(reader, writer, **vars(self.jump), **args) 668 | return handler_stream 669 | if self.jump.unix: 670 | return await conn.start_unix_server(handler, self.jump.bind) 671 | else: 672 | return await conn.start_server(handler, self.jump.host_name, self.jump.port) 673 | 674 | class ProxyBackward(ProxySimple): 675 | def __init__(self, backward, backward_num, **kw): 676 | super().__init__(**kw) 677 | self.backward = backward 678 | self.server = backward 679 | while type(self.server.jump) != ProxyDirect: 680 | self.server = self.server.jump 681 | self.backward_num = backward_num 682 | self.closed = False 683 | self.writers = set() 684 | self.conn = asyncio.Queue() 685 | async def wait_open_connection(self, *args): 686 | while True: 687 | reader, writer = await self.conn.get() 688 | if not reader.at_eof() and not writer.is_closing(): 689 | return reader, writer 690 | def close(self): 691 | self.closed = True 692 | for writer in self.writers: 693 | try: 694 | self.writer.close() 695 | except Exception: 696 | pass 697 | async def start_server(self, args, stream_handler=stream_handler): 698 | handler = functools.partial(stream_handler, **vars(self.server), **args) 699 | for _ in range(self.backward_num): 700 | asyncio.ensure_future(self.start_server_run(handler)) 701 | return self 702 | async def start_server_run(self, handler): 703 | errwait = 0 704 | while not self.closed: 705 | wait = self.backward.open_connection(self.host_name, self.port, self.lbind, None) 706 | try: 707 | reader, writer = await asyncio.wait_for(wait, timeout=SOCKET_TIMEOUT) 708 | if self.closed: 709 | writer.close() 710 | break 711 | if isinstance(self.server, ProxyQUIC): 712 | writer.write(b'\x01') 713 | writer.write(self.server.auth) 714 | self.writers.add(writer) 715 | try: 716 | data = await reader.read_n(1) 717 | except asyncio.TimeoutError: 718 | data = None 719 | if data and data[0] != 0: 720 | reader.rollback(data) 721 | asyncio.ensure_future(handler(reader, writer)) 722 | else: 723 | writer.close() 724 | errwait = 0 725 | self.writers.discard(writer) 726 | writer = None 727 | except Exception as ex: 728 | try: 729 | writer.close() 730 | except Exception: 731 | pass 732 | if not self.closed: 733 | await asyncio.sleep(errwait) 734 | errwait = min(errwait*1.3 + 0.1, 30) 735 | def start_backward_client(self, args): 736 | async def handler(reader, writer, **kw): 737 | auth = self.server.auth 738 | if isinstance(self.server, ProxyQUIC): 739 | auth = b'\x01'+auth 740 | if auth: 741 | try: 742 | assert auth == (await reader.read_n(len(auth))) 743 | except Exception: 744 | return 745 | await self.conn.put((reader, writer)) 746 | return self.backward.start_server(args, handler) 747 | 748 | 749 | def compile_rule(filename): 750 | if filename.startswith("{") and filename.endswith("}"): 751 | return re.compile(filename[1:-1]).match 752 | with open(filename) as f: 753 | return re.compile('(:?'+''.join('|'.join(i.strip() for i in f if i.strip() and not i.startswith('#')))+')$').match 754 | 755 | def proxies_by_uri(uri_jumps): 756 | jump = DIRECT 757 | for uri in reversed(uri_jumps.split('__')): 758 | jump = proxy_by_uri(uri, jump) 759 | return jump 760 | 761 | sslcontexts = [] 762 | 763 | def proxy_by_uri(uri, jump): 764 | scheme, _, uri = uri.partition('://') 765 | url = urllib.parse.urlparse('s://'+uri) 766 | rawprotos = [i.lower() for i in scheme.split('+')] 767 | err_str, protos = proto.get_protos(rawprotos) 768 | protonames = [i.name for i in protos] 769 | if err_str: 770 | raise argparse.ArgumentTypeError(err_str) 771 | if 'ssl' in rawprotos or 'secure' in rawprotos: 772 | import ssl 773 | sslserver = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 774 | sslclient = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) 775 | if 'ssl' in rawprotos: 776 | sslclient.check_hostname = False 777 | sslclient.verify_mode = ssl.CERT_NONE 778 | sslcontexts.append(sslserver) 779 | sslcontexts.append(sslclient) 780 | else: 781 | sslserver = sslclient = None 782 | if 'quic' in rawprotos or 'h3' in protonames: 783 | try: 784 | import ssl, aioquic.quic.configuration 785 | except Exception: 786 | raise Exception('Missing library: "pip3 install aioquic"') 787 | quicserver = aioquic.quic.configuration.QuicConfiguration(is_client=False, max_stream_data=2**60, max_data=2**60, idle_timeout=SOCKET_TIMEOUT) 788 | quicclient = aioquic.quic.configuration.QuicConfiguration(max_stream_data=2**60, max_data=2**60, idle_timeout=SOCKET_TIMEOUT*5) 789 | quicclient.verify_mode = ssl.CERT_NONE 790 | sslcontexts.append(quicserver) 791 | sslcontexts.append(quicclient) 792 | if 'h2' in rawprotos: 793 | try: 794 | import h2 795 | except Exception: 796 | raise Exception('Missing library: "pip3 install h2"') 797 | urlpath, _, plugins = url.path.partition(',') 798 | urlpath, _, lbind = urlpath.partition('@') 799 | plugins = plugins.split(',') if plugins else None 800 | cipher, _, loc = url.netloc.rpartition('@') 801 | if cipher: 802 | from .cipher import get_cipher 803 | if ':' not in cipher: 804 | try: 805 | cipher = base64.b64decode(cipher).decode() 806 | except Exception: 807 | pass 808 | if ':' not in cipher: 809 | raise argparse.ArgumentTypeError('userinfo must be "cipher:key"') 810 | err_str, cipher = get_cipher(cipher) 811 | if err_str: 812 | raise argparse.ArgumentTypeError(err_str) 813 | if plugins: 814 | from .plugin import get_plugin 815 | for name in plugins: 816 | if not name: continue 817 | err_str, plugin = get_plugin(name) 818 | if err_str: 819 | raise argparse.ArgumentTypeError(err_str) 820 | cipher.plugins.append(plugin) 821 | if loc: 822 | host_name, port = proto.netloc_split(loc, default_port=22 if 'ssh' in rawprotos else 8080) 823 | else: 824 | host_name = port = None 825 | if url.fragment.startswith('#'): 826 | with open(url.fragment[1:]) as f: 827 | auth = f.read().rstrip().encode() 828 | else: 829 | auth = url.fragment.encode() 830 | users = [i.rstrip() for i in auth.split(b'\n')] if auth else None 831 | if 'direct' in protonames: 832 | return ProxyDirect(lbind=lbind) 833 | else: 834 | params = dict(jump=jump, protos=protos, cipher=cipher, users=users, rule=url.query, bind=loc or urlpath, 835 | host_name=host_name, port=port, unix=not loc, lbind=lbind, sslclient=sslclient, sslserver=sslserver) 836 | if 'quic' in rawprotos: 837 | proxy = ProxyQUIC(quicserver, quicclient, **params) 838 | elif 'h3' in protonames: 839 | proxy = ProxyH3(quicserver, quicclient, **params) 840 | elif 'h2' in protonames: 841 | proxy = ProxyH2(**params) 842 | elif 'ssh' in protonames: 843 | proxy = ProxySSH(**params) 844 | else: 845 | proxy = ProxySimple(**params) 846 | if 'in' in rawprotos: 847 | proxy = ProxyBackward(proxy, rawprotos.count('in'), **params) 848 | return proxy 849 | 850 | async def test_url(url, rserver): 851 | url = urllib.parse.urlparse(url) 852 | assert url.scheme in ('http', 'https'), f'Unknown scheme {url.scheme}' 853 | host_name, port = proto.netloc_split(url.netloc, default_port = 80 if url.scheme=='http' else 443) 854 | initbuf = f'GET {url.path or "/"} HTTP/1.1\r\nHost: {host_name}\r\nUser-Agent: pproxy-{__version__}\r\nAccept: */*\r\nConnection: close\r\n\r\n'.encode() 855 | for roption in rserver: 856 | print(f'============ {roption.bind} ============') 857 | try: 858 | reader, writer = await roption.open_connection(host_name, port, None, None) 859 | except asyncio.TimeoutError: 860 | raise Exception(f'Connection timeout {rserver}') 861 | try: 862 | reader, writer = await roption.prepare_connection(reader, writer, host_name, port) 863 | except Exception: 864 | writer.close() 865 | raise Exception('Unknown remote protocol') 866 | if url.scheme == 'https': 867 | import ssl 868 | sslclient = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) 869 | sslclient.check_hostname = False 870 | sslclient.verify_mode = ssl.CERT_NONE 871 | reader, writer = proto.sslwrap(reader, writer, sslclient, False, host_name) 872 | writer.write(initbuf) 873 | headers = await reader.read_until(b'\r\n\r\n') 874 | print(headers.decode()[:-4]) 875 | print(f'--------------------------------') 876 | body = bytearray() 877 | while not reader.at_eof(): 878 | s = await reader.read(65536) 879 | if not s: 880 | break 881 | body.extend(s) 882 | print(body.decode('utf8', 'ignore')) 883 | print(f'============ success ============') 884 | 885 | def print_server_started(option, server, print_fn): 886 | for s in server.sockets: 887 | # https://github.com/MagicStack/uvloop/blob/master/uvloop/pseudosock.pyx 888 | laddr = s.getsockname() # tuple size varies with protocol family 889 | h = laddr[0] 890 | p = laddr[1] 891 | f = str(s.family) 892 | ipversion = "ipv4" if f == "AddressFamily.AF_INET" else ("ipv6" if f == "AddressFamily.AF_INET6" else "ipv?") # TODO better 893 | bind = ipversion+' '+h+':'+str(p) 894 | print_fn(option, bind) 895 | 896 | def main(args = None): 897 | origin_argv = sys.argv[1:] if args is None else args 898 | 899 | parser = argparse.ArgumentParser(description=__description__+'\nSupported protocols: http,socks4,socks5,shadowsocks,shadowsocksr,redirect,pf,tunnel', epilog=f'Online help: <{__url__}>') 900 | parser.add_argument('-l', dest='listen', default=[], action='append', type=proxies_by_uri, help='tcp server uri (default: http+socks4+socks5://:8080/)') 901 | parser.add_argument('-r', dest='rserver', default=[], action='append', type=proxies_by_uri, help='tcp remote server uri (default: direct)') 902 | parser.add_argument('-ul', dest='ulisten', default=[], action='append', type=proxies_by_uri, help='udp server setting uri (default: none)') 903 | parser.add_argument('-ur', dest='urserver', default=[], action='append', type=proxies_by_uri, help='udp remote server uri (default: direct)') 904 | parser.add_argument('-b', dest='block', type=compile_rule, help='block regex rules') 905 | parser.add_argument('-a', dest='alived', default=0, type=int, help='interval to check remote alive (default: no check)') 906 | parser.add_argument('-s', dest='salgorithm', default='fa', choices=('fa', 'rr', 'rc', 'lc'), help='scheduling algorithm (default: first_available)') 907 | parser.add_argument('-d', dest='debug', action='count', help='turn on debug to see tracebacks (default: no debug)') 908 | parser.add_argument('-v', dest='v', action='count', help='print verbose output') 909 | parser.add_argument('--ssl', dest='sslfile', help='certfile[,keyfile] if server listen in ssl mode') 910 | parser.add_argument('--pac', help='http PAC path') 911 | parser.add_argument('--get', dest='gets', default=[], action='append', help='http custom {path,file}') 912 | parser.add_argument('--auth', dest='authtime', type=int, default=86400*30, help='re-auth time interval for same ip (default: 86400*30)') 913 | parser.add_argument('--sys', action='store_true', help='change system proxy setting (mac, windows)') 914 | parser.add_argument('--reuse', dest='ruport', action='store_true', help='set SO_REUSEPORT (Linux only)') 915 | parser.add_argument('--daemon', dest='daemon', action='store_true', help='run as a daemon (Linux only)') 916 | parser.add_argument('--test', help='test this url for all remote proxies and exit') 917 | parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}') 918 | args = parser.parse_args(args) 919 | if args.sslfile: 920 | sslfile = args.sslfile.split(',') 921 | for context in sslcontexts: 922 | context.load_cert_chain(*sslfile) 923 | elif any(map(lambda o: o.sslclient or isinstance(o, ProxyQUIC), args.listen+args.ulisten)): 924 | print('You must specify --ssl to listen in ssl mode') 925 | return 926 | if args.test: 927 | asyncio.get_event_loop().run_until_complete(test_url(args.test, args.rserver)) 928 | return 929 | if not args.listen and not args.ulisten: 930 | args.listen.append(proxies_by_uri('http+socks4+socks5://:8080/')) 931 | args.httpget = {} 932 | if args.pac: 933 | pactext = 'function FindProxyForURL(u,h){' + (f'var b=/^(:?{args.block.__self__.pattern})$/i;if(b.test(h))return "";' if args.block else '') 934 | for i, option in enumerate(args.rserver): 935 | pactext += (f'var m{i}=/^(:?{option.rule.__self__.pattern})$/i;if(m{i}.test(h))' if option.rule else '') + 'return "PROXY %(host)s";' 936 | args.httpget[args.pac] = pactext+'return "DIRECT";}' 937 | args.httpget[args.pac+'/all'] = 'function FindProxyForURL(u,h){return "PROXY %(host)s";}' 938 | args.httpget[args.pac+'/none'] = 'function FindProxyForURL(u,h){return "DIRECT";}' 939 | for gets in args.gets: 940 | path, filename = gets.split(',', 1) 941 | with open(filename, 'rb') as f: 942 | args.httpget[path] = f.read() 943 | if args.daemon: 944 | try: 945 | __import__('daemon').DaemonContext().open() 946 | except ModuleNotFoundError: 947 | print("Missing library: pip3 install python-daemon") 948 | return 949 | # Try to use uvloop instead of the default event loop 950 | try: 951 | __import__('uvloop').install() 952 | print('Using uvloop') 953 | except ModuleNotFoundError: 954 | pass 955 | loop = asyncio.get_event_loop() 956 | if args.v: 957 | from . import verbose 958 | verbose.setup(loop, args) 959 | servers = [] 960 | admin.config.update({'argv': origin_argv, 'servers': servers, 'args': args, 'loop': loop}) 961 | def print_fn(option, bind=None): 962 | print('Serving on', (bind or option.bind), 'by', ",".join(i.name for i in option.protos) + ('(SSL)' if option.sslclient else ''), '({}{})'.format(option.cipher.name, ' '+','.join(i.name() for i in option.cipher.plugins) if option.cipher and option.cipher.plugins else '') if option.cipher else '') 963 | for option in args.listen: 964 | try: 965 | server = loop.run_until_complete(option.start_server(vars(args))) 966 | print_server_started(option, server, print_fn) 967 | servers.append(server) 968 | except Exception as ex: 969 | print_fn(option) 970 | print('Start server failed.\n\t==>', ex) 971 | def print_fn(option, bind=None): 972 | print('Serving on UDP', (bind or option.bind), 'by', ",".join(i.name for i in option.protos), f'({option.cipher.name})' if option.cipher else '') 973 | for option in args.ulisten: 974 | try: 975 | server, protocol = loop.run_until_complete(option.udp_start_server(vars(args))) 976 | print_server_started(option, server, print_fn) 977 | servers.append(server) 978 | except Exception as ex: 979 | print_fn(option) 980 | print('Start server failed.\n\t==>', ex) 981 | def print_fn(option, bind=None): 982 | print('Serving on', (bind or option.bind), 'backward by', ",".join(i.name for i in option.protos) + ('(SSL)' if option.sslclient else ''), '({}{})'.format(option.cipher.name, ' '+','.join(i.name() for i in option.cipher.plugins) if option.cipher and option.cipher.plugins else '') if option.cipher else '') 983 | for option in args.rserver: 984 | if isinstance(option, ProxyBackward): 985 | try: 986 | server = loop.run_until_complete(option.start_backward_client(vars(args))) 987 | print_server_started(option, server, print_fn) 988 | servers.append(server) 989 | except Exception as ex: 990 | print_fn(option) 991 | print('Start server failed.\n\t==>', ex) 992 | if servers: 993 | if args.sys: 994 | from . import sysproxy 995 | args.sys = sysproxy.setup(args) 996 | if args.alived > 0 and args.rserver: 997 | asyncio.ensure_future(check_server_alive(args.alived, args.rserver, args.verbose if args.v else DUMMY)) 998 | try: 999 | loop.run_forever() 1000 | except KeyboardInterrupt: 1001 | print('exit') 1002 | if args.sys: 1003 | args.sys.clear() 1004 | for task in asyncio.all_tasks(loop) if hasattr(asyncio, 'all_tasks') else asyncio.Task.all_tasks(): 1005 | task.cancel() 1006 | for server in servers: 1007 | server.close() 1008 | for server in servers: 1009 | if hasattr(server, 'wait_closed'): 1010 | loop.run_until_complete(server.wait_closed()) 1011 | loop.run_until_complete(loop.shutdown_asyncgens()) 1012 | if admin.config.get('reload', False): 1013 | admin.config['reload'] = False 1014 | main(admin.config['argv']) 1015 | loop.close() 1016 | 1017 | if __name__ == '__main__': 1018 | main() 1019 | -------------------------------------------------------------------------------- /pproxy/sysproxy.py: -------------------------------------------------------------------------------- 1 | import os, sys, subprocess, struct 2 | 3 | class MacSetting(object): 4 | def __init__(self, args): 5 | self.device = None 6 | self.listen = None 7 | self.modes = None 8 | self.mode_name = None 9 | for option in args.listen: 10 | protos = [x.name for x in option.protos] 11 | if option.unix or 'ssl' in protos or 'secure' in protos: 12 | continue 13 | if 'socks5' in protos: 14 | self.modes = ['setsocksfirewallproxy'] 15 | self.mode_name = 'socks5' 16 | self.listen = option 17 | break 18 | if 'http' in protos: 19 | self.modes = ['setwebproxy', 'setsecurewebproxy'] 20 | self.mode_name = 'http' 21 | self.listen = option 22 | break 23 | if self.listen is None: 24 | print('No server listen on localhost by http/socks5') 25 | ret = subprocess.check_output(['/usr/sbin/networksetup', '-listnetworkserviceorder']).decode() 26 | en0 = next(filter(lambda x: 'Device: en0' in x, ret.split('\n\n')), None) 27 | if en0 is None: 28 | print('Cannot find en0 device name!\n\nInfo:\n\n'+ret) 29 | return 30 | line = next(filter(lambda x: x.startswith('('), en0.split('\n')), None) 31 | if line is None: 32 | print('Cannot find en0 device name!\n\nInfo:\n\n'+ret) 33 | return 34 | self.device = line[3:].strip() 35 | for mode in self.modes: 36 | subprocess.check_call(['/usr/sbin/networksetup', mode, self.device, 'localhost', str(self.listen.port), 'off']) 37 | print(f'System proxy setting -> {self.mode_name} localhost:{self.listen.port}') 38 | def clear(self): 39 | if self.device is None: 40 | return 41 | for mode in self.modes: 42 | subprocess.check_call(['/usr/sbin/networksetup', mode+'state', self.device, 'off']) 43 | print('System proxy setting -> off') 44 | 45 | class WindowsSetting(object): 46 | KEY = r'Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections' 47 | SUBKEY = 'DefaultConnectionSettings' 48 | def __init__(self, args): 49 | self.listen = None 50 | for option in args.listen: 51 | protos = [x.name for x in option.protos] 52 | if option.unix or 'ssl' in protos or 'secure' in protos: 53 | continue 54 | if 'http' in protos: 55 | self.listen = option 56 | break 57 | if self.listen is None: 58 | print('No server listen on localhost by http') 59 | import winreg 60 | key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, self.KEY, 0, winreg.KEY_ALL_ACCESS) 61 | value, regtype = winreg.QueryValueEx(key, self.SUBKEY) 62 | assert regtype == winreg.REG_BINARY 63 | server = f'localhost:{self.listen.port}'.encode() 64 | bypass = ''.encode() 65 | counter = int.from_bytes(value[4:8], 'little') + 1 66 | value = value[:4] + struct.pack('=2**30 else f'{i/2**20:.1f}M' if i>=2**20 else f'{i/1024:.1f}K' 4 | 5 | def all_stat_other(stats): 6 | cmd = sys.stdin.readline() 7 | all_stat(stats) 8 | 9 | def all_stat(stats): 10 | if len(stats) <= 1: 11 | print('no traffic') 12 | return 13 | print('='*70) 14 | hstat = {} 15 | for remote_ip, v in stats.items(): 16 | if remote_ip == 0: continue 17 | stat = [0]*6 18 | for host_name, v2 in v.items(): 19 | for h in (stat, hstat.setdefault(host_name, [0]*6)): 20 | for i in range(6): 21 | h[i] += v2[i] 22 | stat = [b2s(i) for i in stat[:4]] + stat[4:] 23 | print(remote_ip, '\tDIRECT: {5} ({1},{3}) PROXY: {4} ({0},{2})'.format(*stat)) 24 | print(' '*3+'-'*64) 25 | hstat = sorted(hstat.items(), key=lambda x: sum(x[1]), reverse=True)[:15] 26 | hlen = max(map(lambda x: len(x[0]), hstat)) if hstat else 0 27 | for host_name, stat in hstat: 28 | stat, conn = (b2s(stat[0]+stat[1]), b2s(stat[2]+stat[3])), stat[4]+stat[5] 29 | print(host_name.ljust(hlen+5), '{0} / {1}'.format(*stat), '/ {}'.format(conn) if conn else '') 30 | print('='*70) 31 | 32 | async def realtime_stat(stats): 33 | history = [(stats[:4], time.perf_counter())] 34 | while True: 35 | await asyncio.sleep(1) 36 | history.append((stats[:4], time.perf_counter())) 37 | i0, t0, i1, t1 = history[0][0], history[0][1], history[-1][0], history[-1][1] 38 | stat = [b2s((i1[i]-i0[i])/(t1-t0))+'/s' for i in range(4)] + stats[4:] 39 | sys.stdout.write('DIRECT: {5} ({1},{3}) PROXY: {4} ({0},{2})\x1b[0K\r'.format(*stat)) 40 | sys.stdout.flush() 41 | if len(history) >= 10: 42 | del history[:1] 43 | 44 | def setup(loop, args): 45 | def verbose(s): 46 | if args.v >= 2: 47 | sys.stdout.write('\x1b[32m'+time.strftime('%Y-%m-%d %H:%M:%S')+'\x1b[m ') 48 | sys.stdout.write(s+'\x1b[0K\n') 49 | else: 50 | sys.stdout.write(s+'\n') 51 | sys.stdout.flush() 52 | args.verbose = verbose 53 | args.stats = {0: [0]*6} 54 | def modstat(user, remote_ip, host_name, stats=args.stats): 55 | u = user.decode().split(':')[0]+':' if isinstance(user, (bytes,bytearray)) else '' 56 | host_name_2 = '.'.join(host_name.split('.')[-3 if host_name.endswith('.com.cn') else -2:]) if host_name.split('.')[-1].isalpha() else host_name 57 | tostat = (stats[0], stats.setdefault(u+remote_ip, {}).setdefault(host_name_2, [0]*6)) 58 | return lambda i: lambda s: [st.__setitem__(i, st[i] + s) for st in tostat] 59 | args.modstat = modstat 60 | def win_readline(handler): 61 | while True: 62 | line = sys.stdin.readline() 63 | handler() 64 | if args.v >= 2: 65 | asyncio.ensure_future(realtime_stat(args.stats[0])) 66 | if sys.platform != 'win32': 67 | loop.add_reader(sys.stdin, functools.partial(all_stat_other, args.stats)) 68 | else: 69 | loop.run_in_executor(None, win_readline, functools.partial(all_stat, args.stats)) 70 | 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import os, re 3 | 4 | def read(*names, **kwargs): 5 | with open(os.path.join(os.path.dirname(__file__), *names), encoding='utf8') as fp: 6 | return fp.read() 7 | 8 | def find_value(name): 9 | data_file = read('pproxy', '__doc__.py') 10 | data_match = re.search(r"^__%s__ += ['\"]([^'\"]*)['\"]" % name, data_file, re.M) 11 | if data_match: 12 | return data_match.group(1) 13 | raise RuntimeError(f"Unable to find '{name}' string.") 14 | 15 | setup( 16 | name = find_value('title'), 17 | use_scm_version = True, 18 | description = find_value('description'), 19 | long_description = read('README.rst'), 20 | url = find_value('url'), 21 | author = find_value('author'), 22 | author_email = find_value('email'), 23 | license = find_value('license'), 24 | python_requires = '>=3.6', 25 | keywords = find_value('keywords'), 26 | packages = ['pproxy'], 27 | classifiers = [ 28 | 'Development Status :: 5 - Production/Stable', 29 | 'Environment :: Console', 30 | 'Intended Audience :: Developers', 31 | 'Natural Language :: English', 32 | 'Topic :: Software Development :: Build Tools', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Programming Language :: Python :: 3', 35 | 'Programming Language :: Python :: 3.6', 36 | 'Programming Language :: Python :: 3.7', 37 | ], 38 | extras_require = { 39 | 'accelerated': [ 40 | 'pycryptodome >= 3.7.2', 41 | 'uvloop >= 0.13.0' 42 | ], 43 | 'sshtunnel': [ 44 | 'asyncssh >= 2.5.0', 45 | ], 46 | 'quic': [ 47 | 'aioquic >= 0.9.7', 48 | ], 49 | 'daemon': [ 50 | 'python-daemon >= 2.2.3', 51 | ], 52 | }, 53 | install_requires = [], 54 | entry_points = { 55 | 'console_scripts': [ 56 | 'pproxy = pproxy.server:main', 57 | ], 58 | }, 59 | ) 60 | -------------------------------------------------------------------------------- /tests/api_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pproxy 3 | 4 | async def test_tcp(): 5 | conn = pproxy.Connection('ss://chacha20:123@127.0.0.1:12345') 6 | reader, writer = await conn.tcp_connect('google.com', 80) 7 | writer.write(b'GET / HTTP/1.1\r\n\r\n') 8 | data = await reader.read(1024*16) 9 | print(data.decode()) 10 | 11 | async def test_udp(): 12 | conn = pproxy.Connection('ss://chacha20:123@127.0.0.1:12345') 13 | answer = asyncio.Future() 14 | await conn.udp_sendto('8.8.8.8', 53, b'hello', answer.set_result) 15 | await answer 16 | print(answer.result()) 17 | 18 | asyncio.run(test_tcp()) 19 | asyncio.run(test_udp()) 20 | -------------------------------------------------------------------------------- /tests/api_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pproxy 3 | 4 | server = pproxy.Server('ss://0.0.0.0:1234') 5 | remote = pproxy.Connection('ss://1.2.3.4:5678') 6 | args = dict( rserver = [remote], 7 | verbose = print ) 8 | 9 | loop = asyncio.get_event_loop() 10 | handler = loop.run_until_complete(server.start_server(args)) 11 | try: 12 | loop.run_forever() 13 | except KeyboardInterrupt: 14 | print('exit!') 15 | 16 | handler.close() 17 | loop.run_until_complete(handler.wait_closed()) 18 | loop.run_until_complete(loop.shutdown_asyncgens()) 19 | loop.close() 20 | -------------------------------------------------------------------------------- /tests/cipher_compare.py: -------------------------------------------------------------------------------- 1 | import os, time 2 | from pproxy.cipher import AES_256_CFB_Cipher as A 3 | from pproxy.cipherpy import AES_256_CFB_Cipher as B 4 | from pproxy.cipher import ChaCha20_Cipher as C 5 | from pproxy.cipherpy import ChaCha20_Cipher as D 6 | from pproxy.cipherpy import Camellia_256_CFB_Cipher as E 7 | 8 | TO_TEST = (A, B, C, D, E) 9 | 10 | for X in TO_TEST: 11 | t = time.perf_counter() 12 | for i in range(10): 13 | c = X(os.urandom(X.KEY_LENGTH)) 14 | c.setup_iv() 15 | for j in range(100): 16 | c.encrypt(os.urandom(1024)) 17 | print(time.perf_counter()-t) 18 | -------------------------------------------------------------------------------- /tests/cipher_speed.py: -------------------------------------------------------------------------------- 1 | import os, time, sys, os 2 | from pproxy.cipher import MAP 3 | from pproxy.cipherpy import MAP as MAP_PY 4 | 5 | def test_cipher(A, size=32*1024, repeat=128): 6 | for i in range(repeat): 7 | key = os.urandom(A.KEY_LENGTH) 8 | iv = os.urandom(A.IV_LENGTH) 9 | a = A(key) 10 | a.setup_iv(iv) 11 | s = os.urandom(size) 12 | s2 = a.encrypt(s) 13 | a = A(key, True) 14 | a.setup_iv(iv) 15 | s4 = a.decrypt(s2) 16 | assert s == s4 17 | 18 | cipher = sys.argv[1] if len(sys.argv) > 1 else None 19 | 20 | if cipher and cipher.endswith('-py'): 21 | A = MAP_PY.get(cipher[:-3]) 22 | else: 23 | A = MAP.get(cipher) 24 | if A: 25 | t = time.perf_counter() 26 | test_cipher(A) 27 | print(cipher, time.perf_counter()-t) 28 | else: 29 | print('unknown cipher', cipher) 30 | 31 | -------------------------------------------------------------------------------- /tests/cipher_verify.py: -------------------------------------------------------------------------------- 1 | import os, time, sys, pickle, os 2 | from pproxy.cipher import MAP 3 | from pproxy.cipherpy import MAP as MAP_PY 4 | 5 | def test_both_cipher(A, B, size=4*1024, repeat=16): 6 | print('Testing', B.__name__, '...') 7 | t1 = t2 = 0 8 | for i in range(repeat): 9 | assert A.KEY_LENGTH == B.KEY_LENGTH and A.IV_LENGTH == B.IV_LENGTH 10 | key = os.urandom(A.KEY_LENGTH) 11 | iv = os.urandom(A.IV_LENGTH) 12 | t = time.perf_counter() 13 | a = A(key) 14 | a.setup_iv(iv) 15 | t1 += time.perf_counter() - t 16 | t = time.perf_counter() 17 | b = B(key) 18 | b.setup_iv(iv) 19 | t2 += time.perf_counter() - t 20 | s = os.urandom(size) 21 | t = time.perf_counter() 22 | s2 = a.encrypt(s) 23 | t1 += time.perf_counter() - t 24 | t = time.perf_counter() 25 | s3 = b.encrypt(s) 26 | t2 += time.perf_counter() - t 27 | assert s2 == s3 28 | 29 | t = time.perf_counter() 30 | a = A(key, True) 31 | a.setup_iv(iv) 32 | t1 += time.perf_counter() - t 33 | t = time.perf_counter() 34 | b = B(key, True) 35 | b.setup_iv(iv) 36 | t2 += time.perf_counter() - t 37 | t = time.perf_counter() 38 | s4 = a.decrypt(s2) 39 | t1 += time.perf_counter() - t 40 | t = time.perf_counter() 41 | s5 = b.decrypt(s2) 42 | t2 += time.perf_counter() - t 43 | assert s4 == s5 == s 44 | 45 | print('Passed', t1, t2) 46 | 47 | def test_cipher(A, data, size=4*1024, repeat=16): 48 | if A.__name__ not in data: 49 | if input('Correct now? (Y/n)').upper() != 'Y': 50 | return 51 | d = [] 52 | for i in range(repeat): 53 | key = os.urandom(A.KEY_LENGTH) 54 | iv = os.urandom(A.IV_LENGTH) 55 | a = A(key) 56 | a.setup_iv(iv) 57 | s = os.urandom(size) 58 | s2 = a.encrypt(s) 59 | a = A(key, True) 60 | a.setup_iv(iv) 61 | s4 = a.decrypt(s2) 62 | assert s == s4 63 | d.append((key, iv, s, s2)) 64 | data[A.__name__] = d 65 | print('Saved correct data') 66 | else: 67 | t = time.perf_counter() 68 | print('Testing', A.__name__, '...') 69 | for key, iv, s, s2 in data[A.__name__]: 70 | a = A(key) 71 | a.setup_iv(iv) 72 | s3 = a.encrypt(s) 73 | assert s2 == s3 74 | a = A(key, True) 75 | a.setup_iv(iv) 76 | s4 = a.decrypt(s2) 77 | assert s == s4 78 | print('Passed', time.perf_counter()-t) 79 | 80 | 81 | cipher = sys.argv[1] if len(sys.argv) > 1 else None 82 | data = pickle.load(open('.cipherdata', 'rb')) if os.path.exists('.cipherdata') else {} 83 | 84 | if cipher is None: 85 | print('Testing all ciphers') 86 | 87 | for cipher, B in sorted(MAP_PY.items()): 88 | A = MAP.get(cipher) 89 | if A: 90 | test_both_cipher(A, B) 91 | elif B.__name__ in data: 92 | test_cipher(B, data) 93 | else: 94 | B = MAP_PY[cipher] 95 | A = MAP.get(cipher) 96 | if A: 97 | test_both_cipher(A, B) 98 | else: 99 | test_cipher(B, data) 100 | 101 | 102 | pickle.dump(data, open('.cipherdata', 'wb')) 103 | 104 | --------------------------------------------------------------------------------