Server fixes: - Move H3Connection initialization to ProtocolNegotiated event (matches official aioquic pattern) - Fix datagram routing to use session_id instead of flow_id - Add max_datagram_frame_size=65536 to enable QUIC datagrams - Fix send_datagram() to use keyword arguments - Add certificate chain handling for Let's Encrypt - Add no-cache headers to static server Command-line improvements: - Move settings from environment variables to argparse - Add comprehensive CLI arguments with defaults - Default mode=wt, cert=cert.pem, key=key.pem Test clients: - Add test_webtransport_client.py - Python WebTransport client that successfully connects - Add test_http3.py - Basic HTTP/3 connectivity test Client updates: - Auto-configure server URL and certificate hash from /cert-hash.json - Add ES6 module support Status: ✅ Python WebTransport client works perfectly ✅ Server properly handles WebTransport connections and datagrams ❌ Chrome fails due to cached QUIC state (QUIC_IETF_GQUIC_ERROR_MISSING) 🔍 Firefox sends packets but fails differently - to be debugged next session 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
78 lines
2.7 KiB
Python
78 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
import ssl
|
|
import logging
|
|
import threading
|
|
import json
|
|
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
|
|
from pathlib import Path
|
|
from typing import Tuple, Optional
|
|
|
|
|
|
class _Handler(SimpleHTTPRequestHandler):
|
|
# Allow passing a base directory and cert_hash_json at construction time
|
|
cert_hash_json: Optional[str] = None
|
|
|
|
def __init__(self, *args, directory: str | None = None, **kwargs):
|
|
super().__init__(*args, directory=directory, **kwargs)
|
|
|
|
def end_headers(self):
|
|
# Add no-cache headers for all static files to force browser to fetch fresh versions
|
|
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
|
|
self.send_header('Pragma', 'no-cache')
|
|
self.send_header('Expires', '0')
|
|
super().end_headers()
|
|
|
|
def do_GET(self):
|
|
# Intercept /cert-hash.json requests
|
|
if self.path == '/cert-hash.json' and self.cert_hash_json:
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
self.end_headers()
|
|
self.wfile.write(self.cert_hash_json.encode('utf-8'))
|
|
else:
|
|
# Serve regular static files
|
|
super().do_GET()
|
|
|
|
|
|
def start_https_static(
|
|
host: str,
|
|
port: int,
|
|
certfile: str,
|
|
keyfile: str,
|
|
docroot: str,
|
|
cert_hash_json: Optional[str] = None
|
|
) -> Tuple[ThreadingHTTPServer, threading.Thread]:
|
|
"""Start a simple HTTPS static file server in a background thread.
|
|
|
|
Args:
|
|
host: Host to bind to
|
|
port: Port to bind to
|
|
certfile: Path to TLS certificate
|
|
keyfile: Path to TLS private key
|
|
docroot: Document root directory
|
|
cert_hash_json: Optional JSON string to serve at /cert-hash.json
|
|
|
|
Returns the (httpd, thread). Caller is responsible for calling httpd.shutdown()
|
|
to stop the server on application exit.
|
|
"""
|
|
docroot_path = str(Path(docroot).resolve())
|
|
|
|
# Set class variable for the handler
|
|
if cert_hash_json:
|
|
_Handler.cert_hash_json = cert_hash_json
|
|
|
|
def handler(*args, **kwargs):
|
|
return _Handler(*args, directory=docroot_path, **kwargs)
|
|
|
|
httpd = ThreadingHTTPServer((host, port), handler)
|
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
ctx.load_cert_chain(certfile=certfile, keyfile=keyfile)
|
|
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
|
|
|
|
t = threading.Thread(target=httpd.serve_forever, name="https-static", daemon=True)
|
|
t.start()
|
|
logging.info("HTTPS static server listening on https://%s:%d serving '%s'", host, port, docroot_path)
|
|
return httpd, t
|