"""Loopback-only, fixed-upstream transport fixture. Never logs headers or payloads.""" import http.client import json import select import socket import socketserver import threading import time from pathlib import Path from urllib.parse import urlsplit, parse_qs ROOT = Path(__file__).parent HOST, PORT = 's101.local', 18080 lock = threading.Lock() state = dict(mode='pass', blocked_until=0, upgrades=0, active=0, negotiate=0, login_forwarded=0, faults=0, errors=0) channels = set() def snapshot(): with lock: return {k: v for k, v in state.items() if k != 'blocked_until'} class Handler(socketserver.BaseRequestHandler): def reply(self, code, body=b'', extra=()): headers = [('Content-Length', str(len(body))), ('Connection', 'close'), *extra] data = f'HTTP/1.1 {code} Result\r\n' + ''.join(f'{k}: {v}\r\n' for k, v in headers) self.request.sendall(data.encode('latin1') + b'\r\n' + body) def handle(self): upstream = None registered = False try: self.request.settimeout(25) incoming = b'' while b'\r\n\r\n' not in incoming: chunk = self.request.recv(4096) if not chunk: return incoming += chunk if len(incoming) > 65536: return head, body = incoming.split(b'\r\n\r\n', 1) lines = head.decode('latin1').split('\r\n') method, target, _ = lines[0].split(' ') headers = dict(line.split(':', 1) for line in lines[1:]) lower = {k.lower(): v.strip() for k, v in headers.items()} parsed = urlsplit(target) path = parsed.path if path == '/__status': self.reply(200, json.dumps(snapshot()).encode()) return if path == '/__control': mode = parse_qs(parsed.query).get('mode', [''])[0] if mode not in ('pass', '401', 'timeout', 'reset', 'drop', 'stop'): self.reply(400) return with lock: if mode == 'drop': state['blocked_until'] = time.monotonic() + 3 victims = list(channels) else: state['mode'] = mode victims = [] for peer in victims: try: peer.shutdown(socket.SHUT_RDWR) except OSError: pass self.reply(200, b'OK') if mode == 'stop': threading.Thread(target=self.server.shutdown, daemon=True).start() return if path == '/gamehub/negotiate': with lock: state['negotiate'] += 1 self.reply(403) return if path == '/gamehub' and lower.get('upgrade', '').lower() == 'websocket': with lock: blocked = time.monotonic() < state['blocked_until'] if blocked: self.reply(503) return upstream = socket.create_connection((HOST, PORT), timeout=8) rebuilt = [lines[0]] + [f'{k}: {v.strip()}' for k, v in headers.items() if k.lower() != 'host'] rebuilt.append(f'Host: {HOST}:{PORT}') upstream.sendall(('\r\n'.join(rebuilt) + '\r\n\r\n').encode('latin1') + body) response = b'' while b'\r\n\r\n' not in response: response += upstream.recv(4096) if len(response) > 65536: raise ValueError('oversized upgrade') self.request.sendall(response) if not response.startswith(b'HTTP/1.1 101 '): return with lock: state['upgrades'] += 1 state['active'] += 1 channels.add(self.request) registered = True peers = (self.request, upstream) while True: ready, _, _ = select.select(peers, [], [], 1) for src in ready: chunk = src.recv(65536) if not chunk: return (upstream if src is self.request else self.request).sendall(chunk) return if method != 'POST' or path not in ('/account/Login', '/playerdata/Get'): self.reply(403) return size = int(lower.get('content-length', '0')) if size > 2048 or 'transfer-encoding' in lower: self.reply(400) return while len(body) < size: chunk = self.request.recv(size - len(body)) if not chunk: return body += chunk payload = json.loads(body[:size]) if (path == '/account/Login' and payload not in ({'playerId': '101'}, {'playerId': '102'})) or (path == '/playerdata/Get' and payload != {}): self.reply(403) return with lock: mode = state['mode'] if path == '/account/Login' and mode in ('401', 'timeout', 'reset'): state['mode'] = 'pass' state['faults'] += 1 else: mode = 'pass' if mode == '401': self.reply(401) return if mode == 'timeout': time.sleep(18) self.reply(504) return if mode == 'reset': self.request.shutdown(socket.SHUT_RDWR) return conn = http.client.HTTPConnection(HOST, PORT, timeout=10) try: forward = {'Content-Type': 'application/json'} if 'cookie' in lower: forward['Cookie'] = lower['cookie'] conn.request('POST', path, body=body[:size], headers=forward) res = conn.getresponse() value = res.read() extra = [(k, v) for k, v in res.getheaders() if k.lower() in ('set-cookie', 'content-type')] self.reply(res.status, value, extra) if path == '/account/Login': with lock: state['login_forwarded'] += 1 finally: conn.close() except (OSError, ValueError): with lock: state['errors'] += 1 finally: if registered: with lock: channels.discard(self.request) state['active'] -= 1 if upstream: upstream.close() class Server(socketserver.ThreadingTCPServer): daemon_threads = True with Server(('127.0.0.1', 0), Handler) as server: (ROOT / 'network-proxy-address.json').write_text(json.dumps({'url': f'http://127.0.0.1:{server.server_address[1]}/'})) print('Loopback transport fixture started; fixed upstream s101.local:18080', flush=True) try: server.serve_forever(poll_interval=0.2) finally: (ROOT / 'network-proxy-final.json').write_text(json.dumps(snapshot(), indent=2))