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
|
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(
|
def _mint_tokens(
|
||||||
*,
|
*,
|
||||||
email: str,
|
email: str,
|
||||||
@@ -73,32 +139,87 @@ def _mint_tokens(
|
|||||||
proxy: str | None,
|
proxy: str | None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""优先 SSO 授权码;可选回退设备码。"""
|
"""优先 SSO 授权码;可选回退设备码。"""
|
||||||
|
from oidc_mint import set_runtime_proxy
|
||||||
from oidc_mint.oauth_code import OAuthCodeError, mint_from_sso, normalize_sso_cookie
|
from oidc_mint.oauth_code import OAuthCodeError, mint_from_sso, normalize_sso_cookie
|
||||||
|
|
||||||
sso_token = normalize_sso_cookie(sso or "")
|
sso_token = normalize_sso_cookie(sso or "")
|
||||||
prefer_sso = bool(cfg.get("cpa_prefer_sso_oauth", True))
|
prefer_sso = bool(cfg.get("cpa_prefer_sso_oauth", True))
|
||||||
allow_device = bool(cfg.get("cpa_allow_device_fallback", False))
|
allow_device = bool(cfg.get("cpa_allow_device_fallback", False))
|
||||||
timeout = float(cfg.get("mint_timeout_sec", 300) or 300)
|
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:
|
if prefer_sso and sso_token:
|
||||||
log("[cpa] 使用 SSO→OAuth(PKCE, referrer=grok-build)")
|
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:
|
try:
|
||||||
return mint_from_sso(
|
return mint_from_sso(
|
||||||
sso_token,
|
sso_token,
|
||||||
proxy=proxy,
|
proxy=use_proxy,
|
||||||
log=lambda m: log(f"[Debug] {m}"),
|
log=lambda m: log(f"[Debug] {m}"),
|
||||||
require_referrer=bool(cfg.get("cpa_require_referrer", True)),
|
require_referrer=bool(cfg.get("cpa_require_referrer", True)),
|
||||||
)
|
)
|
||||||
except OAuthCodeError as exc:
|
except OAuthCodeError as exc:
|
||||||
|
last_exc = exc
|
||||||
log(f"[!] SSO→OAuth 失败: {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:
|
if not allow_device:
|
||||||
raise
|
raise
|
||||||
log("[cpa] 回退设备码铸造(可能缺 referrer)")
|
log("[cpa] 回退设备码铸造(可能缺 referrer)")
|
||||||
|
break
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
|
last_exc = exc
|
||||||
log(f"[!] SSO→OAuth 异常: {exc}")
|
log(f"[!] SSO→OAuth 异常: {exc}")
|
||||||
|
if _is_cf_mint_error(exc):
|
||||||
|
_expand_proxy_candidates()
|
||||||
|
idx += 1
|
||||||
|
continue
|
||||||
if not allow_device:
|
if not allow_device:
|
||||||
raise
|
raise
|
||||||
log("[cpa] 回退设备码铸造(可能缺 referrer)")
|
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:
|
if not allow_device and not sso_token:
|
||||||
raise RuntimeError("无 sso cookie,且已禁用设备码回退;无法铸造带 referrer 的 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)
|
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):
|
def _make_std_session(proxy: str | None = None):
|
||||||
try:
|
try:
|
||||||
import requests as std_requests
|
import requests as std_requests
|
||||||
@@ -300,6 +333,10 @@ def open_authorize_page(session: Any, flow: AuthCodeFlow) -> str:
|
|||||||
body = resp.text or ""
|
body = resp.text or ""
|
||||||
final = _final_url(resp)
|
final = _final_url(resp)
|
||||||
if resp.status_code < 200 or resp.status_code >= 300:
|
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(
|
raise OAuthCodeError(
|
||||||
f"authorize HTTP {resp.status_code}: {_short(body)}"
|
f"authorize HTTP {resp.status_code}: {_short(body)}"
|
||||||
)
|
)
|
||||||
@@ -478,11 +515,16 @@ def sso_to_token(
|
|||||||
require_referrer: bool = True,
|
require_referrer: bool = True,
|
||||||
) -> TokenResult:
|
) -> TokenResult:
|
||||||
"""SSO cookie → 带 referrer=grok-build 的 OAuth token。"""
|
"""SSO cookie → 带 referrer=grok-build 的 OAuth token。"""
|
||||||
|
from .proxyutil import proxy_log_label, resolve_proxy
|
||||||
|
|
||||||
log = log or _noop_log
|
log = log or _noop_log
|
||||||
sso = normalize_sso_cookie(sso_cookie)
|
sso = normalize_sso_cookie(sso_cookie)
|
||||||
if not sso:
|
if not sso:
|
||||||
raise OAuthCodeError("sso cookie 为空")
|
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
|
# 先 curl_cffi;若遇到 OpenSSL invalid library / curl(35),自动回退 std requests
|
||||||
session = _make_session(proxy, prefer="curl")
|
session = _make_session(proxy, prefer="curl")
|
||||||
try:
|
try:
|
||||||
@@ -495,6 +537,8 @@ def sso_to_token(
|
|||||||
backend.startswith("curl_cffi") and "curl: (35)" in str(exc).lower()
|
backend.startswith("curl_cffi") and "curl: (35)" in str(exc).lower()
|
||||||
)
|
)
|
||||||
if not can_fallback:
|
if not can_fallback:
|
||||||
|
if is_cloudflare_block(exc=exc):
|
||||||
|
raise OAuthCodeError(_cf_block_hint(proxy_log_label(resolved))) from exc
|
||||||
raise
|
raise
|
||||||
log(f"curl TLS 异常,回退标准 requests: {_short(str(exc), 160)}")
|
log(f"curl TLS 异常,回退标准 requests: {_short(str(exc), 160)}")
|
||||||
try:
|
try:
|
||||||
@@ -506,6 +550,10 @@ def sso_to_token(
|
|||||||
return _run_sso_flow(
|
return _run_sso_flow(
|
||||||
sso, session=session, log=log, require_referrer=require_referrer
|
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:
|
finally:
|
||||||
try:
|
try:
|
||||||
session.close()
|
session.close()
|
||||||
|
|||||||
Reference in new issue
Block a user