#!/usr/bin/env python3 """Drop one complete HTTPS response after proving the upstream produced it. This is a test-only fault proxy for the serviceification walking slice. It buffers exactly one Content-Length request before contacting the upstream, forwards that complete request, drains exactly one complete response, records the response status atomically, and sends no response bytes to the caller. """ from __future__ import annotations import argparse import http.client import json import os import socket import ssl from pathlib import Path from typing import BinaryIO MAX_HEADER_BYTES = 64 * 1024 MAX_BODY_BYTES = 2 * 1024 * 1024 def write_state(path: Path, phase: str, **fields: object) -> None: payload = {"schema": "nexuscrm.response-blackhole-state/v1", "phase": phase} payload.update(fields) temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp") descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) try: with os.fdopen(descriptor, "w", encoding="utf-8") as output: json.dump(payload, output, sort_keys=True, separators=(",", ":")) output.write("\n") output.flush() os.fsync(output.fileno()) os.replace(temporary, path) finally: try: temporary.unlink() except FileNotFoundError: pass def receive_content_length_request(stream: BinaryIO) -> bytes: buffered = bytearray() while b"\r\n\r\n" not in buffered: block = stream.recv(65536) if not block: raise ValueError("client_closed_before_headers") buffered.extend(block) if b"\r\n\r\n" not in buffered and len(buffered) > MAX_HEADER_BYTES: raise ValueError("headers_too_large") header, body = bytes(buffered).split(b"\r\n\r\n", 1) if len(header) > MAX_HEADER_BYTES: raise ValueError("headers_too_large") lines = header.split(b"\r\n") if not lines or len(lines[0].split(b" ")) != 3: raise ValueError("invalid_request_line") content_lengths: list[int] = [] transfer_encoding = False for line in lines[1:]: if b":" not in line: raise ValueError("invalid_header") name, value = line.split(b":", 1) normalized_name = name.strip().lower() normalized_value = value.strip() if normalized_name == b"content-length": try: content_lengths.append(int(normalized_value, 10)) except ValueError as error: raise ValueError("invalid_content_length") from error elif normalized_name == b"transfer-encoding": transfer_encoding = True if transfer_encoding: raise ValueError("transfer_encoding_not_supported") if len(content_lengths) != 1: raise ValueError("one_content_length_required") content_length = content_lengths[0] if content_length < 0 or content_length > MAX_BODY_BYTES: raise ValueError("body_size_out_of_range") if len(body) > content_length: raise ValueError("multiple_requests_not_supported") while len(body) < content_length: block = stream.recv(min(65536, content_length - len(body))) if not block: raise ValueError("client_closed_before_body") body += block return header + b"\r\n\r\n" + body def serve_once( listen_port: int, upstream_port: int, server_name: str, certificate: Path, private_key: Path, state_path: Path, ) -> None: server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) server_context.load_cert_chain(certificate, private_key) upstream_context = ssl.create_default_context(cafile=certificate) try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener: listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listener.bind(("127.0.0.1", listen_port)) listener.listen(1) write_state(state_path, "listening") raw_client, _ = listener.accept() with server_context.wrap_socket(raw_client, server_side=True) as client: client.settimeout(15) request = receive_content_length_request(client) write_state( state_path, "request_received", request_bytes=len(request), ) raw_upstream = socket.create_connection( ("127.0.0.1", upstream_port), timeout=5 ) with upstream_context.wrap_socket( raw_upstream, server_hostname=server_name ) as upstream: upstream.settimeout(60) upstream.sendall(request) response = http.client.HTTPResponse(upstream) response.begin() while response.read(65536): pass write_state( state_path, "response_received", upstream_http_status=response.status, ) except Exception as error: write_state(state_path, "failed", error_type=type(error).__name__) raise def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--listen-port", type=int, required=True) parser.add_argument("--upstream-port", type=int, required=True) parser.add_argument("--server-name", required=True) parser.add_argument("--certificate", type=Path, required=True) parser.add_argument("--private-key", type=Path, required=True) parser.add_argument("--state", type=Path, required=True) return parser.parse_args() def main() -> None: args = parse_args() serve_once( args.listen_port, args.upstream_port, args.server_name, args.certificate, args.private_key, args.state, ) if __name__ == "__main__": main()