多平台账号批量注册系统,支持 OpenAI / Grok / Kiro / Gemini 平台的全自动注册。 Features: - 用户名/密码注册登录(首个用户自动管理员) - JWT 认证 + bcrypt 密码加密 - 积分系统与多用户管理 - HuggingFace Space 弹性 Worker 节点管理 - Cloudflare Worker 负载均衡 - 实时日志推送(SSE/WebSocket) - 管理后台(用户管理、系统设置、代理池、卡池) Tech Stack: Go 1.25 / Gin / GORM / PostgreSQL / Vue 3 / Vite / TailwindCSS
244 lines
8.1 KiB
Python
244 lines
8.1 KiB
Python
"""
|
||
代理链工具 — 在环境变量代理(Clash)和后端代理之间建立链式转发。
|
||
|
||
链路:App → Clash(HTTP_PROXY) → 后端代理 → 目标
|
||
|
||
使用方式:
|
||
from proxy_chain import chain_proxy, curl_proxy_args
|
||
|
||
# playwright / camoufox / requests — 替换代理 URL
|
||
proxy_url = chain_proxy("http://user:pass@us-proxy:8080")
|
||
# 返回 "http://127.0.0.1:xxxxx"(本地链式代理),或原样返回
|
||
|
||
# curl — 获取额外参数
|
||
extra_args = curl_proxy_args("http://user:pass@us-proxy:8080")
|
||
# 返回 ["--preproxy", "http://clash:7890"] 或 []
|
||
"""
|
||
|
||
import base64
|
||
import logging
|
||
import os
|
||
import select
|
||
import socket
|
||
import threading
|
||
from urllib.parse import urlparse
|
||
|
||
logger = logging.getLogger("proxy-chain")
|
||
|
||
# ─── 环境变量检测 ─────────────────────────────────────────────────────────────
|
||
|
||
def _get_env_proxy() -> str:
|
||
"""读取环境变量中的代理地址(HTTPS_PROXY 优先)"""
|
||
for var in ("HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy"):
|
||
val = os.environ.get(var, "").strip()
|
||
if val:
|
||
return val
|
||
return ""
|
||
|
||
|
||
# ─── curl 代理链 ─────────────────────────────────────────────────────────────
|
||
|
||
def curl_proxy_args(backend_proxy: str) -> list:
|
||
"""
|
||
返回 curl 的代理链参数。
|
||
当环境变量有代理(Clash)且有后端代理时,返回 ["--preproxy", clash_url],
|
||
curl 会先通过 Clash 建立隧道,再连后端代理。
|
||
"""
|
||
env_proxy = _get_env_proxy()
|
||
if env_proxy and backend_proxy:
|
||
return ["--preproxy", env_proxy]
|
||
return []
|
||
|
||
|
||
# ─── 通用代理链(playwright / camoufox / requests)─────────────────────────────
|
||
|
||
_chain_servers = {} # backend_proxy_url -> "http://127.0.0.1:port"
|
||
_chain_lock = threading.Lock()
|
||
|
||
|
||
def chain_proxy(backend_proxy: str) -> str:
|
||
"""
|
||
代理链入口。返回应该使用的代理 URL:
|
||
- 有 Clash + 有后端代理 → 启动本地链式代理,返回 "http://127.0.0.1:port"
|
||
- 只有后端代理 → 原样返回
|
||
- 无后端代理 → 返回空串(让调用方走默认 / 环境变量)
|
||
"""
|
||
if not backend_proxy:
|
||
return ""
|
||
|
||
env_proxy = _get_env_proxy()
|
||
if not env_proxy:
|
||
return backend_proxy # 无 Clash,直连后端代理
|
||
|
||
with _chain_lock:
|
||
# double-check:锁内检查 + 创建,避免 TOCTOU 竞态
|
||
if backend_proxy in _chain_servers:
|
||
return _chain_servers[backend_proxy]
|
||
|
||
# 启动本地链式代理
|
||
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||
srv.bind(("127.0.0.1", 0))
|
||
port = srv.getsockname()[1]
|
||
srv.listen(32)
|
||
|
||
bp = urlparse(backend_proxy)
|
||
clash = urlparse(env_proxy)
|
||
|
||
# 后端代理认证信息(注入到转发请求中)
|
||
proxy_auth_header = ""
|
||
if bp.username:
|
||
cred = f"{bp.username}:{bp.password or ''}"
|
||
b64 = base64.b64encode(cred.encode()).decode()
|
||
proxy_auth_header = f"Proxy-Authorization: Basic {b64}\r\n"
|
||
|
||
bp_host = bp.hostname
|
||
bp_port = bp.port or (443 if bp.scheme == "https" else 80)
|
||
clash_host = clash.hostname or "127.0.0.1"
|
||
clash_port = clash.port or 7890
|
||
|
||
t = threading.Thread(
|
||
target=_accept_loop,
|
||
args=(srv, clash_host, clash_port, bp_host, bp_port, proxy_auth_header, backend_proxy),
|
||
daemon=True,
|
||
)
|
||
t.start()
|
||
|
||
local_url = f"http://127.0.0.1:{port}"
|
||
_chain_servers[backend_proxy] = local_url
|
||
|
||
logger.info("代理链已启动: %s → Clash(%s:%d) → %s:%d",
|
||
local_url, clash_host, clash_port, bp_host, bp_port)
|
||
return local_url
|
||
|
||
|
||
# ─── 内部实现 ─────────────────────────────────────────────────────────────────
|
||
|
||
def _accept_loop(srv, clash_host, clash_port, bp_host, bp_port, proxy_auth, cache_key):
|
||
try:
|
||
while True:
|
||
client, _ = srv.accept()
|
||
threading.Thread(
|
||
target=_handle_client,
|
||
args=(client, clash_host, clash_port, bp_host, bp_port, proxy_auth),
|
||
daemon=True,
|
||
).start()
|
||
except Exception:
|
||
pass
|
||
finally:
|
||
_safe_close(srv)
|
||
# 清理缓存,下次调用 chain_proxy 时会重建
|
||
with _chain_lock:
|
||
_chain_servers.pop(cache_key, None)
|
||
|
||
|
||
def _handle_client(client, clash_host, clash_port, bp_host, bp_port, proxy_auth):
|
||
"""处理单个客户端连接:Clash → 后端代理 双跳 CONNECT"""
|
||
tunnel = None
|
||
try:
|
||
# 1. 读取客户端请求头
|
||
header = b""
|
||
while b"\r\n\r\n" not in header:
|
||
chunk = client.recv(8192)
|
||
if not chunk:
|
||
return
|
||
header += chunk
|
||
|
||
first_line = header.split(b"\r\n")[0].decode("utf-8", errors="replace")
|
||
|
||
# 2. TCP 连接 Clash
|
||
tunnel = socket.create_connection((clash_host, clash_port), timeout=15)
|
||
|
||
# 3. 通过 Clash CONNECT 到后端代理
|
||
connect_to_bp = (
|
||
f"CONNECT {bp_host}:{bp_port} HTTP/1.1\r\n"
|
||
f"Host: {bp_host}:{bp_port}\r\n"
|
||
f"\r\n"
|
||
)
|
||
tunnel.sendall(connect_to_bp.encode())
|
||
|
||
resp = _read_http_header(tunnel)
|
||
if resp is None or b"200" not in resp.split(b"\r\n")[0]:
|
||
client.sendall(b"HTTP/1.1 502 Bad Gateway\r\n\r\n")
|
||
return
|
||
|
||
# 4. 现在 tunnel 是到后端代理的连接
|
||
if first_line.upper().startswith("CONNECT"):
|
||
# CONNECT 请求:注入认证后转发给后端代理
|
||
target = first_line.split()[1] # host:port
|
||
fwd = (
|
||
f"CONNECT {target} HTTP/1.1\r\n"
|
||
f"Host: {target}\r\n"
|
||
f"{proxy_auth}"
|
||
f"\r\n"
|
||
)
|
||
tunnel.sendall(fwd.encode())
|
||
|
||
bp_resp = _read_http_header(tunnel)
|
||
if bp_resp is None:
|
||
client.sendall(b"HTTP/1.1 502 Bad Gateway\r\n\r\n")
|
||
return
|
||
client.sendall(bp_resp)
|
||
_pipe(client, tunnel)
|
||
else:
|
||
# 普通 HTTP 代理请求:注入认证头后转发
|
||
if proxy_auth:
|
||
# 在请求头中插入 Proxy-Authorization
|
||
parts = header.split(b"\r\n", 1)
|
||
header = parts[0] + b"\r\n" + proxy_auth.encode() + parts[1]
|
||
tunnel.sendall(header)
|
||
_pipe(client, tunnel)
|
||
|
||
except Exception as e:
|
||
logger.debug("代理链连接异常: %s", e)
|
||
try:
|
||
client.sendall(b"HTTP/1.1 502 Bad Gateway\r\n\r\n")
|
||
except Exception:
|
||
pass
|
||
finally:
|
||
_safe_close(client)
|
||
_safe_close(tunnel)
|
||
|
||
|
||
def _read_http_header(sock, timeout=30) -> bytes | None:
|
||
"""读取完整的 HTTP 响应头(直到 \\r\\n\\r\\n)"""
|
||
sock.settimeout(timeout)
|
||
buf = b""
|
||
try:
|
||
while b"\r\n\r\n" not in buf:
|
||
chunk = sock.recv(8192)
|
||
if not chunk:
|
||
return None
|
||
buf += chunk
|
||
except socket.timeout:
|
||
return None
|
||
finally:
|
||
sock.settimeout(None)
|
||
return buf
|
||
|
||
|
||
def _pipe(sock1, sock2):
|
||
"""双向数据管道"""
|
||
pair = [sock1, sock2]
|
||
try:
|
||
while True:
|
||
readable, _, errored = select.select(pair, [], pair, 120)
|
||
if errored or not readable:
|
||
break
|
||
for s in readable:
|
||
data = s.recv(65536)
|
||
if not data:
|
||
return
|
||
other = sock2 if s is sock1 else sock1
|
||
other.sendall(data)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def _safe_close(sock):
|
||
if sock:
|
||
try:
|
||
sock.close()
|
||
except Exception:
|
||
pass
|