"""Proxy streaming, status, header, request-ID and WebSocket tests.""" import copy import hashlib from http.client import HTTPConnection, HTTPResponse import json import re import socket import time from common import http def run(check, rust, peer): port = rust.reverse_port for name in ["small.html", "large.bin", "huge.bin"]: expected = (rust.fixture.site / name).read_bytes() s, h, b = http(port, "/" + name) check("proxy.get." + name, s == 200 and b == expected, status=s, bytes=len(b)) s, h, b = http(port, "/" + name, "HEAD") check("proxy.head." + name, s == 200 and b == b"" and h.get("content-length") == str(len(expected)), status=s) s, h, b = http(port, "/large.bin", headers={"Range": "bytes=0-127"}) check("proxy.range", s == 206 and b == bytes(range(128)), status=s, bytes=len(b)) s, h, b = http(port) s, h, b = http(port, headers={"If-None-Match": h["etag"]}) check("proxy.conditional304", s == 304 and b == b"", status=s) for status in [201, 400, 404, 500, 503]: s, h, b = http(port, f"/status/{status}") check(f"proxy.origin_status.{status}", s == status and b == b"origin status payload\n" and h.get("x-origin-fixture") == "yes", status=s) s, h, b = http(port, "/redirect") check("proxy.redirect", s == 302 and h.get("location") == "/small.html", status=s) payload = bytes(range(256)) * 4096 ids = [] for method in ["POST", "PUT"]: s, h, b = http(port, "/echo?query=preserved", method, {"Content-Type": "application/octet-stream", "X-Request-ID": "untrusted-client-id"}, payload) data = json.loads(b) request_id = data["headers"].get("x-request-id", "") ids.append(request_id) check("proxy.upload." + method, s == 200 and data["method"] == method and data["path"] == "/echo?query=preserved" and data["body_bytes"] == len(payload) and data["body_sha256"] == hashlib.sha256(payload).hexdigest(), status=s, bytes=data["body_bytes"]) check("proxy.request_id." + method, bool(re.fullmatch("[0-9a-f]{64}", request_id)), valid_64_hex=bool(re.fullmatch("[0-9a-f]{64}", request_id))) check("proxy.request_id_unique", len(set(ids)) == 2) s, h, b = http(port, "/echo", "GET", {"Content-Type": "application/octet-stream"}, b"nonempty-get-body") data = json.loads(b) check("proxy.nonempty_get_body_preserved", s == 200 and data["body_bytes"] == 17 and data["body_sha256"] == hashlib.sha256(b"nonempty-get-body").hexdigest(), status=s) peer.started_upload.clear() first, last = b"first streamed chunk" * 64, b"final streamed chunk" * 64 with socket.create_connection(("127.0.0.1", port), timeout=5) as sock: sock.sendall((f"POST /stream-upload HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\n" "Transfer-Encoding: chunked\r\nContent-Type: application/octet-stream\r\n\r\n").encode()) sock.sendall(f"{len(first):x}\r\n".encode() + first + b"\r\n") forwarded_before_end = peer.started_upload.wait(2) sock.sendall(f"{len(last):x}\r\n".encode() + last + b"\r\n0\r\n\r\n") response = HTTPResponse(sock) response.begin() data = json.loads(response.read()) check("proxy.chunked_streaming_upload", forwarded_before_end and response.status == 200 and data["body_sha256"] == hashlib.sha256(first + last).hexdigest(), first_chunk_arrived_before_end=forwarded_before_end, status=response.status) peer.release_stream.clear() connection = HTTPConnection("127.0.0.1", port, timeout=3) connection.request("GET", "/stream-download") started = time.monotonic() response = connection.getresponse() first = response.read(5) first_seconds = time.monotonic() - started peer.release_stream.set() last = response.read() connection.close() check("proxy.streaming_response", response.status == 200 and first == b"first" and last == b"last" and first_seconds < 2, first_chunk_seconds=first_seconds) peer.release_stream.clear() connection = HTTPConnection("127.0.0.1", port, timeout=3) connection.request("GET", "/stream-download") response = connection.getresponse() response.read(5) connection.close() peer.release_stream.set() s, h, b = http(port) check("proxy.client_disconnect_survives", s == 200, status=s) before = peer.count("POST", "/no-replay") s, h, b = http(port, "/no-replay", "POST", {"Content-Type": "application/octet-stream"}, b"write-once") after = peer.count("POST", "/no-replay") check("proxy.failed_post_not_replayed", s in (502, 503, 504) and after - before == 1, status=s, upstream_writes=after - before) config = copy.deepcopy(rust.base) config["headers"] = {"request": [{"op": "set", "name": "X-Fixture-Global", "value": "global"}], "response": [{"op": "set", "name": "X-Fixture-Global", "value": "response"}]} config["vhosts"][0]["headers"] = {"request": [{"op": "set", "name": "X-Fixture-Vhost", "value": "vhost"}]} config["vhosts"][0]["routes"] = [{"id": "echo", "path_prefix": "/echo", "headers": { "request": [{"op": "set", "name": "X-Fixture-Route", "value": "route"}], "response": [{"op": "set", "name": "X-Fixture-Route", "value": "response"}]}}] rust.apply(config, "proxy-headers") s, h, b = http(port, "/echo", headers={"Connection": "keep-alive, x-hop-fixture", "X-Hop-Fixture": "must-be-stripped"}) observed = json.loads(b)["headers"] check("proxy.header_scopes", s == 200 and observed.get("x-fixture-global") == "global" and observed.get("x-fixture-vhost") == "vhost" and observed.get("x-fixture-route") == "route" and h.get("x-fixture-global") == "response" and h.get("x-fixture-route") == "response", status=s) check("proxy.hop_by_hop_removed", "x-hop-fixture" not in observed) s, h, b = http(port, "/echo", headers={"X-Forwarded-For": "203.0.113.22", "X-Real-IP": "203.0.113.22"}) observed = json.loads(b)["headers"] check("proxy.untrusted_forwarded_ip", observed.get("x-real-ip") == "127.0.0.1" and "203.0.113.22" not in observed.get("x-forwarded-for", ""), observed_x_real_ip=observed.get("x-real-ip"), observed_x_forwarded_for=observed.get("x-forwarded-for")) rust.reset() with socket.create_connection(("127.0.0.1", port), timeout=4) as sock: sock.sendall((f"GET /ws HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nUpgrade: websocket\r\n" "Connection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Sec-WebSocket-Version: 13\r\n\r\n").encode()) buffer = b"" while b"\r\n\r\n" not in buffer: piece = sock.recv(4096) if not piece: break buffer += piece upgraded = buffer.startswith(b"HTTP/1.1 101") echoed = b"" if upgraded: message, mask = b"hello", b"\x11\x22\x33\x44" sock.sendall(bytes([0x81, 0x80 | len(message)]) + mask + bytes(c ^ mask[i % 4] for i, c in enumerate(message))) frame = sock.recv(2) if len(frame) == 2: echoed = sock.recv(frame[1] & 127) check("proxy.websocket_echo", upgraded and echoed == b"hello", upgraded=upgraded, bytes=len(echoed))