#!/usr/bin/env python3 """ ASW Dev Server — http.server with ASW-styled error pages. Drop this in any directory and run it instead of `python -m http.server`. All error responses (403, 404, 500, etc.) are styled with ASW CSS. Usage: python asw_server.py # serve on port 8000 python asw_server.py 3000 # custom port python asw_server.py 3000 /dir # custom port + directory The server inlines ASW styles so no external dependencies are needed. """ import http.server import sys import os import socket import urllib.parse import html # ── Minimal ASW inline styles ───────────────────────────────────────────────── # Reduced version for standalone error pages — no external deps. ASW_INLINE = """ :root { --asw-bg: #0d1117; --asw-bg-elevated: #161b22; --asw-bg-overlay: #1c2128; --asw-text: #e6edf3; --asw-text-secondary: #8b949e; --asw-text-muted: #484f58; --asw-accent: #3fb950; --asw-border: #30363d; --asw-font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; --asw-font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', ui-monospace, monospace; } *, *::before, *::after { box-sizing: border-box; } html { font-size: 16px; } body { background: var(--asw-bg); color: var(--asw-text); font-family: var(--asw-font-body); font-size: 1rem; line-height: 1.6; margin: 0; display: flex; flex-direction: column; min-height: 100vh; } nav { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1.5rem; border-bottom: 1px solid var(--asw-border); background: var(--asw-bg-elevated); } nav a { color: var(--asw-text); text-decoration: none; font-weight: 600; } nav a:hover { color: var(--asw-accent); } .badge { font-family: var(--asw-font-mono); font-size: 0.75rem; color: var(--asw-text-muted); background: var(--asw-bg-overlay); padding: 0.25rem 0.5rem; border-radius: 0.25rem; border: 1px solid var(--asw-border); } main { flex: 1; display: flex; align-items: center; justify-content: center; padding: 2rem; } .error-card { max-width: 42ch; text-align: center; } .error-code { font-family: var(--asw-font-mono); font-size: 6rem; font-weight: 700; line-height: 1; color: var(--asw-text-muted); letter-spacing: -0.05em; margin: 0 0 0.5rem; } h1 { margin: 0 0 1rem; font-size: 1.5rem; color: var(--asw-text); } p { margin: 0 0 0.75rem; color: var(--asw-text-secondary); } a.button { display: inline-block; margin-top: 1.5rem; padding: 0.5rem 1.25rem; background: transparent; color: var(--asw-accent); border: 1px solid var(--asw-accent); border-radius: 0.375rem; text-decoration: none; font-size: 0.875rem; transition: background 0.15s; } a.button:hover { background: rgba(63, 185, 80, 0.1); } code { font-family: var(--asw-font-mono); font-size: 0.85em; background: var(--asw-bg-overlay); padding: 0.1em 0.35em; border-radius: 0.2rem; color: var(--asw-text-secondary); } footer { text-align: center; padding: 1rem; font-size: 0.75rem; color: var(--asw-text-muted); border-top: 1px solid var(--asw-border); } footer a { color: var(--asw-text-muted); } /* Directory listing */ .dir-listing { max-width: 72ch; width: 100%; } .dir-listing h1 { font-size: 1.25rem; margin-bottom: 1.5rem; text-align: left; } .dir-listing h1 code { font-size: 1rem; } table { width: 100%; border-collapse: collapse; } th, td { padding: 0.5rem 0.75rem; text-align: left; border-bottom: 1px solid var(--asw-border); } th { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--asw-text-muted); } td a { color: var(--asw-accent); text-decoration: none; } td a:hover { text-decoration: underline; } .size { color: var(--asw-text-muted); font-family: var(--asw-font-mono); font-size: 0.8rem; } .parent { color: var(--asw-text-muted); } """ ERROR_MESSAGES = { 400: ("Bad Request", "The server couldn't understand the request."), 401: ("Unauthorized", "Authentication is required to access this resource."), 403: ("Forbidden", "You don't have permission to access this path."), 404: ("Not Found", "The file or directory you requested doesn't exist."), 405: ("Method Not Allowed", "That HTTP method isn't supported here."), 408: ("Request Timeout", "The request took too long to process."), 500: ("Server Error", "Something went wrong on the server."), 501: ("Not Implemented", "The server doesn't support that feature."), 503: ("Service Unavailable", "The server is temporarily unable to handle requests."), } def error_page(code: int, path: str = "") -> bytes: title, message = ERROR_MESSAGES.get(code, ("Error", "An error occurred.")) path_hint = f"

Path: {html.escape(path)}

" if path else "" body = f""" {code} — {title}

{code}

{title}

{message}

{path_hint} ← Back to root
""" return body.encode("utf-8") def dir_listing_page(path: str, display_path: str, entries: list) -> bytes: rows = [] if display_path != "/": parent = "/" + "/".join(display_path.strip("/").split("/")[:-1]) rows.append(f'../—') for name, is_dir, size in sorted(entries, key=lambda e: (not e[1], e[0].lower())): link_name = html.escape(name) + ("/" if is_dir else "") href = urllib.parse.quote(name) + ("/" if is_dir else "") size_str = "—" if is_dir else _human_size(size) rows.append(f'{link_name}{size_str}') rows_html = "\n ".join(rows) body = f""" Index of {html.escape(display_path)}

Index of {html.escape(display_path)}

{rows_html}
NameSize
""" return body.encode("utf-8") def _human_size(n: int) -> str: for unit in ("B", "KB", "MB", "GB"): if n < 1024: return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}" n /= 1024 return f"{n:.1f} TB" class ASWHandler(http.server.SimpleHTTPRequestHandler): """SimpleHTTPRequestHandler with ASW-styled error pages and directory listings.""" server_version = "ASWServer/1.0" def send_error(self, code, message=None, explain=None): """Override to send ASW-styled error pages.""" try: short, long = self.responses[code] except KeyError: short, long = "???", "???" if message is None: message = short if explain is None: explain = long # Log to terminal self.log_error("code %d, message %s", code, message) # Build ASW error page path = getattr(self, "path", "") content = error_page(code, path) self.send_response(code, message) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(content))) self.send_header("Connection", "close") self.end_headers() if self.command != "HEAD" and code >= 200 and code not in (204, 304): self.wfile.write(content) def list_directory(self, path): """Override to send ASW-styled directory listing.""" try: names = os.listdir(path) except OSError: self.send_error(403, "No permission to list directory") return None display_path = urllib.parse.unquote(self.path) entries = [] for name in names: full = os.path.join(path, name) is_dir = os.path.isdir(full) size = 0 if not is_dir: try: size = os.path.getsize(full) except OSError: pass entries.append((name, is_dir, size)) content = dir_listing_page(path, display_path, entries) self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(content))) self.end_headers() self.wfile.write(content) return None def log_message(self, fmt, *args): """Slightly cleaner terminal output.""" print(f" {self.address_string()} — {fmt % args}") def main(): port = 8000 directory = "." if len(sys.argv) > 1: try: port = int(sys.argv[1]) except ValueError: print(f"Usage: {sys.argv[0]} [port] [directory]", file=sys.stderr) sys.exit(1) if len(sys.argv) > 2: directory = sys.argv[2] directory = os.path.abspath(directory) handler = lambda *args, **kwargs: ASWHandler(*args, directory=directory, **kwargs) # Find a free port if needed (dev convenience) for attempt in range(10): try: server = http.server.HTTPServer(("", port + attempt), handler) actual_port = port + attempt break except OSError: if attempt == 9: print(f"Could not bind to ports {port}–{port+9}", file=sys.stderr) sys.exit(1) hostname = socket.gethostname() print(f"\n ASW Dev Server") print(f" ──────────────────────────────────") print(f" Serving: {directory}") print(f" Local: http://localhost:{actual_port}") print(f" Network: http://{hostname}:{actual_port}") print(f" ──────────────────────────────────") print(f" Press Ctrl+C to stop\n") try: server.serve_forever() except KeyboardInterrupt: print("\n Stopped.") server.server_close() if __name__ == "__main__": main()