#!/usr/bin/env python3 """Concurrency-bounded threaded HTTP server for the Hermes SCM broker.""" from __future__ import annotations import threading import time from http.server import ThreadingHTTPServer MAX_CONCURRENT_REQUESTS = 3 class _AbsoluteDeadlineReader: """Read header lines against one wall-clock deadline, not idle timeouts.""" def __init__(self, stream, connection): self._stream = stream self._connection = connection self._deadline: float | None = None def begin(self, timeout: float) -> None: self._deadline = time.monotonic() + timeout def end(self) -> None: self._deadline = None def readline(self, limit: int = -1) -> bytes: if self._deadline is None: return self._stream.readline(limit) value = bytearray() while limit < 0 or len(value) < limit: remaining = self._deadline - time.monotonic() if remaining <= 0: raise TimeoutError("absolute SCM header deadline exceeded") self._connection.settimeout(remaining) character = self._stream.read(1) if not character: break value.extend(character) if character == b"\n": break return bytes(value) def __getattr__(self, name: str): return getattr(self._stream, name) class AbsoluteHeaderDeadlineMixin: """Apply an absolute request-line plus header deadline to HTTP handlers.""" header_deadline_seconds = 10.0 def setup(self) -> None: super().setup() self._header_reader = _AbsoluteDeadlineReader(self.rfile, self.connection) self.rfile = self._header_reader def handle_one_request(self) -> None: try: self._header_reader.begin(self.header_deadline_seconds) try: self.raw_requestline = self.rfile.readline(65537) if len(self.raw_requestline) > 65536: self.requestline = "" self.request_version = "" self.command = "" self.send_error(414) return if not self.raw_requestline: self.close_connection = True return if not self.parse_request(): return finally: self._header_reader.end() method_name = "do_" + self.command if not hasattr(self, method_name): self.send_error(501, "Unsupported method") return getattr(self, method_name)() self.wfile.flush() except TimeoutError: self.close_connection = True class BoundedThreadingHTTPServer(ThreadingHTTPServer): """Cap active handlers so large Git exchanges cannot exhaust memory.""" daemon_threads = True block_on_close = True request_queue_size = 16 def __init__(self, *args, **kwargs): self._slots = threading.BoundedSemaphore(MAX_CONCURRENT_REQUESTS) super().__init__(*args, **kwargs) def process_request(self, request, client_address) -> None: if not self._slots.acquire(blocking=False): try: request.sendall( b"HTTP/1.1 503 Service Unavailable\r\n" b"Connection: close\r\nContent-Length: 0\r\n\r\n" ) finally: self.shutdown_request(request) return try: super().process_request(request, client_address) except Exception: self._slots.release() self.shutdown_request(request) raise def process_request_thread(self, request, client_address) -> None: try: super().process_request_thread(request, client_address) finally: self._slots.release()