Files
client/serve_local.py
2026-04-17 16:52:51 -04:00

160 lines
4.8 KiB
Python

#!/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()