#!/usr/bin/env python3 """Local HTTP/1.1 origin for manual cache, health, compression and pool tests. Only Python's standard library is used. This is not a production application. """ from __future__ import annotations import argparse from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import ipaddress import json import secrets import threading import time from urllib.parse import parse_qs, urlsplit MAX_BODY = 2 * 1024 * 1024 MAX_WORKERS = 32 class State: def __init__(self, name="origin-a", ttl=30): self.name, self.ttl = name, ttl self.healthy, self.delay_ms, self.sequence = True, 0, 0 self.lock = threading.Lock() class Server(ThreadingHTTPServer): daemon_threads = True allow_reuse_address = True def __init__(self, address, state): self.state = state self.slots = threading.BoundedSemaphore(MAX_WORKERS) super().__init__(address, Handler) def process_request(self, request, client_address): if not self.slots.acquire(blocking=False): self.shutdown_request(request) return try: super().process_request(request, client_address) except BaseException: self.slots.release() raise def process_request_thread(self, request, client_address): try: super().process_request_thread(request, client_address) finally: self.slots.release() class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" server_version = "RustProxy-Mock/0.3.0" def setup(self): super().setup() self.connection.settimeout(15) def log_message(self, *_): pass def send(self, status, body=b"", content_type="application/json; charset=utf-8", control="no-store", extra=None): if isinstance(body, dict): body = (json.dumps(body, ensure_ascii=False) + "\n").encode() if isinstance(body, str): body = body.encode() if status in (204, 304): body = b"" self.send_response(status) self.send_header("Content-Type", content_type) if status != 204: self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", control) self.send_header("X-Mock-Origin", self.server.state.name) for name, value in (extra or {}).items(): self.send_header(name, value) self.end_headers() if self.command != "HEAD": try: self.wfile.write(body) except (BrokenPipeError, ConnectionResetError, TimeoutError): self.close_connection = True def integer(self, query, key, default, maximum): value = int(query.get(key, [str(default)])[0]) if not 0 <= value <= maximum: raise ValueError(key) return value def do_HEAD(self): self.do_GET() def do_GET(self): target = urlsplit(self.path) q = parse_qs(target.query) state = self.server.state try: delay = self.integer(q, "ms", 800 if target.path == "/slow" else 0, 10000) size = self.integer(q, "size", 65536, MAX_BODY) ttl = self.integer(q, "ttl", state.ttl, 604800) except ValueError: return self.send(400, {"error":"invalid query parameter"}) with state.lock: state.sequence += 1 sequence, healthy = state.sequence, state.healthy delay += state.delay_ms if delay: time.sleep(min(delay, 10000) / 1000) payload = {"origin":state.name, "sequence":sequence, "generated_at_ns":time.time_ns(), "nonce":secrets.token_hex(16), "path":target.path, "accept_language":self.headers.get("Accept-Language", ""), "healthy":healthy} if target.path == "/health": return self.send(200 if healthy else 503, payload) if target.path == "/state": return self.send(200, {**payload, "delay_ms":state.delay_ms, "ttl":state.ttl}) if target.path == "/": return self.send(200, """
Each uncached request generates a new sequence and nonce.
POST /control with JSON {"healthy":false,"delay_ms":0} to test failover/recovery.
""", "text/html; charset=utf-8") if target.path == "/large": prefix = (json.dumps(payload) + "\n").encode() body = (prefix + b"RustProxy compression and cache fixture.\n" * (size // 30 + 1))[:size] return self.send(200, body, "text/plain; charset=utf-8", f"public, max-age={ttl}") if target.path.startswith("/status/"): try: status = int(target.path.rsplit("/", 1)[1]) if status not in [200, 204, 400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504]: raise ValueError() except ValueError: return self.send(400, {"error":"unsupported status"}) return self.send(status, payload, extra={"WWW-Authenticate":'Basic realm="mock"'} if status == 401 else None) if target.path == "/cache": return self.send(200, payload, control=f"public, max-age={ttl}", extra={"Vary":"Accept-Language"}) if target.path == "/private": return self.send(200, payload, control="private, max-age=60") if target.path == "/cookie": return self.send(200, payload, control=f"public, max-age={ttl}", extra={"Set-Cookie":"mock-session=demo; HttpOnly; SameSite=Lax; Path=/"}) self.send(200 if target.path in ("/nocache", "/slow") else 404, payload) def do_POST(self): # Always consume or close a body before reusing an HTTP/1.1 connection. if self.headers.get("Transfer-Encoding") or len(self.headers.get_all("Content-Length", [])) != 1: self.close_connection = True return self.send(411, {"error":"one Content-Length is required"}) try: length = int(self.headers["Content-Length"]) if not 0 <= length <= 4096: raise ValueError() raw = self.rfile.read(length) if len(raw) != length: raise ValueError() data = json.loads(raw) if self.path != "/control" or not isinstance(data, dict) or set(data) - {"healthy", "delay_ms"}: raise ValueError() if "healthy" in data and not isinstance(data["healthy"], bool): raise ValueError() if "delay_ms" in data and (type(data["delay_ms"]) is not int or not 0 <= data["delay_ms"] <= 10000): raise ValueError() except (ValueError, TimeoutError): self.close_connection = True return self.send(400, {"error":"expected healthy: boolean and/or delay_ms: 0..10000"}) with self.server.state.lock: for key, value in data.items(): setattr(self.server.state, key, value) self.send(200, {"updated":True}) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=9001) parser.add_argument("--name", default="origin-a") parser.add_argument("--ttl", type=int, default=30) parser.add_argument("--allow-remote", action="store_true", help="Explicitly expose this development-only server on a non-loopback IPv4 address") args = parser.parse_args() try: ip = ipaddress.ip_address(args.host) except ValueError: parser.error("--host must be an IPv4 address") if ip.version != 4: parser.error("this development helper accepts IPv4 bindings only") if not ip.is_loopback and not args.allow_remote: parser.error("non-loopback binding requires --allow-remote; /control is unauthenticated") if not 0 <= args.port <= 65535 or not 0 <= args.ttl <= 604800: parser.error("invalid port or TTL") if not args.name or len(args.name) > 64 or any(ord(c) < 32 or ord(c) > 126 for c in args.name): parser.error("--name must contain 1..64 printable ASCII characters") server = Server((args.host, args.port), State(args.name, args.ttl)) print(f"Mock origin {args.name}: http://{args.host}:{server.server_port} (development only)", flush=True) print("Endpoints: /cache /nocache /private /cookie /large /slow /health /status/503; POST /control", flush=True) try: server.serve_forever(poll_interval=0.2) except KeyboardInterrupt: pass finally: server.server_close() if __name__ == "__main__": main()