diff --git a/.gitignore b/.gitignore index df23cb1..436b82f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ flash.bin result _scratch/ sd.img +.serve-cert.pem +.serve-key.pem diff --git a/Makefile b/Makefile index ea9c53b..f37f5e3 100644 --- a/Makefile +++ b/Makefile @@ -29,8 +29,10 @@ qemu: qemu-wasm: QEMU_WASM_SRC=$(QEMU_WASM_SRC) WASM_OUT=$(WASM_OUT) JOBS=2 scripts/build-qemu-wasm.sh +# Serve over HTTPS by default so headless/LAN access is cross-origin isolated. Override with WEB_TLS=. +WEB_TLS ?= --tls web: qemu-wasm - python3 scripts/serve-web.py + python3 scripts/serve-web.py $(WEB_TLS) web-test: node scripts/test-web.mjs diff --git a/scripts/serve-web.py b/scripts/serve-web.py index cddb599..ae766b4 100644 --- a/scripts/serve-web.py +++ b/scripts/serve-web.py @@ -1,6 +1,11 @@ #!/usr/bin/env python3 import argparse +import ipaddress +import socket +import ssl +import subprocess from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path class IsolatedHandler(SimpleHTTPRequestHandler): @@ -11,15 +16,56 @@ class IsolatedHandler(SimpleHTTPRequestHandler): super().end_headers() +def local_ip(): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + try: + s.connect(("8.8.8.8", 80)) + return s.getsockname()[0] + except OSError: + return "127.0.0.1" + + +def ensure_cert(cert: Path, key: Path): + """Generate a self-signed cert covering localhost and the LAN IP, so a + headless box can be reached directly over HTTPS (secure context).""" + if cert.exists() and key.exists(): + return + ip = local_ip() + san = f"subjectAltName=DNS:localhost,IP:127.0.0.1,IP:{ip}" + subprocess.run( + ["openssl", "req", "-x509", "-newkey", "rsa:2048", "-sha256", + "-days", "3650", "-nodes", "-keyout", str(key), "-out", str(cert), + "-subj", "/CN=localhost", "-addext", san], + check=True, + ) + print(f"Generated self-signed cert for localhost and {ip} at {cert}") + + def main(): parser = argparse.ArgumentParser() parser.add_argument("--port", type=int, default=8000) parser.add_argument("--host", default="0.0.0.0") + parser.add_argument("--tls", action="store_true", help="serve over HTTPS with a self-signed cert") + parser.add_argument("--cert", default=".serve-cert.pem") + parser.add_argument("--key", default=".serve-key.pem") args = parser.parse_args() + server = ThreadingHTTPServer((args.host, args.port), IsolatedHandler) - print(f"Serving on http://{args.host}:{args.port}/web/") - print("Note: WASM pthreads need a secure context — use localhost or HTTPS; a" - " bare LAN IP over HTTP is not cross-origin isolated.") + scheme = "http" + if args.tls: + ensure_cert(Path(args.cert), Path(args.key)) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(args.cert, args.key) + server.socket = context.wrap_socket(server.socket, server_side=True) + scheme = "https" + + print(f"Serving on {scheme}://{args.host}:{args.port}/web/") + if args.tls: + print(f"Reach it directly at {scheme}://{local_ip()}:{args.port}/web/ " + "(accept the self-signed cert warning once).") + else: + print("Note: WASM pthreads need a secure context — use localhost, an SSH " + "tunnel, or --tls; a bare LAN IP over HTTP is not cross-origin isolated.") server.serve_forever()