Add http3 module: HTTP/3 in the Java HTTP Client (JEP 517), opt-in, discovery modes, fallback timings, proxy downgrade and a packet-loss comparison
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
"""The test application. respond() is shared by the Hypercorn ASGI wrapper (HTTP/1.1, HTTP/2) and the aioquic
|
||||
HTTP/3 server. It reports which HTTP version the request arrived on, returns N bytes for /bytes/N, and can redirect."""
|
||||
import os
|
||||
|
||||
ALT_SVC = os.environ.get("ALT_SVC_PORT") # set by serve.py in h3 mode: advertise h3 on that UDP port
|
||||
|
||||
def respond(path, version, host, port):
|
||||
hdrs = []
|
||||
if ALT_SVC:
|
||||
hdrs.append((b"alt-svc", ('h3=":%s"; ma=3600' % ALT_SVC).encode()))
|
||||
if path.startswith("/bytes/"):
|
||||
body = b"x" * int(path.rsplit("/", 1)[1]); ctype = b"application/octet-stream"; status = 200
|
||||
elif path == "/redirect":
|
||||
hdrs.append((b"location", b"/hello")); return 302, hdrs, b""
|
||||
else:
|
||||
body = ("hello from %s:%s via HTTP/%s\n" % (host, port, version)).encode(); ctype = b"text/plain"; status = 200
|
||||
hdrs += [(b"content-type", ctype), (b"content-length", str(len(body)).encode())]
|
||||
return status, hdrs, body
|
||||
|
||||
async def app(scope, receive, send):
|
||||
if scope["type"] == "lifespan":
|
||||
while True:
|
||||
m = await receive()
|
||||
if m["type"] == "lifespan.startup":
|
||||
await send({"type": "lifespan.startup.complete"})
|
||||
elif m["type"] == "lifespan.shutdown":
|
||||
await send({"type": "lifespan.shutdown.complete"}); return
|
||||
if scope["type"] != "http":
|
||||
return
|
||||
while (await receive()).get("more_body"): # drain the request body
|
||||
pass
|
||||
status, headers, body = respond(scope["path"], scope["http_version"], scope["server"][0], scope["server"][1])
|
||||
await send({"type": "http.response.start", "status": status, "headers": headers})
|
||||
await send({"type": "http.response.body", "body": body})
|
||||
@@ -0,0 +1,25 @@
|
||||
"""A 25-line HTTP CONNECT proxy, just enough to show that the JDK will not send HTTP/3 through one."""
|
||||
import asyncio, sys
|
||||
|
||||
async def pipe(r, w):
|
||||
try:
|
||||
while data := await r.read(65536):
|
||||
w.write(data); await w.drain()
|
||||
finally:
|
||||
w.close()
|
||||
|
||||
async def handle(cr, cw):
|
||||
line = (await cr.readline()).decode()
|
||||
while (await cr.readline()) not in (b"\r\n", b""):
|
||||
pass
|
||||
method, target, _ = line.split()
|
||||
print("proxy saw:", method, target, flush=True)
|
||||
host, port = target.rsplit(":", 1)
|
||||
ur, uw = await asyncio.open_connection(host, int(port))
|
||||
cw.write(b"HTTP/1.1 200 Connection established\r\n\r\n"); await cw.drain()
|
||||
await asyncio.gather(pipe(cr, uw), pipe(ur, cw))
|
||||
|
||||
async def main():
|
||||
srv = await asyncio.start_server(handle, "127.0.0.1", int(sys.argv[1]))
|
||||
async with srv: await srv.serve_forever()
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Minimal HTTP/3 server on aioquic (the library Hypercorn uses for QUIC). It answers the same paths as app.py.
|
||||
Hypercorn's own QUIC listener proved unreliable under packet loss (its UDPServer task dies on a KeyError and the
|
||||
port then goes silent), so HTTP/3 is served from here and Hypercorn serves only TCP."""
|
||||
import asyncio, ssl
|
||||
from aioquic.asyncio import QuicConnectionProtocol, serve as quic_serve
|
||||
from aioquic.h3.connection import H3_ALPN, H3Connection
|
||||
from aioquic.h3.events import HeadersReceived, DataReceived
|
||||
from aioquic.quic.configuration import QuicConfiguration
|
||||
from aioquic.quic.events import ProtocolNegotiated
|
||||
|
||||
from app import respond # (path, http_version, host, port) -> (status, headers, body)
|
||||
|
||||
class H3Protocol(QuicConnectionProtocol):
|
||||
def __init__(self, *a, **kw):
|
||||
super().__init__(*a, **kw)
|
||||
self._http = None
|
||||
self._pending = {}
|
||||
|
||||
def quic_event_received(self, event):
|
||||
if isinstance(event, ProtocolNegotiated) and event.alpn_protocol in H3_ALPN:
|
||||
self._http = H3Connection(self._quic)
|
||||
if self._http is None:
|
||||
return
|
||||
for ev in self._http.handle_event(event):
|
||||
if isinstance(ev, HeadersReceived):
|
||||
self._pending[ev.stream_id] = dict(ev.headers)
|
||||
if ev.stream_ended: self._reply(ev.stream_id)
|
||||
elif isinstance(ev, DataReceived) and ev.stream_ended:
|
||||
self._reply(ev.stream_id)
|
||||
|
||||
def _reply(self, stream_id):
|
||||
h = self._pending.pop(stream_id, None)
|
||||
if h is None: return
|
||||
status, headers, body = respond(h[b":path"].decode(), "3", "127.0.0.1", self._port)
|
||||
self._http.send_headers(stream_id, [(b":status", str(status).encode())] + headers)
|
||||
self._http.send_data(stream_id, body, end_stream=True)
|
||||
self.transmit()
|
||||
|
||||
_port = 4433
|
||||
|
||||
async def start(port, certfile, keyfile):
|
||||
H3Protocol._port = port
|
||||
cfg = QuicConfiguration(is_client=False, alpn_protocols=H3_ALPN)
|
||||
cfg.load_cert_chain(certfile, keyfile)
|
||||
await quic_serve("127.0.0.1", port, configuration=cfg, create_protocol=H3Protocol)
|
||||
await asyncio.Event().wait()
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Self-signed certificate for localhost / 127.0.0.1, valid 30 days. Only the test server uses it.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."; mkdir -p certs
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -days 30 -keyout certs/key.pem -out certs/cert.pem \
|
||||
-subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2>/dev/null
|
||||
echo "wrote certs/cert.pem and certs/key.pem"
|
||||
@@ -0,0 +1,29 @@
|
||||
"""serve.py MODE [PORT]
|
||||
h3 HTTP/1.1 + HTTP/2 on TCP (Hypercorn) and HTTP/3 on UDP (aioquic), same port number, Alt-Svc advertised
|
||||
h2 TCP only, ALPN h2 + http/1.1
|
||||
h1 TCP only, ALPN http/1.1
|
||||
"""
|
||||
import asyncio, os, sys
|
||||
mode = sys.argv[1]; port = sys.argv[2] if len(sys.argv) > 2 else "4433"
|
||||
if mode == "h3": os.environ["ALT_SVC_PORT"] = port
|
||||
from hypercorn.asyncio import serve
|
||||
from hypercorn.config import Config
|
||||
from app import app
|
||||
|
||||
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
c = Config()
|
||||
c.certfile, c.keyfile = f"{root}/certs/cert.pem", f"{root}/certs/key.pem"
|
||||
c.bind = [f"127.0.0.1:{port}"]
|
||||
c.loglevel = "WARNING"
|
||||
if mode == "h1":
|
||||
c.alpn_protocols = ["http/1.1"]
|
||||
elif mode not in ("h2", "h3"):
|
||||
sys.exit("mode must be h3, h2 or h1")
|
||||
|
||||
async def main():
|
||||
tasks = [serve(app, c)]
|
||||
if mode == "h3":
|
||||
import h3server
|
||||
tasks.append(h3server.start(int(port), c.certfile, c.keyfile))
|
||||
await asyncio.gather(*tasks)
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user