Files

113 lines
3.8 KiB
Python

"""验证 keep-alive 连接上「不读 body 的接口」不会污染下一个请求。
背景(真实 bug):`/api/devices/connect` 的 handler 从不读请求体。
在 HTTP/1.1 keep-alive 连接上,没读走的 `{}` 会留在 socket 缓冲区里,
被 BaseHTTPRequestHandler 当成下一个请求的请求行,于是登录到:
501 Unsupported method ('{}POST')
而且是**间歇性**的(取决于客户端是否复用连接),非常难查。
修法:do_POST 进入路由前先把 body 读完缓存起来。
用法:先起一个 mock 服务,再跑本脚本。
.venv\\Scripts\\python.exe tools\\_keepalive_probe.py --port 8855
"""
from __future__ import annotations
import argparse
import json
import socket
import sys
def build_post(host: str, port: int, path: str, body: str) -> bytes:
payload = body.encode("utf-8")
head = (
f"POST {path} HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
f"Content-Type: application/json\r\n"
f"Content-Length: {len(payload)}\r\n"
f"Connection: keep-alive\r\n"
f"\r\n"
).encode("ascii")
return head + payload
def recv_response(sock: socket.socket) -> str:
"""读一个完整响应(按 Content-Length)。"""
data = b""
while b"\r\n\r\n" not in data:
chunk = sock.recv(4096)
if not chunk:
return data.decode("utf-8", "replace")
data += chunk
head, _, rest = data.partition(b"\r\n\r\n")
length = 0
for line in head.split(b"\r\n"):
if line.lower().startswith(b"content-length:"):
length = int(line.split(b":", 1)[1].strip())
break
while len(rest) < length:
chunk = sock.recv(4096)
if not chunk:
break
rest += chunk
return (head + b"\r\n\r\n" + rest).decode("utf-8", "replace")
def status_of(response: str) -> int:
first = response.split("\r\n", 1)[0]
parts = first.split()
try:
return int(parts[1])
except (IndexError, ValueError):
return -1
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=8855)
args = parser.parse_args()
failures: list[str] = []
def check(name: str, ok: bool, detail: str = "") -> None:
print(f" [{'PASS' if ok else 'FAIL'}] {name}" + (f" — {detail}" if detail else ""))
if not ok:
failures.append(name)
with socket.create_connection((args.host, args.port), timeout=15) as sock:
# 第 1 个请求:body 为 {} 且服务端 handler 不读它
sock.sendall(build_post(args.host, args.port, "/api/devices/connect", "{}"))
r1 = recv_response(sock)
s1 = status_of(r1)
check("第 1 个请求(connect) 成功", s1 in (200, 409), f"status={s1}")
print(f" 第 1 个响应前 120 字符:{r1[:120]!r}")
# 第 2 个请求:如果残留的 {} 没被排空,请求行会被污染 → 501
body2 = json.dumps({"lat": 39.9087, "lng": 116.3975})
sock.sendall(build_post(args.host, args.port, "/api/location", body2))
r2 = recv_response(sock)
s2 = status_of(r2)
check("同连接第 2 个请求(location) 未被污染", s2 == 200, f"status={s2}")
if s2 != 200:
print(f" 第 2 个响应:{r2[:300]!r}")
# 第 3 个请求:再压一次,确认连续多请求也稳
sock.sendall(build_post(args.host, args.port, "/api/location", body2))
r3 = recv_response(sock)
s3 = status_of(r3)
check("同连接第 3 个请求仍然正常", s3 == 200, f"status={s3}")
print()
if failures:
print(f"失败 {len(failures)} 项:{failures}")
return 1
print("全部通过:keep-alive 上不会串请求")
return 0
if __name__ == "__main__":
sys.exit(main())