diff --git a/run.py b/run.py index 77c08ba..5f5d74a 100644 --- a/run.py +++ b/run.py @@ -25,13 +25,28 @@ async def run_quic(): async def run_webtransport(): from server.webtransport_server import WebTransportServer + from server.static_server import start_https_static cfg = ServerConfig() host = os.environ.get("WT_HOST", os.environ.get("QUIC_HOST", "0.0.0.0")) port = int(os.environ.get("WT_PORT", os.environ.get("QUIC_PORT", "4433"))) cert = os.environ.get("WT_CERT") or os.environ["QUIC_CERT"] key = os.environ.get("WT_KEY") or os.environ["QUIC_KEY"] + # Optional static HTTPS server for client assets + static = os.environ.get("STATIC", "1") + static_host = os.environ.get("STATIC_HOST", host) + static_port = int(os.environ.get("STATIC_PORT", "8443")) + static_root = os.environ.get("STATIC_ROOT", "client") + httpd = None + if static == "1": + httpd, _t = start_https_static(static_host, static_port, cert, key, static_root) + print(f"HTTPS static server: https://{static_host}:{static_port}/ serving '{static_root}'") server = GameServer(transport=WebTransportServer(host, port, cert, key, lambda d, p: server.on_datagram(d, p)), config=cfg) - await asyncio.gather(server.transport.run(), server.tick_loop()) + print(f"WebTransport server: https://{host}:{port}/ (HTTP/3)") + try: + await asyncio.gather(server.transport.run(), server.tick_loop()) + finally: + if httpd is not None: + httpd.shutdown() if __name__ == "__main__": diff --git a/server/static_server.py b/server/static_server.py new file mode 100644 index 0000000..ca80ffb --- /dev/null +++ b/server/static_server.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import ssl +import threading +from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler +from pathlib import Path +from typing import Tuple + + +class _Handler(SimpleHTTPRequestHandler): + # Allow passing a base directory at construction time + def __init__(self, *args, directory: str | None = None, **kwargs): + super().__init__(*args, directory=directory, **kwargs) + + +def start_https_static(host: str, port: int, certfile: str, keyfile: str, docroot: str) -> Tuple[ThreadingHTTPServer, threading.Thread]: + """Start a simple HTTPS static file server in a background thread. + + Returns the (httpd, thread). Caller is responsible for calling httpd.shutdown() + to stop the server on application exit. + """ + docroot_path = str(Path(docroot).resolve()) + + 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() + return httpd, t +