#!/usr/bin/env python3 """Local static server for this project with HTTP and HTTPS support.""" from __future__ import annotations import argparse import ssl import sys import threading import time from functools import partial from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parent DEFAULT_WEB_DIR = PROJECT_ROOT / "www" DEFAULT_SSL_DIR = Path.home() / ".ssl_for_local_servers" DEFAULT_CERT_PATH = DEFAULT_SSL_DIR / "cert.pem" DEFAULT_KEY_PATH = DEFAULT_SSL_DIR / "key.pem" def make_handler(directory: Path): return partial(SimpleHTTPRequestHandler, directory=str(directory)) def start_http_server(host: str, port: int, directory: Path) -> ThreadingHTTPServer: server = ThreadingHTTPServer((host, port), make_handler(directory)) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() print(f"[HTTP ] Serving {directory} at http://{host}:{port}") return server def start_https_server( host: str, port: int, directory: Path, cert_path: Path, key_path: Path, ) -> ThreadingHTTPServer: server = ThreadingHTTPServer((host, port), make_handler(directory)) context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain(certfile=str(cert_path), keyfile=str(key_path)) server.socket = context.wrap_socket(server.socket, server_side=True) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() print(f"[HTTPS] Serving {directory} at https://{host}:{port}") print(f"[HTTPS] Using cert: {cert_path}") print(f"[HTTPS] Using key : {key_path}") return server def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Serve this project locally over HTTP and/or HTTPS." ) parser.add_argument( "--mode", choices=["http", "https", "both"], default="both", help="Which protocol(s) to serve (default: both).", ) parser.add_argument( "--host", default="127.0.0.1", help="Host/interface to bind (default: 127.0.0.1).", ) parser.add_argument( "--http-port", type=int, default=8080, help="HTTP port (default: 8080).", ) parser.add_argument( "--https-port", type=int, default=8443, help="HTTPS port (default: 8443).", ) parser.add_argument( "--dir", default=str(DEFAULT_WEB_DIR), help=f"Directory to serve (default: {DEFAULT_WEB_DIR}).", ) parser.add_argument( "--cert", default=str(DEFAULT_CERT_PATH), help=f"TLS cert path (default: {DEFAULT_CERT_PATH}).", ) parser.add_argument( "--key", default=str(DEFAULT_KEY_PATH), help=f"TLS key path (default: {DEFAULT_KEY_PATH}).", ) return parser.parse_args() def validate_mode_and_paths(args: argparse.Namespace) -> tuple[Path, Path, Path]: directory = Path(args.dir).expanduser().resolve() cert_path = Path(args.cert).expanduser().resolve() key_path = Path(args.key).expanduser().resolve() if not directory.exists() or not directory.is_dir(): print(f"Error: directory does not exist or is not a directory: {directory}", file=sys.stderr) raise SystemExit(2) if args.mode in {"https", "both"}: if not cert_path.exists(): print(f"Error: cert file not found: {cert_path}", file=sys.stderr) raise SystemExit(2) if not key_path.exists(): print(f"Error: key file not found: {key_path}", file=sys.stderr) raise SystemExit(2) return directory, cert_path, key_path def main() -> None: args = parse_args() directory, cert_path, key_path = validate_mode_and_paths(args) servers: list[ThreadingHTTPServer] = [] try: if args.mode in {"http", "both"}: servers.append(start_http_server(args.host, args.http_port, directory)) if args.mode in {"https", "both"}: if args.mode == "both" and args.http_port == args.https_port: print("Error: --http-port and --https-port must differ in mode=both.", file=sys.stderr) raise SystemExit(2) servers.append( start_https_server( args.host, args.https_port, directory, cert_path, key_path, ) ) print("Press Ctrl+C to stop.") while True: time.sleep(1) except KeyboardInterrupt: print("\nShutting down...") finally: for server in servers: server.shutdown() server.server_close() print("Stopped.") if __name__ == "__main__": main()