#!/usr/bin/env python3 """Compile both real HTTP utilities and exercise native deadlines on loopback.""" import http.server import json import os from pathlib import Path import socketserver import subprocess import tempfile import threading import time ROOT = Path(__file__).resolve().parents[1] class Handler(http.server.BaseHTTPRequestHandler): def log_message(self, *_): pass def do_POST(self): body = self.rfile.read(int(self.headers.get("Content-Length", 0))) assert body == b'{"event":"heartbeat"}' assert self.headers.get("Content-Type") == "application/json" assert self.headers.get("Authorization") == "Bearer test-only" if self.path == "/stall": time.sleep(2) return if self.path == "/partial": self.send_response(200) self.send_header("Content-Length", "100") self.end_headers() self.wfile.write(b"{") self.wfile.flush() time.sleep(2) return if self.path == "/slow": time.sleep(0.1) status = int(self.path[1:]) if self.path[1:].isdigit() else 200 payload = b'{"ok":true}' if status == 200 else b'{"ok":false}' self.send_response(status) self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) class StallTLS(socketserver.BaseRequestHandler): def handle(self): # Accept TCP but never finish TLS: the connection deadline must fire. time.sleep(6) class Server(socketserver.ThreadingTCPServer): daemon_threads = True def main(): with Server(("127.0.0.1", 0), Handler) as http, Server(("127.0.0.1", 0), StallTLS) as tls, tempfile.TemporaryDirectory(prefix="tws-http-tests-") as temp: for server in (http, tls): threading.Thread(target=server.serve_forever, daemon=True).start() try: for game in ("t5", "t6"): source = ROOT / game / "plugins" / f"tws-{game}-gsc-utils" / "src" / "utils" executable = Path(temp) / game subprocess.run([os.environ.get("CXX", "c++"), "-std=c++20", "-Wall", "-Wextra", "-Werror", "-I", str(source), str(ROOT / "tools/tests/http_probe.cpp"), str(source / "http.cpp"), "-lcurl", "-o", str(executable)], check=True) base = f"http://127.0.0.1:{http.server_address[1]}" env = {**os.environ, "NO_PROXY": "*", "no_proxy": "*"} def probe(path, connect="1000", total="1000"): url = path if "://" in path else base + path completed = subprocess.run([str(executable), url, connect, total], env=env, capture_output=True, text=True, check=True, timeout=8) return json.loads(completed.stdout) normal = probe("/200", "-", "-") assert normal["curl_code"] == 0 and normal["http_status"] == 200 and normal["body_size"] == 11, normal assert 0 <= normal["dns_ms"] <= normal["connect_ms"] <= normal["first_byte_ms"] <= normal["total_ms"], normal for status in (401, 429, 500): rejected = probe(f"/{status}") assert rejected["curl_code"] == 0 and rejected["http_status"] == status and rejected["body_size"] == 12, rejected for path, status in (("/stall", 0), ("/partial", 200)): timed_out = probe(path, total="200") assert timed_out["curl_code"] == 28 and timed_out["http_status"] == status, timed_out assert 150 <= timed_out["total_ms"] < 1500 and timed_out["wall_ms"] < 1500, timed_out assert timed_out["body_size"] == 0, timed_out if status: assert timed_out["first_byte_ms"] > 0, timed_out assert probe("/200")["curl_code"] == 0 # The next request completes after the stall. slow = probe("/slow") assert slow["curl_code"] == 0 and slow["first_byte_ms"] >= 90, slow tls_url = f"https://127.0.0.1:{tls.server_address[1]}/" stalled_connect = probe(tls_url, connect="150", total="1000") assert stalled_connect["curl_code"] == 28 and 100 <= stalled_connect["total_ms"] < 800, stalled_connect # Total timeout must also bound connection setup when it is shorter. total_connect = probe(tls_url, connect="1000", total="150") assert total_connect["curl_code"] == 28 and 100 <= total_connect["total_ms"] < 800, total_connect for connect, total in (("0", "100"), ("100", "0"), ("-1", "100")): invalid = probe("/200", connect, total) assert invalid["curl_code"] == 43 and invalid["http_status"] == 0, invalid print(f"PASS {game}: POST body/headers, HTTP errors, stalled/partial responses, recovery, TLS/connect and total deadlines, invalid limits", flush=True) finally: http.shutdown() tls.shutdown() if __name__ == "__main__": main()