Last active
March 9, 2025 07:15
-
-
Save notdaniel/de88c1e7a548c0f0820f2c53b3af5901 to your computer and use it in GitHub Desktop.
Slightly improved version of Python's simple HTTP server enabling threading, CORS, and custom headers.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
Slightly improved version of the simple HTTP server from the Python stdlib. | |
Provides options for a threaded server, CORS headers, and custom headers. | |
""" | |
from argparse import ArgumentParser, Namespace | |
from http.server import HTTPServer, SimpleHTTPRequestHandler, ThreadingHTTPServer | |
from pathlib import Path | |
def valid_header(header: str) -> tuple[str, str]: | |
"""Validate a header string.""" | |
try: | |
name, value = header.split(":", 1) | |
return name.strip(), value.strip() | |
except ValueError as e: | |
raise ValueError(f"Invalid header format: {header}") from e | |
def valid_path(path: str) -> str: | |
"""Validate a path string.""" | |
p = Path(path) | |
if not p.exists() or not p.is_dir(): | |
raise ValueError(f"Invalid path: {path}") | |
return str(p.resolve()) | |
class CustomHeadersHTTPRequestHandler(SimpleHTTPRequestHandler): | |
""" | |
Extends SimpleHTTPRequestHandler to add custom response headers | |
while keeping all other default behavior. | |
""" | |
# Default custom headers (can be extended via command line) | |
CUSTOM_HEADERS: dict[str, str] = {} | |
DIRECTORY: str = "." | |
def __init__(self, *args, **kwargs) -> None: | |
super().__init__(*args, directory=self.DIRECTORY, **kwargs) | |
def end_headers(self) -> None: | |
""" | |
Override end_headers method to add custom headers before | |
the original method sends the blank line signaling end of headers. | |
""" | |
# Add all custom headers | |
for name, value in self.CUSTOM_HEADERS.items(): | |
self.send_header(name, value) | |
# Call the original end_headers method from the parent class | |
super().end_headers() | |
def run_server( | |
port: int = 8000, | |
bind: str = "0.0.0.0", | |
directory: str = ".", | |
threading: bool = False, | |
enable_cors: bool = False, | |
custom_headers: list[tuple[str, str]] | None = None, | |
) -> None: | |
""" | |
Run an HTTP server with the specified configuration. | |
Args: | |
port: Port number to listen on | |
bind: Address to bind to (empty string means all interfaces) | |
threading: Whether to use a threaded server | |
enable_cors: Whether to enable CORS headers | |
custom_headers: Additional headers to add to all responses | |
""" | |
server_address = (bind, port) | |
# Start with default headers from CustomHeadersHTTPRequestHandler | |
handler_headers = CustomHeadersHTTPRequestHandler.CUSTOM_HEADERS.copy() | |
# Add CORS headers if enabled | |
if enable_cors: | |
handler_headers.update( | |
{ | |
"Access-Control-Allow-Origin": "*", | |
"Access-Control-Allow-Methods": "GET, OPTIONS", | |
} | |
) | |
# Add custom headers from command line | |
if custom_headers: | |
for key, value in custom_headers: | |
handler_headers[key] = value | |
# Create a configured handler class with our custom headers | |
class ConfiguredHandler(CustomHeadersHTTPRequestHandler): | |
CUSTOM_HEADERS: dict[str, str] = handler_headers | |
DIRECTORY: str = directory | |
# Choose between regular HTTPServer and ThreadingHTTPServer | |
server_class: type[HTTPServer] = ThreadingHTTPServer if threading else HTTPServer | |
with server_class(server_address, ConfiguredHandler) as httpd: | |
host_name: str = bind | |
print(f"Server started at http://{host_name}:{port}") | |
print(f"Serving from path: {directory}") | |
print(f"Using server type: {server_class.__name__}") | |
if ConfiguredHandler.CUSTOM_HEADERS: | |
print("Custom headers:") | |
for name, value in ConfiguredHandler.CUSTOM_HEADERS.items(): | |
print(f" {name}: {value}") | |
print("Press Ctrl+C to stop") | |
try: | |
httpd.serve_forever() | |
except KeyboardInterrupt: | |
print("\nServer stopped.") | |
def main() -> None: | |
parser: ArgumentParser = ArgumentParser( | |
description="HTTP Server with threading and custom response headers" | |
) | |
parser.add_argument( | |
"--port", "-p", type=int, default=8000, help="Port to listen on (default: 8000)" | |
) | |
parser.add_argument( | |
"--bind", | |
"-b", | |
default="0.0.0.0", | |
help="Address to bind to (default: all interfaces)", | |
) | |
parser.add_argument( | |
"--dir", | |
"-d", | |
dest="directory", | |
type=valid_path, | |
default=".", | |
help="Directory to serve (default: current directory)", | |
) | |
parser.add_argument( | |
"--cors", | |
"-c", | |
action="store_true", | |
help="Enable CORS headers", | |
) | |
parser.add_argument( | |
"--no-threading", | |
"-n", | |
action="store_true", | |
help="Use non-threaded server", | |
) | |
parser.add_argument( | |
"--header", | |
"-H", | |
type=valid_header, | |
action="append", | |
dest="headers", | |
metavar="KEY:VAL", | |
help="Add custom header (repeatable)", | |
) | |
args: Namespace = parser.parse_args() | |
run_server( | |
port=args.port, | |
bind=args.bind, | |
directory=args.directory, | |
threading=not args.no_threading, | |
enable_cors=args.cors, | |
custom_headers=args.headers, | |
) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment