"""Threaded standard-library HTTP adapter for the portal application.""" from __future__ import annotations from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Type from arr_web.app import PortalApplication from arr_web.contracts import MAX_UPLOAD_BYTES, PortalError, Response, failure SECURITY_HEADERS = { "Content-Security-Policy": ( "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; " "connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; " "form-action 'self'" ), "Referrer-Policy": "no-referrer", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "Permissions-Policy": "camera=(), microphone=(), geolocation=()", "Cross-Origin-Resource-Policy": "same-origin", } def handler_for(application: PortalApplication) -> Type[BaseHTTPRequestHandler]: class PortalHandler(BaseHTTPRequestHandler): server_version = "ARRPortal/1.0" def do_GET(self) -> None: # noqa: N802 self._dispatch(b"") def do_POST(self) -> None: # noqa: N802 self._dispatch_with_body() def do_PATCH(self) -> None: # noqa: N802 self._dispatch_with_body() def do_DELETE(self) -> None: # noqa: N802 self._dispatch_with_body() def _dispatch_with_body(self) -> None: raw_length = self.headers.get("Content-Length") if raw_length is None: self._write( Response.json( 411, failure(PortalError("CONTENT_LENGTH_REQUIRED", "请求长度缺失", 411)), ) ) return try: length = int(raw_length) except ValueError: length = -1 if length < 0 or length > MAX_UPLOAD_BYTES: self._write( Response.json( 413, failure(PortalError("REQUEST_TOO_LARGE", "请求内容过大", 413)), ) ) return self._dispatch(self.rfile.read(length)) def _dispatch(self, body: bytes) -> None: response = application.handle( self.command, self.path, {key: value for key, value in self.headers.items()}, body, client_id=str(self.client_address[0]), ) self._write(response) def _write(self, response: Response) -> None: self.send_response(response.status) self.send_header("Content-Type", response.content_type) self.send_header("Content-Length", str(len(response.body))) for key, value in SECURITY_HEADERS.items(): self.send_header(key, value) for key, value in response.headers.items(): self.send_header(key, value) self.end_headers() if self.command != "HEAD": self.wfile.write(response.body) def log_message(self, format: str, *args: object) -> None: # Keep request logs path-only; never print request bodies, cookies, or upload names. super().log_message("%s %s", self.command, self.path.split("?", 1)[0]) return PortalHandler def serve(application: PortalApplication, host: str, port: int) -> None: server = ThreadingHTTPServer((host, port), handler_for(application)) try: server.serve_forever(poll_interval=0.25) finally: server.server_close()