fix: 新增代理
This commit is contained in:
270
backend/main.py
270
backend/main.py
@@ -381,6 +381,84 @@ async def api_ws_status() -> dict:
|
||||
return {"connected": is_ws_connected()}
|
||||
|
||||
|
||||
# 代理检测:用当前代理访问测试 URL,验证是否可用
|
||||
PROXY_CHECK_URL = os.getenv("PROXY_CHECK_URL", "https://httpbin.org/ip")
|
||||
|
||||
|
||||
@app.get("/api/check-proxy")
|
||||
async def api_check_proxy(proxy: Optional[str] = Query(None, description="可选,指定要检测的代理 URL(不传则用环境变量/隧道/KDL)")):
|
||||
"""检测代理是否可用:用解析到的代理请求测试页,返回是否成功及来源。"""
|
||||
proxy_url = (proxy or "").strip()
|
||||
source = "query"
|
||||
if not proxy_url:
|
||||
proxy_url = _resolve_proxy("", allow_auto=True)
|
||||
if proxy_url == "__tunnel__":
|
||||
proxy_url = _proxy_from_tunnel()
|
||||
source = "tunnel"
|
||||
logger.info("check-proxy: using tunnel -> %s", "socks5h://***@%s/" % TUNNEL_PROXY)
|
||||
elif proxy_url == "__kdl__":
|
||||
proxy_url = await _proxy_from_kdl()
|
||||
source = "kdl"
|
||||
logger.info("check-proxy: using kdl -> %s", "http://***@%s/" % (proxy_url.split("@", 1)[-1].rstrip("/") if proxy_url else "?"))
|
||||
else:
|
||||
source = "env" if proxy_url else "none"
|
||||
if proxy_url:
|
||||
logger.info("check-proxy: using env/body, len=%s", len(proxy_url))
|
||||
if not proxy_url:
|
||||
return {
|
||||
"ok": False,
|
||||
"source": "none",
|
||||
"error": "未配置代理。请填写代理、或设置 HTTP_PROXY/HTTPS_PROXY、或配置 TUNNEL_PROXY(固定隧道)、或 KDL 代理 API。",
|
||||
}
|
||||
# 脱敏显示(不暴露密码)
|
||||
def _preview(u: str) -> str:
|
||||
if not u or "@" not in u:
|
||||
return u[:50] + "…" if len(u) > 50 else u
|
||||
pre, at = u.rsplit("@", 1)
|
||||
if "://" in pre:
|
||||
scheme = pre.split("://", 1)[0] + "://"
|
||||
rest = pre[len(scheme) :]
|
||||
if ":" in rest:
|
||||
user, _ = rest.split(":", 1)
|
||||
pre = scheme + user + ":***"
|
||||
else:
|
||||
pre = scheme + "***"
|
||||
else:
|
||||
pre = "***"
|
||||
return pre + "@" + (at[:30] + "…" if len(at) > 30 else at)
|
||||
|
||||
preview = _preview(proxy_url)
|
||||
logger.info("check-proxy: source=%s, proxy_preview=%s", source, preview)
|
||||
try:
|
||||
async with httpx.AsyncClient(trust_env=False, timeout=15.0, proxy=proxy_url) as client:
|
||||
resp = await client.get(PROXY_CHECK_URL)
|
||||
if resp.status_code == 200:
|
||||
logger.info("check-proxy: ok, status=%s", resp.status_code)
|
||||
return {
|
||||
"ok": True,
|
||||
"source": source,
|
||||
"proxy_preview": preview,
|
||||
"check_url": PROXY_CHECK_URL,
|
||||
"status_code": resp.status_code,
|
||||
}
|
||||
logger.warning("check-proxy: fail, status=%s", resp.status_code)
|
||||
return {
|
||||
"ok": False,
|
||||
"source": source,
|
||||
"proxy_preview": preview,
|
||||
"error": f"请求测试页返回 {resp.status_code}",
|
||||
"status_code": resp.status_code,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("check-proxy: exception %s", e)
|
||||
return {
|
||||
"ok": False,
|
||||
"source": source,
|
||||
"proxy_preview": preview,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
|
||||
def _proxy_from_env() -> str:
|
||||
"""当登录页未填代理时,使用环境变量中的代理(服务器上设置 HTTP_PROXY/HTTPS_PROXY 后生效)。"""
|
||||
return (
|
||||
@@ -390,26 +468,170 @@ def _proxy_from_env() -> str:
|
||||
)
|
||||
|
||||
|
||||
# 固定隧道代理(socks5h + 用户名密码):未配置登录页/环境变量时使用
|
||||
# 格式与 requests 示例一致:socks5h://user:pwd@host:port/
|
||||
TUNNEL_PROXY = (os.getenv("TUNNEL_PROXY") or "").strip() # 例如 218.78.109.253:16816
|
||||
TUNNEL_PROXY_USERNAME = (os.getenv("TUNNEL_PROXY_USERNAME") or "").strip()
|
||||
TUNNEL_PROXY_PASSWORD = (os.getenv("TUNNEL_PROXY_PASSWORD") or "").strip()
|
||||
|
||||
# 快代理 KDL API(可选):隧道未配置时从此接口拉取代理
|
||||
KDL_PROXY_API_URL = (os.getenv("KDL_PROXY_API_URL") or "").strip()
|
||||
KDL_PROXY_USERNAME = (os.getenv("KDL_PROXY_USERNAME") or "").strip()
|
||||
KDL_PROXY_PASSWORD = (os.getenv("KDL_PROXY_PASSWORD") or "").strip()
|
||||
|
||||
# 启动时打印代理配置情况(不打印密码)
|
||||
def _log_proxy_config() -> None:
|
||||
tunnel_ok = bool(TUNNEL_PROXY and TUNNEL_PROXY_USERNAME and TUNNEL_PROXY_PASSWORD)
|
||||
kdl_ok = bool(KDL_PROXY_API_URL and KDL_PROXY_USERNAME and KDL_PROXY_PASSWORD)
|
||||
logger.info(
|
||||
"proxy config: tunnel=%s (TUNNEL_PROXY=%s), kdl=%s (KDL_API=%s)",
|
||||
tunnel_ok,
|
||||
TUNNEL_PROXY or "(empty)",
|
||||
kdl_ok,
|
||||
"set" if KDL_PROXY_API_URL else "(empty)",
|
||||
)
|
||||
|
||||
|
||||
_log_proxy_config()
|
||||
|
||||
|
||||
def _proxy_from_tunnel() -> str:
|
||||
"""使用固定隧道代理,格式 socks5h://user:pwd@host:port/,供 7006 使用。"""
|
||||
if not TUNNEL_PROXY:
|
||||
return ""
|
||||
user = (TUNNEL_PROXY_USERNAME or KDL_PROXY_USERNAME or "").strip()
|
||||
pwd = (TUNNEL_PROXY_PASSWORD or KDL_PROXY_PASSWORD or "").strip()
|
||||
if not user or not pwd:
|
||||
return ""
|
||||
return "socks5h://%(user)s:%(pwd)s@%(proxy)s/" % {
|
||||
"user": user,
|
||||
"pwd": pwd,
|
||||
"proxy": TUNNEL_PROXY,
|
||||
}
|
||||
|
||||
|
||||
async def _proxy_from_kdl() -> str:
|
||||
"""从快代理 API 获取一个代理 IP,格式化为 http://user:pwd@ip:port/ 供 7006 使用。"""
|
||||
if not KDL_PROXY_API_URL or not KDL_PROXY_USERNAME or not KDL_PROXY_PASSWORD:
|
||||
return ""
|
||||
try:
|
||||
async with httpx.AsyncClient(trust_env=False, timeout=10.0) as client:
|
||||
resp = await client.get(KDL_PROXY_API_URL)
|
||||
resp.raise_for_status()
|
||||
proxy_ip = (resp.text or "").strip()
|
||||
if not proxy_ip:
|
||||
return ""
|
||||
return "http://%(user)s:%(pwd)s@%(proxy)s/" % {
|
||||
"user": KDL_PROXY_USERNAME,
|
||||
"pwd": KDL_PROXY_PASSWORD,
|
||||
"proxy": proxy_ip,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("KDL proxy fetch failed: %s", e)
|
||||
return ""
|
||||
|
||||
|
||||
# 隧道代理若以 http 形式传入(登录页或 KDL 返回),统一改为 socks5h 再传给 7006
|
||||
TUNNEL_PROXY_NORMALIZE_HOST = (os.getenv("TUNNEL_PROXY_NORMALIZE_HOST") or "218.78.109.253:16816").strip()
|
||||
|
||||
|
||||
def _proxy_preview_for_log(proxy: str) -> str:
|
||||
"""代理脱敏,用于日志打印(不暴露密码)。"""
|
||||
if not proxy or not isinstance(proxy, str):
|
||||
return "(empty)"
|
||||
u = proxy.strip()
|
||||
if not u:
|
||||
return "(empty)"
|
||||
if "@" not in u:
|
||||
return u[:50] + "…" if len(u) > 50 else u
|
||||
pre, at = u.rsplit("@", 1)
|
||||
at = at.rstrip("/").split("/")[0].split("?")[0]
|
||||
if "://" in pre:
|
||||
scheme = pre.split("://", 1)[0] + "://"
|
||||
rest = pre[len(scheme):]
|
||||
user = rest.split(":", 1)[0] if ":" in rest else "***"
|
||||
pre = scheme + user + ":***"
|
||||
else:
|
||||
pre = "***"
|
||||
return pre + "@" + (at[:40] + "…" if len(at) > 40 else at) + "/"
|
||||
|
||||
|
||||
def _normalize_proxy_scheme_to_socks5h(proxy: str) -> str:
|
||||
"""若代理是隧道地址但用了 http,改为 socks5h(7006 需 socks5)。"""
|
||||
if not proxy or not isinstance(proxy, str):
|
||||
return proxy
|
||||
p = proxy.strip()
|
||||
if not p.startswith("http://") or "@" not in p:
|
||||
return p
|
||||
try:
|
||||
host_part = p.split("@", 1)[1].rstrip("/").split("/")[0].split("?")[0]
|
||||
except IndexError:
|
||||
return p
|
||||
if host_part != TUNNEL_PROXY and host_part != "218.78.109.253:16816":
|
||||
return p
|
||||
out = "socks5h://" + p[7:]
|
||||
logger.info("proxy normalize: http -> socks5h for tunnel %s", host_part)
|
||||
return out
|
||||
|
||||
|
||||
def _resolve_proxy(body_proxy: str, *, allow_auto: bool = True) -> str:
|
||||
"""解析最终传给 7006 的代理:请求体 > 环境变量 > 固定隧道 >(可选)KDL API。"""
|
||||
p = (body_proxy or "").strip()
|
||||
if p:
|
||||
logger.debug("proxy resolve: from body, len=%s", len(p))
|
||||
return p
|
||||
p = _proxy_from_env()
|
||||
if p:
|
||||
logger.debug("proxy resolve: from env (HTTP_PROXY/HTTPS_PROXY), len=%s", len(p))
|
||||
return p
|
||||
if not allow_auto:
|
||||
return ""
|
||||
# 隧道:只要 TUNNEL_PROXY 有值且能凑齐账号密码(含用 KDL_* 兜底)则优先隧道
|
||||
tunnel_user = (TUNNEL_PROXY_USERNAME or KDL_PROXY_USERNAME or "").strip()
|
||||
tunnel_pwd = (TUNNEL_PROXY_PASSWORD or KDL_PROXY_PASSWORD or "").strip()
|
||||
if TUNNEL_PROXY and tunnel_user and tunnel_pwd:
|
||||
logger.info("proxy resolve: auto -> tunnel (socks5h), TUNNEL_PROXY=%s", TUNNEL_PROXY)
|
||||
return "__tunnel__"
|
||||
if KDL_PROXY_API_URL and KDL_PROXY_USERNAME and KDL_PROXY_PASSWORD:
|
||||
logger.info("proxy resolve: auto -> kdl (fetch from API)")
|
||||
return "__kdl__"
|
||||
logger.debug("proxy resolve: no auto proxy configured")
|
||||
return ""
|
||||
|
||||
|
||||
@app.post("/auth/wake")
|
||||
async def wake_up_login(body: WakeUpRequest):
|
||||
"""唤醒登录:仅调用上游 /login/WakeUpLogin(只限扫码登录),不获取二维码。"""
|
||||
key = (body.key or "").strip()
|
||||
if not key:
|
||||
raise HTTPException(status_code=400, detail="key is required")
|
||||
proxy = (body.Proxy or "").strip()
|
||||
if not proxy:
|
||||
proxy = _proxy_from_env()
|
||||
proxy = _resolve_proxy(body.Proxy or "", allow_auto=True)
|
||||
if proxy == "__tunnel__":
|
||||
proxy = _proxy_from_tunnel()
|
||||
if proxy:
|
||||
logger.info("WakeUpLogin: using proxy from env (HTTP_PROXY/HTTPS_PROXY), len=%s", len(proxy))
|
||||
else:
|
||||
logger.info("WakeUpLogin: Proxy 为空,请在 .env 中设置 HTTP_PROXY/HTTPS_PROXY,或登录页填写代理后重试")
|
||||
logger.info("WakeUpLogin: using proxy from tunnel (socks5h), len=%s", len(proxy))
|
||||
elif proxy == "__kdl__":
|
||||
proxy = await _proxy_from_kdl()
|
||||
if proxy:
|
||||
logger.info("WakeUpLogin: using proxy from KDL API, len=%s", len(proxy))
|
||||
if not proxy:
|
||||
logger.info("WakeUpLogin: Proxy 为空,请在 .env 中配置 TUNNEL_PROXY 或 HTTP_PROXY/HTTPS_PROXY 或 KDL,或登录页填写代理")
|
||||
elif proxy not in ("__tunnel__", "__kdl__"):
|
||||
logger.info("WakeUpLogin: using proxy from body/env, len=%s", len(proxy))
|
||||
if proxy in ("__tunnel__", "__kdl__"):
|
||||
proxy = ""
|
||||
proxy = _normalize_proxy_scheme_to_socks5h(proxy)
|
||||
ipad_ormac = (body.IpadOrmac or "").strip() or "ipad"
|
||||
payload = {
|
||||
"Check": body.Check,
|
||||
"IpadOrmac": "ipad",
|
||||
"IpadOrmac": ipad_ormac,
|
||||
"Proxy": proxy,
|
||||
}
|
||||
url = f"{WECHAT_UPSTREAM_BASE_URL.rstrip('/')}/login/WakeUpLogin"
|
||||
logger.info("WakeUpLogin: key=%s, payload=%s, url=%s", key, payload, url)
|
||||
logger.info(
|
||||
"WakeUpLogin 请求参数: key=%s, url=%s, Proxy=%s, Check=%s, IpadOrmac=%s",
|
||||
key, url, proxy, body.Check, ipad_ormac,
|
||||
)
|
||||
try:
|
||||
async with httpx.AsyncClient(trust_env=False, timeout=20.0) as client:
|
||||
resp = await client.post(url, params={"key": key}, json=payload)
|
||||
@@ -440,22 +662,32 @@ async def get_login_qrcode(body: QrCodeRequest):
|
||||
if not key:
|
||||
raise HTTPException(status_code=400, detail="key is required")
|
||||
|
||||
proxy = (body.Proxy or "").strip()
|
||||
if not proxy:
|
||||
proxy = _proxy_from_env()
|
||||
if not proxy:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="必须配置代理(Proxy)。服务器在香港,不上代理必封号,请填写 socks5 代理后再取码。",
|
||||
)
|
||||
proxy = _resolve_proxy(body.Proxy or "", allow_auto=True)
|
||||
if proxy == "__tunnel__":
|
||||
proxy = _proxy_from_tunnel()
|
||||
if proxy:
|
||||
logger.info("GetLoginQrCodeNewDirect: using proxy from tunnel (socks5h), len=%s", len(proxy))
|
||||
elif proxy == "__kdl__":
|
||||
proxy = await _proxy_from_kdl()
|
||||
if proxy:
|
||||
logger.info("GetLoginQrCodeNewDirect: using proxy from KDL API, len=%s", len(proxy))
|
||||
if proxy in ("__tunnel__", "__kdl__"):
|
||||
proxy = ""
|
||||
proxy = _normalize_proxy_scheme_to_socks5h(proxy)
|
||||
if proxy:
|
||||
logger.info("GetLoginQrCodeNewDirect: proxy=yes, force_mac=%s, IpadOrmac=%s", body.force_mac, "mac" if body.force_mac else (body.IpadOrmac or "ipad"))
|
||||
else:
|
||||
logger.info("GetLoginQrCodeNewDirect: proxy=empty(未配置则后端自动读 env/KDL),force_mac=%s", body.force_mac)
|
||||
payload = body.dict(exclude={"key", "force_mac"})
|
||||
payload["Check"] = False
|
||||
payload["IpadOrmac"] = "mac" if body.force_mac else "ipad"
|
||||
payload["IpadOrmac"] = "mac" if body.force_mac else ((body.IpadOrmac or "").strip() or "ipad")
|
||||
payload["Proxy"] = proxy
|
||||
logger.info("GetLoginQrCodeNewDirect: proxy=yes, force_mac=%s, IpadOrmac=%s", body.force_mac, payload["IpadOrmac"])
|
||||
|
||||
url = f"{WECHAT_UPSTREAM_BASE_URL}/login/GetLoginQrCodeNewDirect"
|
||||
logger.info("GetLoginQrCodeNewDirect: key=%s, payload=%s, url=%s", key, payload, url)
|
||||
logger.info(
|
||||
"GetLoginQrCodeNewDirect 请求参数: key=%s, url=%s, Proxy=%s, Check=%s, IpadOrmac=%s",
|
||||
key, url, proxy, False, payload["IpadOrmac"],
|
||||
)
|
||||
try:
|
||||
async with httpx.AsyncClient(trust_env=False, timeout=20.0) as client:
|
||||
resp = await client.post(url, params={"key": key}, json=payload)
|
||||
|
||||
Reference in New Issue
Block a user