Detect Cloudflare 403 on mint and retry alternate proxies.
- Clear CF block errors with egress label and config hint - Log mint exit (direct/proxy); probe local proxy ports on 403 - Retry mint_proxy/proxy/pool candidates without browser mint
This commit is contained in:
1 parent
cc0aa6a433
commit
7d6d52ac5d
2 files changed
+170
-1
No files matched your search
+122
-1
@@ -62,6 +62,72 @@ def _resolve_proxy(cfg: dict) -> str | None:
|
||||
return resolved or None
|
||||
|
||||
|
||||
def _proxy_label(proxy: str | None) -> str:
|
||||
try:
|
||||
from oidc_mint.proxyutil import proxy_log_label
|
||||
|
||||
return proxy_log_label(proxy or "") or "(direct)"
|
||||
except Exception:
|
||||
return proxy or "(direct)"
|
||||
|
||||
|
||||
def _port_open(host: str, port: int, timeout: float = 0.25) -> bool:
|
||||
import socket
|
||||
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _local_proxy_candidates(cfg: dict) -> list[str]:
|
||||
"""直连被 CF 拦时尝试的本机/配置代理列表(去重)。"""
|
||||
cands: list[str] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
def _add(raw: str | None) -> None:
|
||||
p = (raw or "").strip()
|
||||
if not p or p in seen:
|
||||
return
|
||||
seen.add(p)
|
||||
cands.append(p)
|
||||
|
||||
for key in ("mint_proxy", "proxy"):
|
||||
_add(str(cfg.get(key) or ""))
|
||||
raw_list = cfg.get("proxy_pool") or []
|
||||
if isinstance(raw_list, str):
|
||||
for line in raw_list.replace(",", "\n").splitlines():
|
||||
_add(line)
|
||||
elif isinstance(raw_list, (list, tuple)):
|
||||
for x in raw_list:
|
||||
_add(str(x))
|
||||
try:
|
||||
import proxy_pool
|
||||
|
||||
for p in proxy_pool.pool_snapshot()[:8]:
|
||||
_add(p)
|
||||
except Exception:
|
||||
pass
|
||||
# 仅探测本机已监听的常见代理端口,避免空转超时
|
||||
for port in (7890, 7897, 10809, 10808, 7891, 20171, 6152, 1080):
|
||||
if _port_open("127.0.0.1", port):
|
||||
_add(f"http://127.0.0.1:{port}")
|
||||
return cands
|
||||
|
||||
|
||||
def _is_cf_mint_error(exc: BaseException | str) -> bool:
|
||||
try:
|
||||
from oidc_mint.oauth_code import is_cloudflare_block
|
||||
|
||||
return is_cloudflare_block(exc=exc)
|
||||
except Exception:
|
||||
text = str(exc or "").lower()
|
||||
return "403" in text and (
|
||||
"cloudflare" in text or "<!doctype" in text or "oldie" in text
|
||||
)
|
||||
|
||||
|
||||
def _mint_tokens(
|
||||
*,
|
||||
email: str,
|
||||
@@ -73,32 +139,87 @@ def _mint_tokens(
|
||||
proxy: str | None,
|
||||
) -> dict[str, Any]:
|
||||
"""优先 SSO 授权码;可选回退设备码。"""
|
||||
from oidc_mint import set_runtime_proxy
|
||||
from oidc_mint.oauth_code import OAuthCodeError, mint_from_sso, normalize_sso_cookie
|
||||
|
||||
sso_token = normalize_sso_cookie(sso or "")
|
||||
prefer_sso = bool(cfg.get("cpa_prefer_sso_oauth", True))
|
||||
allow_device = bool(cfg.get("cpa_allow_device_fallback", False))
|
||||
timeout = float(cfg.get("mint_timeout_sec", 300) or 300)
|
||||
max_proxy_tries = int(cfg.get("mint_proxy_retries", 4) or 4)
|
||||
|
||||
if prefer_sso and sso_token:
|
||||
log("[cpa] 使用 SSO→OAuth(PKCE, referrer=grok-build)")
|
||||
tried: set[str] = set()
|
||||
proxies_to_try: list[str | None] = [proxy]
|
||||
last_exc: Exception | None = None
|
||||
|
||||
def _expand_proxy_candidates() -> None:
|
||||
for p in _local_proxy_candidates(cfg):
|
||||
if p and p not in tried and p not in {
|
||||
x for x in proxies_to_try if x
|
||||
}:
|
||||
proxies_to_try.append(p)
|
||||
|
||||
# 直连时先挂上本机已开端口,减少首次 403 后的空等
|
||||
if not proxy:
|
||||
_expand_proxy_candidates()
|
||||
|
||||
idx = 0
|
||||
while idx < len(proxies_to_try) and idx < max(1, max_proxy_tries):
|
||||
use_proxy = proxies_to_try[idx]
|
||||
label = _proxy_label(use_proxy)
|
||||
if idx == 0:
|
||||
log(f"[cpa] mint 出口={label}")
|
||||
else:
|
||||
log(f"[cpa] CF/403 换出口重试 ({idx + 1}/{max_proxy_tries}): {label}")
|
||||
set_runtime_proxy(use_proxy)
|
||||
tried.add(use_proxy or "")
|
||||
try:
|
||||
return mint_from_sso(
|
||||
sso_token,
|
||||
proxy=proxy,
|
||||
proxy=use_proxy,
|
||||
log=lambda m: log(f"[Debug] {m}"),
|
||||
require_referrer=bool(cfg.get("cpa_require_referrer", True)),
|
||||
)
|
||||
except OAuthCodeError as exc:
|
||||
last_exc = exc
|
||||
log(f"[!] SSO→OAuth 失败: {exc}")
|
||||
if _is_cf_mint_error(exc):
|
||||
if use_proxy:
|
||||
try:
|
||||
import proxy_pool
|
||||
|
||||
proxy_pool.report_failure(
|
||||
use_proxy, reason=f"mint CF: {str(exc)[:120]}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
_expand_proxy_candidates()
|
||||
idx += 1
|
||||
continue
|
||||
if not allow_device:
|
||||
raise
|
||||
log("[cpa] 回退设备码铸造(可能缺 referrer)")
|
||||
break
|
||||
except Exception as exc: # noqa: BLE001
|
||||
last_exc = exc
|
||||
log(f"[!] SSO→OAuth 异常: {exc}")
|
||||
if _is_cf_mint_error(exc):
|
||||
_expand_proxy_candidates()
|
||||
idx += 1
|
||||
continue
|
||||
if not allow_device:
|
||||
raise
|
||||
log("[cpa] 回退设备码铸造(可能缺 referrer)")
|
||||
break
|
||||
# 成功已 return;失败已 continue/break
|
||||
idx += 1 # pragma: no cover
|
||||
else:
|
||||
if last_exc is not None and not allow_device:
|
||||
raise last_exc
|
||||
if last_exc is not None and not allow_device:
|
||||
raise last_exc
|
||||
|
||||
if not allow_device and not sso_token:
|
||||
raise RuntimeError("无 sso cookie,且已禁用设备码回退;无法铸造带 referrer 的 token")
|
||||
|
||||
@@ -138,6 +138,39 @@ def _is_curl_tls_broken(exc: BaseException | str) -> bool:
|
||||
return any(n in text for n in needles)
|
||||
|
||||
|
||||
def is_cloudflare_block(
|
||||
status: int | None = None,
|
||||
body: str = "",
|
||||
exc: BaseException | str | None = None,
|
||||
) -> bool:
|
||||
"""识别 auth.x.ai 被 Cloudflare 拦(常见直连 403 挑战页)。"""
|
||||
text = f"{body or ''} {exc or ''}".lower()
|
||||
if status == 403 and (
|
||||
"<!doctype html" in text
|
||||
or "cloudflare" in text
|
||||
or "cf-ray" in text
|
||||
or "attention required" in text
|
||||
or "just a moment" in text
|
||||
or "enable javascript" in text
|
||||
or "oldie" in text
|
||||
):
|
||||
return True
|
||||
if "authorize http 403" in text and (
|
||||
"<!doctype" in text or "oldie" in text or "cloudflare" in text
|
||||
):
|
||||
return True
|
||||
return "cloudflare" in text and ("403" in text or "blocked" in text)
|
||||
|
||||
|
||||
def _cf_block_hint(proxy_label: str = "") -> str:
|
||||
via = proxy_label or "(direct)"
|
||||
return (
|
||||
f"Cloudflare 拦截 auth.x.ai(出口={via})。"
|
||||
"直连大陆/机房 IP 几乎必 403;请配置 mint_proxy / proxy,"
|
||||
"或开启 proxy_pool_enabled 并保证代理能访问 auth.x.ai。"
|
||||
)
|
||||
|
||||
|
||||
def _make_std_session(proxy: str | None = None):
|
||||
try:
|
||||
import requests as std_requests
|
||||
@@ -300,6 +333,10 @@ def open_authorize_page(session: Any, flow: AuthCodeFlow) -> str:
|
||||
body = resp.text or ""
|
||||
final = _final_url(resp)
|
||||
if resp.status_code < 200 or resp.status_code >= 300:
|
||||
if is_cloudflare_block(resp.status_code, body):
|
||||
raise OAuthCodeError(
|
||||
f"authorize HTTP {resp.status_code}: Cloudflare 拦截 — {_short(body, 80)}"
|
||||
)
|
||||
raise OAuthCodeError(
|
||||
f"authorize HTTP {resp.status_code}: {_short(body)}"
|
||||
)
|
||||
@@ -478,11 +515,16 @@ def sso_to_token(
|
||||
require_referrer: bool = True,
|
||||
) -> TokenResult:
|
||||
"""SSO cookie → 带 referrer=grok-build 的 OAuth token。"""
|
||||
from .proxyutil import proxy_log_label, resolve_proxy
|
||||
|
||||
log = log or _noop_log
|
||||
sso = normalize_sso_cookie(sso_cookie)
|
||||
if not sso:
|
||||
raise OAuthCodeError("sso cookie 为空")
|
||||
|
||||
resolved = resolve_proxy(proxy)
|
||||
log(f"mint 出口={proxy_log_label(resolved) or '(direct)'}")
|
||||
|
||||
# 先 curl_cffi;若遇到 OpenSSL invalid library / curl(35),自动回退 std requests
|
||||
session = _make_session(proxy, prefer="curl")
|
||||
try:
|
||||
@@ -495,6 +537,8 @@ def sso_to_token(
|
||||
backend.startswith("curl_cffi") and "curl: (35)" in str(exc).lower()
|
||||
)
|
||||
if not can_fallback:
|
||||
if is_cloudflare_block(exc=exc):
|
||||
raise OAuthCodeError(_cf_block_hint(proxy_log_label(resolved))) from exc
|
||||
raise
|
||||
log(f"curl TLS 异常,回退标准 requests: {_short(str(exc), 160)}")
|
||||
try:
|
||||
@@ -506,6 +550,10 @@ def sso_to_token(
|
||||
return _run_sso_flow(
|
||||
sso, session=session, log=log, require_referrer=require_referrer
|
||||
)
|
||||
except Exception as exc2: # noqa: BLE001
|
||||
if is_cloudflare_block(exc=exc2):
|
||||
raise OAuthCodeError(_cf_block_hint(proxy_log_label(resolved))) from exc2
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
session.close()
|
||||
|
||||
Reference in new issue
Block a user