Files
grok-keygen-new/cpa_export.py
T
chaos 7d6d52ac5d 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
2026-07-14 14:41:52 +08:00

362 lines
12 KiB
Python
Raw 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.
"""注册成功钩子:SSO → OAuth Authorization Code(PKCE, referrer=grok-build)
→ 写出 CPA (CLIProxyAPI) 的 xai-<email>.json → 推送到远端 CLIProxyAPI 导入。
- 本地写盘目录:config['cpa_auth_dir'](默认 ./cpa_auths)
- 远端推送:POST config['cpa_remote_base'] + /v0/management/auth-files?name=...
认证 X-Management-Key: config['cpa_remote_secret']
2026-07 起:设备码铸造的 token 缺 referrer=grok-build,cli-chat-proxy 不可用。
默认优先走 SSO cookie 的授权码流程;仅在无 sso 且允许时才回退设备码。
"""
from __future__ import annotations
import os
import time
from pathlib import Path
from typing import Any, Callable
_REG_DIR = Path(__file__).resolve().parent
_DEFAULT_OUT = _REG_DIR / "cpa_auths"
def _resolve_out_dir(cfg: dict) -> Path:
raw = str(cfg.get("cpa_auth_dir") or _DEFAULT_OUT).strip()
p = Path(raw).expanduser()
if not p.is_absolute():
p = (_REG_DIR / p).resolve()
return p
def _record_failure(out_dir: Path, email: str, reason: str) -> None:
try:
out_dir.mkdir(parents=True, exist_ok=True)
with open(out_dir / "cpa_auth_failed.txt", "a", encoding="utf-8") as f:
f.write(f"{email}----{reason}----{int(time.time())}\n")
except Exception:
pass
def _resolve_proxy(cfg: dict) -> str | None:
from oidc_mint import resolve_proxy, set_runtime_proxy
# 1) 显式 mint_proxy / proxy
proxy = (cfg.get("mint_proxy") or cfg.get("proxy") or "").strip()
# 2) 注册机代理池线程绑定(与浏览器同出口)
if not proxy:
try:
import proxy_pool
proxy = (proxy_pool.get_thread_proxy() or "").strip()
except Exception:
proxy = ""
# 3) 环境变量
if not proxy:
proxy = (
os.environ.get("https_proxy")
or os.environ.get("HTTPS_PROXY")
or os.environ.get("http_proxy")
or ""
).strip()
resolved = resolve_proxy(proxy or None)
set_runtime_proxy(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(
*,
email: str,
password: str,
sso: str,
page: Any | None,
cfg: dict,
log: Callable[[str], None],
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=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")
# 设备码回退(旧路径,通常无 referrer)
from oidc_mint import mint_with_browser
if page is None and not (email and password):
raise RuntimeError("设备码回退需要 page 或 email/password")
log("[cpa] 使用设备码铸造(兼容路径)")
tokens = mint_with_browser(
email=email,
password=password,
page=page,
proxy=proxy,
browser_timeout_sec=timeout,
force_standalone=(page is None),
cookies=None,
poll_log=lambda m: log(f"[Debug] {m}"),
)
return tokens
# ── 主入口 ──
def export_cpa_for_account(
email: str,
password: str = "",
*,
page: Any | None = None,
sso: str = "",
config: dict | None = None,
log_callback: Callable[[str], None] | None = None,
) -> dict:
"""铸造 OIDC → 写本地 xai-<email>.json → 推送远端 CLIProxyAPI。
优先使用 sso cookie 走 Authorization Code + referrer=grok-build。
返回 {ok, email, path, pushed, push_status?, error?}。
"""
cfg = config or {}
log = log_callback or (lambda m: print(m, flush=True))
if not cfg.get("cpa_export_enabled", True):
log("[cpa] 已关闭导出,跳过")
return {"ok": False, "skipped": True, "reason": "disabled"}
email = (email or "").strip()
if not email:
return {"ok": False, "error": "缺少 email", "email": email}
import cpa
from oidc_mint.oauth_code import normalize_sso_cookie
out_dir = _resolve_out_dir(cfg)
proxy = _resolve_proxy(cfg)
base_url = cfg.get("cpa_base_url") or cpa.CLI_BASE_URL
sso_token = normalize_sso_cookie(sso or "")
try:
tokens = _mint_tokens(
email=email,
password=password or "",
sso=sso_token,
page=page,
cfg=cfg,
log=log,
proxy=proxy,
)
except Exception as exc: # noqa: BLE001
log(f"[!] 铸造失败: {exc}")
_record_failure(out_dir, email, str(exc))
if cfg.get("mint_required", False):
raise
return {"ok": False, "error": str(exc), "email": email}
try:
payload = cpa.build_cpa_xai_auth(
email=email,
access_token=tokens["access_token"],
refresh_token=tokens["refresh_token"],
id_token=tokens.get("id_token"),
expires_in=tokens.get("expires_in"),
base_url=base_url,
sso=tokens.get("sso") or sso_token or None,
)
path = cpa.write_cpa_xai_auth(out_dir, payload)
filename = Path(path).name
except Exception as exc: # noqa: BLE001
log(f"[!] 写本地文件失败: {exc}")
_record_failure(out_dir, email, f"write: {exc}")
if cfg.get("mint_required", False):
raise
return {"ok": False, "error": str(exc), "email": email}
ref = payload.get("referrer") or tokens.get("referrer") or ""
log(f"[Debug] 已写本地: {path} referrer={ref or '(empty)'}")
result: dict[str, Any] = {
"ok": True,
"email": email,
"path": str(path),
"pushed": False,
"referrer": ref,
}
# 推送远端 CLIProxyAPI(失败记入 cpa_push_pending.txt,下次推送时一并重试)
if cfg.get("cpa_push_enabled", False):
remote_base = str(cfg.get("cpa_remote_base") or "").strip()
secret = str(cfg.get("cpa_remote_secret") or "").strip()
if not remote_base or not secret:
log("[!] 推送已开启但未配置 cpa_remote_base/cpa_remote_secret,跳过推送")
cpa.record_push_failure(out_dir, filename, "remote not configured")
else:
push_proxy = str(cfg.get("cpa_push_proxy") or "").strip() or None
verify_tls = bool(cfg.get("cpa_remote_verify_tls", True))
push_res = cpa.push_with_queue(
out_dir,
filename,
payload,
remote_base=remote_base,
secret=secret,
proxy=push_proxy,
verify_tls=verify_tls,
flush_first=True,
log=log,
)
result["pushed"] = bool(push_res.get("pushed"))
if "push_status" in push_res:
result["push_status"] = push_res["push_status"]
if push_res.get("push_error"):
result["push_error"] = push_res["push_error"]
if push_res.get("flush"):
result["push_flush"] = push_res["flush"]
if not result["pushed"] and cfg.get("cpa_push_required", False):
result["ok"] = False
result["error"] = (
f"push: {result.get('push_error') or result.get('push_status')}"
)
return result