# -*- coding: utf-8 -*- """验证中转能正确解析 chunked 请求体,且不再污染后续请求。 背景(这次踩的坑): Cloudflare 转发时可能用 Transfer-Encoding: chunked。 中转原来只读 Content-Length,遇到 chunked 就返回 b"" 且**不读数据**, 残留的 "0\r\n\r\n" 被当成下一个请求行 -> "Bad request syntax ('0')" -> 连接错位 -> 隧道大面积 502。 这个测试直接用裸 socket 发 chunked,最能说明问题。 """ from __future__ import annotations import json import os import shutil import socket import subprocess import sys import tempfile import time from pathlib import Path ROOT = Path(__file__).resolve().parent.parent PORT = 8130 passed = 0 failed = 0 def check(name, ok, detail=""): global passed, failed if ok: passed += 1 print(" [PASS] " + name) else: failed += 1 print(" [FAIL] " + name + " " + str(detail)) def raw_request(sock: socket.socket, payload: bytes) -> str: sock.sendall(payload) sock.settimeout(8) data = b"" try: while b"\r\n\r\n" not in data: piece = sock.recv(4096) if not piece: break data += piece # 再读 body head, _, rest = data.partition(b"\r\n\r\n") length = 0 for line in head.decode("utf-8", "replace").splitlines(): if line.lower().startswith("content-length:"): length = int(line.split(":")[1].strip()) while len(rest) < length: piece = sock.recv(4096) if not piece: break rest += piece except socket.timeout: pass # 注意:要把补读进来的 rest 拼回去。 # 之前这里直接返回 data —— body 单独到达时(机器忙的时候就会) # 读到的 body 根本没被返回,断言全挂。 return (head + b"\r\n\r\n" + rest).decode("utf-8", "replace") def chunked(body: bytes) -> bytes: """把 body 编成 chunked 形式(模拟 Cloudflare 的转发)。""" out = b"" step = 24 for index in range(0, len(body), step): piece = body[index:index + step] out += f"{len(piece):x}\r\n".encode() + piece + b"\r\n" out += b"0\r\n\r\n" return out def main() -> int: work = Path(tempfile.mkdtemp(prefix="chunked_")) relay = None try: data = work / "d" data.mkdir(parents=True, exist_ok=True) env = dict(os.environ) env.update({"RELAY_PORT": str(PORT), "RELAY_BIND": "127.0.0.1", "RELAY_DATA": str(data), "PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1"}) log_path = work / "relay.log" log_file = open(log_path, "wb") relay = subprocess.Popen( [sys.executable, "-u", str(ROOT / "relay" / "relay.py")], env=env, stdout=log_file, stderr=subprocess.STDOUT) up = False for _ in range(40): time.sleep(0.5) try: probe = socket.create_connection(("127.0.0.1", PORT), 2) probe.sendall(b"GET /v1/health HTTP/1.1\r\n" b"Host: 127.0.0.1\r\nConnection: close\r\n\r\n") if b"200" in probe.recv(200): up = True probe.close() break except Exception: # noqa: BLE001 continue check("本地 relay 起来了", up) if not up: return 1 body = json.dumps({"deviceId": "chunk-1", "code": "135791", "name": "分块机", "version": "1.4.0"}, ensure_ascii=False).encode("utf-8") print() print("一、chunked 的 POST 能不能被正确解析") sock = socket.create_connection(("127.0.0.1", PORT), 8) head = (b"POST /v1/agent/register HTTP/1.1\r\n" b"Host: 127.0.0.1\r\n" b"Content-Type: application/json\r\n" b"Transfer-Encoding: chunked\r\n" b"User-Agent: LanRemote/1.4.0\r\n" b"Connection: keep-alive\r\n\r\n") reply = raw_request(sock, head + chunked(body)) print(f" 响应: {reply.split(chr(13) + chr(10) + chr(13) + chr(10))[-1][:110]}") check("chunked POST 不是 400", "400" not in reply.split("\r\n")[0], reply.split("\r\n")[0]) check("chunked POST 登记成功", '"ok": true' in reply or '"ok":true' in reply, reply[-160:]) check("没有 Bad request syntax", "Bad request syntax" not in reply) check("响应里带回了配对码", "135791" in reply, reply[-160:]) print() print("二、同一条连接上紧接着再发一个普通请求(验证没有错位)") normal = json.dumps({"deviceId": "chunk-2", "code": "246802", "name": "普通机", "version": "1.4.0"}, ensure_ascii=False).encode("utf-8") head2 = (b"POST /v1/agent/register HTTP/1.1\r\n" b"Host: 127.0.0.1\r\n" b"Content-Type: application/json\r\n" + ("Content-Length: %d\r\n" % len(normal)).encode() + b"User-Agent: LanRemote/1.4.0\r\n" + b"Connection: close\r\n\r\n") reply2 = raw_request(sock, head2 + normal) print(f" 响应: {reply2.split(chr(13) + chr(10) + chr(13) + chr(10))[-1][:110]}") check("第二个请求也成功(连接没被污染)", '"ok": true' in reply2 or '"ok":true' in reply2, reply2[-160:]) check("第二个响应是 200", " 200 " in reply2.split("\r\n")[0], reply2.split("\r\n")[0]) try: sock.close() except Exception: # noqa: BLE001 pass print() print("三、relay 日志里不该有 Bad request syntax") time.sleep(0.6) log_file.flush() text = log_path.read_text(encoding="utf-8", errors="replace") bad = text.count("Bad request syntax") check("没有 Bad request syntax", bad == 0, bad) if bad: for line in text.splitlines(): if "Bad request" in line: print(f" {line.strip()[:130]}") print() print("四、超大的 Content-Length 要被读掉、不能污染连接") sock2 = socket.create_connection(("127.0.0.1", PORT), 8) huge = (b"POST /v1/agent/register HTTP/1.1\r\n" b"Host: 127.0.0.1\r\n" b"Content-Type: application/json\r\n" b"Content-Length: 999999999\r\n" b"Connection: close\r\n\r\n") sock2.sendall(huge + b"x" * 100) try: sock2.recv(200) except Exception: # noqa: BLE001 pass try: sock2.close() except Exception: # noqa: BLE001 pass time.sleep(0.5) # 再来一个正常请求,确认服务还活着 probe = socket.create_connection(("127.0.0.1", PORT), 5) probe.sendall(b"GET /v1/health HTTP/1.1\r\nHost: 127.0.0.1\r\n" b"Connection: close\r\n\r\n") alive = b"200" in probe.recv(300) probe.close() check("超大请求之后服务还正常", alive) print() print("=" * 58) print(f"通过 {passed} 项,失败 {failed} 项") return 0 if failed == 0 else 1 finally: if relay is not None: try: relay.terminate() relay.wait(timeout=8) except Exception: # noqa: BLE001 try: relay.kill() except Exception: # noqa: BLE001 pass time.sleep(0.5) shutil.rmtree(work, ignore_errors=True) if __name__ == "__main__": sys.exit(main())