from __future__ import annotations import hmac import json import secrets from http import HTTPStatus from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any from urllib.parse import urlsplit from .domain import Zone MAX_BODY_BYTES = 64 * 1024 def parse_bind(value: str) -> tuple[str, int]: parsed = urlsplit(f"//{value}") host = parsed.hostname try: port = parsed.port except ValueError as exc: raise ValueError("invalid demo bind address") from exc if host not in {"127.0.0.1", "localhost", "::1"} or port is None or not 1 <= port <= 65535: raise ValueError("Brain demo must bind to an explicit loopback address and valid port") return host, port def index_path() -> Path: return Path(__file__).resolve().parents[2] / "docs" / "design" / "brain" / "index.html" class DemoHTTPServer(ThreadingHTTPServer): daemon_threads = True def __init__(self, address: tuple[str, int], engine: Any, token: str, html: str) -> None: self.engine = engine self.demo_token = token self.html = html super().__init__(address, DemoHandler) class DemoHandler(BaseHTTPRequestHandler): server: DemoHTTPServer def log_message(self, _format: str, *_args: object) -> None: # Do not echo request paths or headers; URL credentials never reach HTTP paths. return def _security_headers(self) -> None: self.send_header("X-Content-Type-Options", "nosniff") self.send_header("X-Frame-Options", "DENY") self.send_header("Referrer-Policy", "no-referrer") self.send_header("Cache-Control", "no-store") self.send_header( "Content-Security-Policy", "default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; " "img-src 'self' data: blob:; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'", ) def _send_bytes(self, status: int, content_type: str, body: bytes) -> None: self.send_response(status) self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(len(body))) self._security_headers() self.end_headers() self.wfile.write(body) def _send_json(self, status: int, payload: object) -> None: body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") self._send_bytes(status, "application/json; charset=utf-8", body) def do_GET(self) -> None: # noqa: N802 path = urlsplit(self.path).path if path in {"/", "/brain-demo"}: body = self.server.html.replace("__BRAIN_DEMO_TOKEN__", self.server.demo_token).encode("utf-8") self._send_bytes(HTTPStatus.OK, "text/html; charset=utf-8", body) return if path == "/healthz": self._send_json(HTTPStatus.OK, {"status": "ok"}) return if path == "/api/v1/state": self._send_json(HTTPStatus.OK, self.server.engine.state()) return if path == "/api/v1/frame.jpg": frame = self.server.engine.frame_jpeg() if frame is None: self._send_json(HTTPStatus.SERVICE_UNAVAILABLE, {"code": "frame_not_ready"}) return self._send_bytes(HTTPStatus.OK, "image/jpeg", frame) return self._send_json(HTTPStatus.NOT_FOUND, {"code": "not_found"}) def do_PUT(self) -> None: # noqa: N802 if urlsplit(self.path).path != "/api/v1/zones/active": self._send_json(HTTPStatus.NOT_FOUND, {"code": "not_found"}) return raw_length = self.headers.get("Content-Length") try: length = int(raw_length or "-1") except ValueError: length = -1 if length < 0 or length > MAX_BODY_BYTES: self._send_json(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, {"code": "body_too_large"}) return body = self.rfile.read(length) supplied = self.headers.get("X-Brain-Demo-Token", "") if not hmac.compare_digest(supplied, self.server.demo_token): self._send_json(HTTPStatus.FORBIDDEN, {"code": "forbidden"}) return try: payload = json.loads(body.decode("utf-8")) zone: Zone = self.server.engine.update_zone(payload) except (UnicodeError, json.JSONDecodeError, ValueError): self._send_json(HTTPStatus.BAD_REQUEST, {"code": "invalid_zone"}) return self._send_json( HTTPStatus.OK, { "id": zone.zone_id, "name": zone.name, "version": zone.version, "points": [{"x": point.x, "y": point.y} for point in zone.points], }, ) def build_server(engine: Any, bind: str = "127.0.0.1:8090", token: str | None = None) -> DemoHTTPServer: html = index_path().read_text(encoding="utf-8") return DemoHTTPServer(parse_bind(bind), engine, token or secrets.token_urlsafe(32), html) def serve(engine: Any, bind: str) -> None: server = build_server(engine, bind) engine.start() try: server.serve_forever(poll_interval=0.25) finally: server.server_close() engine.stop()