Files
vpn-loopback-firewall/channel/proxy_http.py
T

130 lines
4.2 KiB
Python
Raw Normal View History

"""L2 本地 HTTP 透明代理(软路由侧, 监听 8080)。
iptables 把 LAN 出向 dport 80 的明文 HTTP REDIRECT 到这里:
1. recv 请求(头 + Content-Length body)
2. 原始字节打包走通道(云端负责 https 套壳)
3. 响应原样写回客户端后关闭连接(每个请求一条连接)
连接断/通道错误 -> 502。目标为内网段(学校)的流量被 hook 规则排除,
不会到本代理。
"""
from __future__ import annotations
import socket
import threading
from typing import Any
from .client import ChannelClient, ChannelError
from .httpmsg import parse_request_head
LISTEN_BACKLOG = 128
MAX_HEAD = 64 * 1024
class HttpTransparentProxy:
def __init__(self, client: ChannelClient, listen_port: int) -> None:
if listen_port < 1 or listen_port > 65535:
raise ValueError(f"bad listen port: {listen_port}")
self.client = client
self.port = listen_port
self.served = 0
self.failed = 0
def serve(self) -> None:
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", self.port))
srv.listen(LISTEN_BACKLOG)
print(f"[http-proxy] listen 0.0.0.0:{self.port}", flush=True)
while True:
conn, _addr = srv.accept()
threading.Thread(
target=self._handle,
args=(conn,),
name="http-proxy-conn",
daemon=True,
).start()
def _recv_request(self, conn: socket.socket) -> bytes | None:
buf = bytearray()
while len(buf) < MAX_HEAD:
chunk = conn.recv(8192)
if not chunk:
return None
buf += chunk
parsed = parse_request_head(bytes(buf))
if parsed is None:
continue
_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
total = head_len + content_length
if len(buf) >= total:
return bytes(buf[:total])
if content_length > MAX_HEAD:
return None
return None
def _handle(self, conn: socket.socket) -> None:
try:
conn.settimeout(30)
raw = self._recv_request(conn)
if raw is None:
return
resp = self._channel_send(raw)
if resp is None:
self._write_502(conn)
self.failed += 1
return
conn.sendall(resp)
self.served += 1
except OSError:
self.failed += 1
finally:
try:
conn.close()
except OSError:
pass
def _channel_send(self, raw: bytes) -> bytes | None:
try:
return self.client.send_http_request(raw)
except ChannelError:
try:
self.client.ensure_connected()
return self.client.send_http_request(raw)
except ChannelError:
return None
def _write_502(self, conn: socket.socket) -> None:
body = b"<html><body><h1>502 Bad Gateway</h1>"
body += b"<p>channel unavailable</p></body></html>"
head = (
b"HTTP/1.1 502 Bad Gateway\r\n"
b"Content-Type: text/html; charset=utf-8\r\n"
+ f"Content-Length: {len(body)}\r\n".encode("ascii")
+ b"Connection: close\r\n\r\n"
)
try:
conn.sendall(head + body)
except OSError:
pass
def run_http_proxy(cfg: dict[str, Any]) -> None:
cloud: dict[str, Any] = cfg["cloud"]
proxy_cfg: dict[str, Any] = cfg["http_proxy"]
client = ChannelClient(
host=str(cloud["host"]),
port=int(cloud["port"]),
token=str(cloud["token"]),
ca_path=str(cloud.get("ca_path", "")),
heartbeat=float(cfg.get("heartbeat", 30)),
)
client.connect()
HttpTransparentProxy(client, int(proxy_cfg["listen_port"])).serve()