Skip to content

Commit 7f9a523

Browse files
committed
h3 proto
1 parent 5326c17 commit 7f9a523

File tree

3 files changed

+99
-4
lines changed

3 files changed

+99
-4
lines changed

README.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ python-proxy
1212
.. |Downloads| image:: https://pepy.tech/badge/pproxy
1313
:target: https://pepy.tech/project/pproxy
1414

15-
HTTP/Socks4/Socks5/Shadowsocks/ShadowsocksR/SSH/Redirect/Pf TCP/UDP asynchronous tunnel proxy implemented in Python3 asyncio.
15+
HTTP/Socks4/Socks5/Shadowsocks/ShadowsocksR/SSH/Redirect/Pf/HTTP2/HTTP3/QUIC TCP/UDP asynchronous tunnel proxy implemented in Python3 asyncio.
1616

1717
QuickStart
1818
----------
@@ -75,6 +75,7 @@ Features
7575
- Incoming traffic auto-detect.
7676
- Tunnel/jump/backward-jump support.
7777
- Unix domain socket support.
78+
- HTTP v2, HTTP v3 (based on QUIC)
7879
- User/password authentication support.
7980
- Filter/block hostname by regex patterns.
8081
- SSL/TLS client/server support.
@@ -100,6 +101,8 @@ Protocols
100101
+-------------------+------------+------------+------------+------------+--------------+
101102
| http v2 (connect) ||| | | h2:// |
102103
+-------------------+------------+------------+------------+------------+--------------+
104+
| http v3 (connect) ||| | | h3:// |
105+
+-------------------+------------+------------+------------+------------+--------------+
103106
| https ||| | | http+ssl:// |
104107
+-------------------+------------+------------+------------+------------+--------------+
105108
| socks4 ||| | | socks4:// |

pproxy/proto.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,9 @@ async def connect(self, reader_remote, writer_remote, rauth, host_name, port, my
401401
headers.append(('proxy-authorization', 'Basic '+base64.b64encode(rauth)))
402402
writer_remote.send_headers(headers)
403403

404+
class H3(H2):
405+
pass
406+
404407
class SSH(BaseProtocol):
405408
async def connect(self, reader_remote, writer_remote, rauth, host_name, port, myhost, **kw):
406409
pass
@@ -564,7 +567,7 @@ def udp_accept(protos, data, **kw):
564567
return (proto,) + ret
565568
raise Exception(f'Unsupported protocol {data[:10]}')
566569

567-
MAPPINGS = dict(direct=Direct, http=HTTP, httponly=HTTPOnly, 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, ssl='', secure='', quic='')
570+
MAPPINGS = dict(direct=Direct, http=HTTP, httponly=HTTPOnly, 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='')
568571
MAPPINGS['in'] = ''
569572

570573
def get_protos(rawprotos):

pproxy/server.py

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,93 @@ def handler(reader, writer):
508508
asyncio.ensure_future(stream_handler(reader, writer, **vars(self), **args))
509509
return aioquic.asyncio.serve(self.host_name, self.port, configuration=self.quicserver, stream_handler=handler)
510510

511+
class ProxyH3(ProxyQUIC):
512+
def get_stream(self, conn, stream_id):
513+
remote_addr = conn._quic._network_paths[0].addr
514+
reader = asyncio.StreamReader()
515+
class StreamWriter():
516+
def __init__(self):
517+
self.closed = False
518+
self.headers = asyncio.get_event_loop().create_future()
519+
def get_extra_info(self, key):
520+
return dict(peername=remote_addr, sockname=remote_addr).get(key)
521+
def write(self, data):
522+
conn.http.send_data(stream_id, data, False)
523+
conn.transmit()
524+
async def drain(self):
525+
conn.transmit()
526+
def is_closing(self):
527+
return self.closed
528+
def close(self):
529+
if not self.closed:
530+
conn.http.send_data(stream_id, b'', True)
531+
conn.transmit()
532+
conn.close_stream(stream_id)
533+
self.closed = True
534+
def send_headers(self, headers):
535+
conn.http.send_headers(stream_id, [(i.encode(), j.encode()) for i, j in headers])
536+
conn.transmit()
537+
return reader, StreamWriter()
538+
def get_protocol(self, server_side=False, handler=None):
539+
import aioquic.asyncio, aioquic.quic.events, aioquic.h3.connection, aioquic.h3.events
540+
class Protocol(aioquic.asyncio.QuicConnectionProtocol):
541+
def __init__(s, *args, **kw):
542+
super().__init__(*args, **kw)
543+
s.http = aioquic.h3.connection.H3Connection(s._quic)
544+
s.streams = {}
545+
def quic_event_received(s, event):
546+
if not server_side:
547+
if isinstance(event, aioquic.quic.events.HandshakeCompleted):
548+
self.handshake.set_result(s)
549+
elif isinstance(event, aioquic.quic.events.ConnectionTerminated):
550+
self.handshake = None
551+
self.quic_egress_acm = None
552+
if s.http is not None:
553+
for http_event in s.http.handle_event(event):
554+
s.http_event_received(http_event)
555+
def http_event_received(s, event):
556+
if isinstance(event, aioquic.h3.events.HeadersReceived):
557+
if event.stream_id not in s.streams and server_side:
558+
reader, writer = s.create_stream(event.stream_id)
559+
writer.headers.set_result(event.headers)
560+
asyncio.ensure_future(handler(reader, writer))
561+
elif isinstance(event, aioquic.h3.events.DataReceived) and event.stream_id in s.streams:
562+
reader, writer = s.streams[event.stream_id]
563+
if event.data:
564+
reader.feed_data(event.data)
565+
if event.stream_ended:
566+
reader.feed_eof()
567+
s.close_stream(event.stream_id)
568+
def create_stream(s, stream_id=None):
569+
if stream_id is None:
570+
stream_id = s._quic.get_next_available_stream_id(False)
571+
s._quic._get_or_create_stream_for_send(stream_id)
572+
reader, writer = self.get_stream(s, stream_id)
573+
s.streams[stream_id] = (reader, writer)
574+
return reader, writer
575+
def close_stream(s, stream_id):
576+
if stream_id in s.streams:
577+
reader, writer = s.streams[stream_id]
578+
if reader.at_eof() and writer.is_closing():
579+
s.streams.pop(stream_id)
580+
return Protocol
581+
async def wait_h3_connection(self):
582+
if self.handshake is not None:
583+
if not self.handshake.done():
584+
await self.handshake
585+
else:
586+
import aioquic.asyncio
587+
self.handshake = asyncio.get_event_loop().create_future()
588+
self.quic_egress_acm = aioquic.asyncio.connect(self.host_name, self.port, create_protocol=self.get_protocol(), configuration=self.quicclient)
589+
conn = await self.quic_egress_acm.__aenter__()
590+
await self.handshake
591+
async def wait_open_connection(self, *args):
592+
await self.wait_h3_connection()
593+
return self.handshake.result().create_stream()
594+
def start_server(self, args, stream_handler=stream_handler):
595+
import aioquic.asyncio
596+
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)))
597+
511598
class ProxySSH(ProxySimple):
512599
def __init__(self, **kw):
513600
super().__init__(**kw)
@@ -670,6 +757,7 @@ def proxy_by_uri(uri, jump):
670757
url = urllib.parse.urlparse('s://'+uri)
671758
rawprotos = [i.lower() for i in scheme.split('+')]
672759
err_str, protos = proto.get_protos(rawprotos)
760+
protonames = [i.name for i in protos]
673761
if err_str:
674762
raise argparse.ArgumentTypeError(err_str)
675763
if 'ssl' in rawprotos or 'secure' in rawprotos:
@@ -683,7 +771,7 @@ def proxy_by_uri(uri, jump):
683771
sslcontexts.append(sslclient)
684772
else:
685773
sslserver = sslclient = None
686-
if 'quic' in rawprotos:
774+
if 'quic' in rawprotos or 'h3' in protonames:
687775
try:
688776
import ssl, aioquic.quic.configuration
689777
except Exception:
@@ -698,7 +786,6 @@ def proxy_by_uri(uri, jump):
698786
import h2
699787
except Exception:
700788
raise Exception('Missing library: "pip3 install h2"')
701-
protonames = [i.name for i in protos]
702789
urlpath, _, plugins = url.path.partition(',')
703790
urlpath, _, lbind = urlpath.partition('@')
704791
plugins = plugins.split(',') if plugins else None
@@ -740,6 +827,8 @@ def proxy_by_uri(uri, jump):
740827
host_name=host_name, port=port, unix=not loc, lbind=lbind, sslclient=sslclient, sslserver=sslserver)
741828
if 'quic' in rawprotos:
742829
proxy = ProxyQUIC(quicserver, quicclient, **params)
830+
elif 'h3' in protonames:
831+
proxy = ProxyH3(quicserver, quicclient, **params)
743832
elif 'h2' in protonames:
744833
proxy = ProxyH2(**params)
745834
elif 'ssh' in protonames:

0 commit comments

Comments
 (0)