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