Files
xiaolajiaoyyds 6f71df4849 Initial open-source release
多平台账号批量注册系统,支持 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
2026-03-22 03:44:42 +08:00

244 lines
8.1 KiB
Python
Raw Permalink 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.
"""
代理链工具 — 在环境变量代理(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