"""Adversarial HTTP boundary tests for the loopback HUX foundation service.""" from __future__ import annotations import json import socket import sys import threading from http.client import HTTPConnection from pathlib import Path ROOT = Path(__file__).resolve().parents[2] FOUNDATION = ROOT / "dockerfiles" / "hermes-hux-foundation" if str(FOUNDATION) not in sys.path: sys.path.insert(0, str(FOUNDATION)) from hux import contracts # noqa: E402 from hux import http as hux_http # noqa: E402 from hux.http import DEFAULT_REQUEST_TIMEOUT_SECONDS, RateLimiter, Router, serve # noqa: E402 from hux.server import build_router # noqa: E402 ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"]) HEADERS = { "X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk", } def _router(tmp_path: Path, **extra: str) -> Router: return build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk", **extra}) def _start(router: Router): server = serve(router, "127.0.0.1", 0) threading.Thread(target=server.serve_forever, daemon=True).start() return server def _raw_request(address: tuple[str, int], request: bytes) -> bytes: """Send one HTTP/1.0 request and return its complete close-delimited response.""" with socket.create_connection(address, timeout=1) as client: client.sendall(request) chunks = [] while chunk := client.recv(4096): chunks.append(chunk) return b"".join(chunks) def test_declared_body_is_rejected_before_socket_read(tmp_path): """An oversized Content-Length receives 413 without waiting for the claimed body.""" server = _start(_router(tmp_path)) try: conn = HTTPConnection("127.0.0.1", server.server_address[1], timeout=1) conn.putrequest("POST", "/hux/v1/capabilities") conn.putheader("Content-Length", str(1024 * 1024 + 1)) conn.endheaders() reply = conn.getresponse() body = json.loads(reply.read()) assert (reply.status, body["code"]) == (413, "too_large") assert reply.getheader("Cache-Control") == "no-store" finally: server.shutdown() server.server_close() def test_incomplete_body_times_out_and_transfer_encoding_is_rejected(tmp_path): """Accepted sockets have a deadline, and unsupported framing fails closed.""" server = _start(_router(tmp_path, HUX_REQUEST_TIMEOUT_SECONDS="0.1")) try: reply = _raw_request( server.server_address, b"POST /hux/v1/capabilities HTTP/1.0\r\nContent-Length: 2\r\n\r\n{", ) assert b" 400 " in reply and b"request body is incomplete or timed out" in reply conn = HTTPConnection("127.0.0.1", server.server_address[1], timeout=1) conn.putrequest("POST", "/hux/v1/capabilities") conn.putheader("Transfer-Encoding", "chunked") conn.endheaders() assert conn.getresponse().status == 400 finally: server.shutdown() server.server_close() def test_malformed_duplicate_and_absurd_content_lengths_fail_closed(tmp_path): """Ambiguous or computationally large length headers never reach ``int`` or a body allocation.""" server = _start(_router(tmp_path)) try: malformed = _raw_request( server.server_address, b"POST /hux/v1/capabilities HTTP/1.0\r\nContent-Length: nope\r\n\r\n", ) duplicate = _raw_request( server.server_address, b"POST /hux/v1/capabilities HTTP/1.0\r\nContent-Length: 0\r\nContent-Length: 0\r\n\r\n", ) absurd = _raw_request( server.server_address, b"POST /hux/v1/capabilities HTTP/1.0\r\nContent-Length: 99999999999\r\n\r\n", ) assert b" 400 " in malformed and b" 400 " in duplicate assert b" 413 " in absurd finally: server.shutdown() server.server_close() def test_rate_limit_is_per_subject_and_method_class(tmp_path): """Read and write buckets are distinct and return a bounded Retry-After.""" router = _router(tmp_path, HUX_READS_PER_MINUTE="1", HUX_WRITES_PER_MINUTE="1") first = router.dispatch("GET", "/hux/v1/capabilities", HEADERS, b"") limited = router.dispatch("GET", "/hux/v1/capabilities", HEADERS, b"") write = router.dispatch("POST", "/hux/v1/capabilities", HEADERS, b"{}") assert first.status == 200 assert (limited.status, limited.body["code"]) == (429, "rate_limited") assert 1 <= int(limited.headers["Retry-After"]) <= 60 assert write.status == 405, "method routing precedes the write rate bucket" live = _start(_router(tmp_path / "live", HUX_READS_PER_MINUTE="1")) try: conn = HTTPConnection("127.0.0.1", live.server_address[1], timeout=1) conn.request("GET", "/hux/v1/capabilities", headers=HEADERS) conn.getresponse().read() conn.request("GET", "/hux/v1/capabilities", headers=HEADERS) reply = conn.getresponse() reply.read() assert reply.status == 429 and reply.getheader("Retry-After") assert reply.getheader("Cache-Control") == "no-store" finally: live.shutdown() live.server_close() def test_limiter_expires_windows_and_invalid_settings_use_safe_defaults(tmp_path, monkeypatch): """Expired subjects are evicted and malformed operator settings cannot disable bounds.""" moments = iter((0.0, 0.0, 61.0)) limiter = RateLimiter(1, 1, clock=lambda: next(moments)) assert limiter.check("a", "GET") is None assert limiter.check("a", "GET") == 60 assert limiter.check("a", "GET") is None monkeypatch.setattr(hux_http, "MAX_RATE_BUCKETS", 1) bounded = RateLimiter(2, 2, clock=lambda: 0.0) assert bounded.check("a", "GET") is None assert bounded.check("b", "GET") == 60 router = _router( tmp_path, HUX_READS_PER_MINUTE="nan", HUX_WRITES_PER_MINUTE="0", HUX_REQUEST_TIMEOUT_SECONDS="forever", ) assert router.rate_limiter.limits == {"read": 300, "write": 30} assert router.request_timeout == DEFAULT_REQUEST_TIMEOUT_SECONDS def test_health_and_json_are_no_store_and_health_refuses_bodies(tmp_path): """Sensitive JSON never enters browser caches; health cannot be used for body smuggling.""" server = _start(_router(tmp_path)) try: conn = HTTPConnection("127.0.0.1", server.server_address[1], timeout=1) conn.request("GET", "/healthz") reply = conn.getresponse() reply.read() assert reply.getheader("Cache-Control") == "no-store" conn.request("GET", "/healthz", body=b"x", headers={"Content-Length": "1"}) assert conn.getresponse().status == 413 finally: server.shutdown() server.server_close()