Files
wechatAiclaw/scripts/proxy_server.py
丹尼尔 152877cef2 fix:bug
2026-03-11 13:59:22 +08:00

212 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
当前环境下自建的 HTTP 代理服务(支持 HTTP 与 HTTPS CONNECT
使用方式:
# 安装依赖(与后端相同环境即可,无需额外包)
pip install -r backend/requirements.txt # 可选,本脚本仅用标准库
# 启动(默认 0.0.0.0:8888
python scripts/proxy_server.py
python scripts/proxy_server.py --host 0.0.0.0 --port 8888
# 或用环境变量
PROXY_PORT=9999 python scripts/proxy_server.py
客户端设置代理后即可走本机:
export HTTP_PROXY=http://127.0.0.1:8888
export HTTPS_PROXY=http://127.0.0.1:8888
curl -x http://127.0.0.1:8888 https://example.com
PySocks 已加入 requirements.txt供后端/脚本作为客户端通过 SOCKS 代理访问时使用;
本代理服务仅用 Python 标准库 asyncio不依赖 PySocks。
"""
from __future__ import annotations
import argparse
import asyncio
import logging
import os
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
)
logger = logging.getLogger("proxy-server")
async def handle_connect(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, host: str, port: int) -> None:
"""建立到目标 host:port 的隧道并双向转发。"""
try:
remote_reader, remote_writer = await asyncio.wait_for(
asyncio.open_connection(host, port),
timeout=15.0,
)
except Exception as e:
logger.warning("Connect to %s:%s failed: %s", host, port, e)
writer.write(b"HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n")
await writer.drain()
writer.close()
return
try:
writer.write(b"HTTP/1.1 200 Connection Established\r\n\r\n")
await writer.drain()
async def forward(a: asyncio.StreamReader, b: asyncio.StreamWriter, name: str) -> None:
try:
while True:
data = await a.read(65536)
if not data:
break
b.write(data)
await b.drain()
except (ConnectionResetError, BrokenPipeError, asyncio.CancelledError):
pass
finally:
try:
b.close()
await b.wait_closed()
except Exception:
pass
await asyncio.gather(
forward(reader, remote_writer, "c->r"),
forward(remote_reader, writer, "r->c"),
)
except Exception as e:
logger.debug("Tunnel error: %s", e)
finally:
try:
writer.close()
await writer.wait_closed()
except Exception:
pass
try:
remote_writer.close()
await remote_writer.wait_closed()
except Exception:
pass
async def handle_http_request(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, first_line: bytes, headers: list[bytes]) -> None:
"""解析请求并转发到目标主机,将响应回传给客户端。"""
parts = first_line.decode("latin-1", errors="replace").split()
if len(parts) < 3:
writer.write(b"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n")
await writer.drain()
writer.close()
return
method, url, _ = parts[0], parts[1], parts[2]
host = port = None
for h in headers:
if h.lower().startswith(b"host:"):
host_line = h.split(b":", 1)[1].strip().decode("latin-1", errors="replace")
if ":" in host_line:
host, _, port_s = host_line.rpartition(":")
port = int(port_s)
else:
host = host_line
port = 80
break
if not host:
writer.write(b"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\nNo Host header\r\n")
await writer.drain()
writer.close()
return
if port is None:
port = 80
try:
remote_reader, remote_writer = await asyncio.wait_for(
asyncio.open_connection(host, port),
timeout=15.0,
)
except Exception as e:
logger.warning("Forward to %s:%s failed: %s", host, port, e)
writer.write(b"HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n")
await writer.drain()
writer.close()
return
try:
remote_writer.write(first_line + b"\r\n")
remote_writer.write(b"\r\n".join(headers) + b"\r\n\r\n")
await remote_writer.drain()
body_len = 0
for h in headers:
if h.lower().startswith(b"content-length:"):
body_len = int(h.split(b":", 1)[1].strip())
break
if body_len > 0:
body = await reader.readexactly(body_len)
remote_writer.write(body)
await remote_writer.drain()
response = await remote_reader.read(1024 * 1024)
writer.write(response)
await writer.drain()
except Exception as e:
logger.debug("HTTP forward error: %s", e)
finally:
try:
remote_writer.close()
await remote_writer.wait_closed()
except Exception:
pass
try:
writer.close()
await writer.wait_closed()
except Exception:
pass
async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
peername = writer.get_extra_info("peername", ("?", "?"))
try:
line = await asyncio.wait_for(reader.readline(), timeout=10.0)
if not line:
return
first_line = line.rstrip(b"\r\n")
headers = []
while True:
line = await reader.readline()
if line in (b"\r\n", b"\n"):
break
headers.append(line.rstrip(b"\r\n"))
parts = first_line.split()
if len(parts) >= 2 and parts[0].upper() == b"CONNECT":
_, host_port, _ = parts[0], parts[1], parts[2] if len(parts) > 2 else b""
hp = host_port.decode("latin-1", errors="replace")
if ":" in hp:
host, _, port_s = hp.rpartition(":")
port = int(port_s)
else:
host = hp
port = 443
logger.info("CONNECT %s:%s from %s", host, port, peername)
await handle_connect(reader, writer, host, port)
else:
logger.info("HTTP %s from %s", first_line[:60], peername)
await handle_http_request(reader, writer, first_line, headers)
except asyncio.TimeoutError:
logger.warning("Timeout from %s", peername)
except Exception as e:
logger.debug("Client error %s: %s", peername, e)
finally:
try:
writer.close()
await writer.wait_closed()
except Exception:
pass
async def main() -> None:
parser = argparse.ArgumentParser(description="HTTP/HTTPS proxy server (CONNECT support)")
parser.add_argument("--host", default=os.environ.get("PROXY_HOST", "0.0.0.0"), help="Bind host")
parser.add_argument("--port", type=int, default=int(os.environ.get("PROXY_PORT", "8888")), help="Bind port")
args = parser.parse_args()
server = await asyncio.start_server(handle_client, args.host, args.port)
addr = server.sockets[0].getsockname() if server.sockets else (args.host, args.port)
logger.info("Proxy server listening on http://%s:%s", addr[0], addr[1])
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())