92 lines
3.0 KiB
Python
92 lines
3.0 KiB
Python
"""本地转发器的无状态报文解析与改写函数。"""
|
|
|
|
from urllib.parse import urlsplit
|
|
|
|
|
|
HTTP_METHODS = (
|
|
b"CONNECT ", b"GET ", b"POST ", b"PUT ", b"DELETE ",
|
|
b"HEAD ", b"OPTIONS ", b"PATCH ",
|
|
)
|
|
|
|
|
|
def starts_with_http_method(data: bytes) -> bool:
|
|
return any(data.startswith(method) for method in HTTP_METHODS)
|
|
|
|
|
|
def inject_proxy_auth(data: bytes, basic_auth: str) -> bytes:
|
|
"""为完整 HTTP 代理请求注入上游 Basic Authorization。"""
|
|
if not starts_with_http_method(data):
|
|
return data
|
|
end = data.find(b"\r\n\r\n")
|
|
if end == -1:
|
|
return data
|
|
head = data[:end]
|
|
first_end = head.find(b"\r\n")
|
|
request_line = head if first_end == -1 else head[:first_end]
|
|
headers = b"" if first_end == -1 else head[first_end + 2:]
|
|
for line in headers.split(b"\r\n"):
|
|
if b":" not in line:
|
|
continue
|
|
name = line[:line.index(b":")].strip().lower()
|
|
if name == b"authorization":
|
|
return data
|
|
auth_line = f"Authorization: Basic {basic_auth}\r\n".encode()
|
|
return request_line + b"\r\n" + auth_line + headers + data[end:]
|
|
|
|
|
|
def parse_connect_authority(first_line: str):
|
|
"""从 CONNECT 首行提取 ``(host, port)``,失败时返回 ``None``。"""
|
|
parts = first_line.split(" ")
|
|
if len(parts) < 2:
|
|
return None
|
|
authority = parts[1]
|
|
if authority.startswith("[") and "]" in authority:
|
|
try:
|
|
host = authority[1:authority.index("]")]
|
|
tail = authority[authority.index("]") + 1:]
|
|
if tail.startswith(":") and tail[1:].isdigit():
|
|
return host, int(tail[1:])
|
|
except ValueError:
|
|
pass
|
|
if ":" not in authority:
|
|
return None
|
|
host, _, port_s = authority.rpartition(":")
|
|
if not host or not port_s.isdigit():
|
|
return None
|
|
return host, int(port_s)
|
|
|
|
|
|
def parse_http_absolute(first_line: str):
|
|
"""从明文 HTTP 代理首行提取 ``(host, port)``,失败时返回 ``None``。"""
|
|
parts = first_line.split(" ")
|
|
if len(parts) < 2 or not parts[1].lower().startswith(("http://", "https://")):
|
|
return None
|
|
parsed = urlsplit(parts[1])
|
|
if not parsed.hostname:
|
|
return None
|
|
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
return parsed.hostname, port
|
|
|
|
|
|
def force_http_close(buffer: bytes) -> bytes:
|
|
"""直连明文 HTTP 时强制 Connection: close,避免错复用连接。"""
|
|
end = buffer.find(b"\r\n\r\n")
|
|
if end == -1:
|
|
return buffer
|
|
head, tail = buffer[:end], buffer[end:]
|
|
lines = head.split(b"\r\n")
|
|
out = [lines[0]]
|
|
conn_seen = False
|
|
for line in lines[1:]:
|
|
low = line.lower()
|
|
if low.startswith(b"connection:"):
|
|
out.append(b"Connection: close")
|
|
conn_seen = True
|
|
elif low.startswith(b"proxy-connection:"):
|
|
continue
|
|
else:
|
|
out.append(line)
|
|
if not conn_seen:
|
|
out.append(b"Connection: close")
|
|
return b"\r\n".join(out) + tail
|