feat(web): add self-signed HTTPS mode for headless/LAN access

--tls generates a self-signed cert (SANs: localhost, 127.0.0.1, LAN IP) so a
headless box is reachable directly over HTTPS, which browsers treat as a secure
context (required for cross-origin isolation). make web now defaults to TLS.
This commit is contained in:
2026-07-20 08:27:51 -04:00
parent 15436ee089
commit 08f5e342b0
3 changed files with 54 additions and 4 deletions
+2
View File
@@ -8,3 +8,5 @@ flash.bin
result
_scratch/
sd.img
.serve-cert.pem
.serve-key.pem
+3 -1
View File
@@ -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
+49 -3
View File
@@ -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()