Files
codexPySnake/server/static_server.py
Vladyslav Doloman 1de5a8f3e6 Fix WebTransport server implementation and add test client
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>
2025-10-19 23:50:08 +00:00

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