96d44569fd
- proto.py: 帧协议 [type][req_id][len][payload], req_id 多路复用 - client.py: TLS 长连 + AUTH + 断线自动重连(全标准库, OpenWrt 可跑) - proxy_http.py: 本地 HTTP 透明代理(明文 80 走通道, 失败回 502) - proxy_dns.py: DNS 分流(内网域 -> 学校 DNS, 公网域 -> 云通道递归) - server.py: 云端 AUTH + HTTPS 套壳(统一 UA, 剔 hop-by-hop)+ DNS 递归 - deploy/hook.sh: REDIRECT 规则(内网段放行, 认证 eportal 绝不走通道) - 30 个新单测(40 总)+ smoke_channel 端到端冒烟 PASS
107 lines
3.1 KiB
Python
107 lines
3.1 KiB
Python
"""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
|