160 lines
4.8 KiB
Python
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()
|