"""HTTP 请求解析与重组(共享: 本地代理解析, 云端套壳重组)。 保持最小够用集: 请求行 + headers(保留顺序/重复) + Content-Length body。 chunked 请求体与 obs-fold 不做(第一版), 遇到按无 body 处理。 """ from __future__ import annotations from dataclasses import dataclass, field _HEAD_END = b"\r\n\r\n" # hop-by-hop 头, 套壳重组时剔除(不跨连接转发) _HOP_BY_HOP = { "connection", "keep-alive", "proxy-connection", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", } @dataclass class HttpRequest: method: str path: str host: str headers: list[tuple[str, str]] = field(default_factory=list) body: bytes = b"" def header(self, name: str) -> str | None: low = name.lower() for key, value in self.headers: if key.lower() == low: return value return None def without_hop_by_hop(self) -> list[tuple[str, str]]: out: list[tuple[str, str]] = [] for key, value in self.headers: if key.lower() not in _HOP_BY_HOP and key.lower() != "host": out.append((key, value)) return out def parse_request_head(data: bytes) -> tuple[HttpRequest, int] | None: """解析请求头部区, 返回 (请求, 头部区字节数); 数据不足/畸形返回 None。""" idx = data.find(_HEAD_END) if idx < 0: return None head = data[:idx] lines = head.split(b"\r\n") if not lines or len(lines[0].split(b" ")) != 3: return None method_b, path_b, _ver_b = lines[0].split(b" ", 2) headers: list[tuple[str, str]] = [] for line in lines[1:]: if not line: continue if b":" not in line: continue name_b, value_b = line.split(b":", 1) name = name_b.decode("latin-1").strip() value = value_b.decode("latin-1").strip() headers.append((name, value)) try: method = method_b.decode("latin-1") path = path_b.decode("latin-1") except UnicodeDecodeError: return None host = "" body_len = 0 for key, value in headers: low = key.lower() if low == "host" and not host: host = value elif low == "content-length": try: body_len = int(value) except ValueError: body_len = 0 if body_len < 0 or body_len > 64 * 1024 * 1024: return None return HttpRequest(method=method, path=path, host=host, headers=headers), idx + 4 def parse_request(data: bytes) -> HttpRequest | None: """解析完整请求(头部 + body), body 不足返回 None。""" parsed = parse_request_head(data) if parsed is None: return None req, head_len = parsed content_length = 0 for key, value in req.headers: if key.lower() == "content-length": try: content_length = int(value) except ValueError: content_length = 0 if len(data) < head_len + content_length: return None req.body = data[head_len : head_len + content_length] return req