Revert mint path to pre-browser PKCE baseline (2e833f2).
Restore HTTP-only SSO OAuth with curl_cffi then std requests fallback; remove browser PKCE prefer/fallback knobs and mint_from_sso_browser.
This commit is contained in:
1 parent
d74d14d3f4
commit
4abdc8aa4a
5 files changed
+51
-751
No files matched your search
@@ -40,9 +40,6 @@
|
|||||||
"cpa_push_required": false,
|
"cpa_push_required": false,
|
||||||
"cpa_prefer_sso_oauth": true,
|
"cpa_prefer_sso_oauth": true,
|
||||||
"cpa_require_referrer": true,
|
"cpa_require_referrer": true,
|
||||||
"cpa_prefer_browser_mint": false,
|
|
||||||
"cpa_allow_browser_fallback": false,
|
|
||||||
"cpa_browser_mint_timeout_sec": 90,
|
|
||||||
"cpa_allow_device_fallback": false,
|
"cpa_allow_device_fallback": false,
|
||||||
"mint_proxy": "",
|
"mint_proxy": "",
|
||||||
"mint_timeout_sec": 300,
|
"mint_timeout_sec": 300,
|
||||||
|
|||||||
+9
-59
@@ -72,49 +72,13 @@ def _mint_tokens(
|
|||||||
log: Callable[[str], None],
|
log: Callable[[str], None],
|
||||||
proxy: str | None,
|
proxy: str | None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""铸造策略(仅 HTTP;浏览器/设备码默认关):
|
"""优先 SSO 授权码;可选回退设备码。"""
|
||||||
|
from oidc_mint.oauth_code import OAuthCodeError, mint_from_sso, normalize_sso_cookie
|
||||||
1. 仅当 cpa_prefer_browser_mint=true 且有 page:先浏览器 PKCE
|
|
||||||
2. HTTP SSO→OAuth(curl_cffi,短超时)— 默认唯一主路径
|
|
||||||
3. 仅当 cpa_allow_browser_fallback=true 且 HTTP 失败:浏览器 PKCE
|
|
||||||
4. 可选设备码(通常无 referrer,默认关)
|
|
||||||
"""
|
|
||||||
from oidc_mint.oauth_code import (
|
|
||||||
OAuthCodeError,
|
|
||||||
_is_http_tls_failure,
|
|
||||||
mint_from_sso,
|
|
||||||
mint_from_sso_browser,
|
|
||||||
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_browser = bool(cfg.get("cpa_allow_browser_fallback", False))
|
|
||||||
# 默认 False:不走浏览器优先
|
|
||||||
prefer_browser = bool(cfg.get("cpa_prefer_browser_mint", False))
|
|
||||||
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)
|
||||||
require_ref = bool(cfg.get("cpa_require_referrer", True))
|
|
||||||
http_err: Exception | None = None
|
|
||||||
browser_timeout = min(timeout, float(cfg.get("cpa_browser_mint_timeout_sec", 90) or 90))
|
|
||||||
|
|
||||||
def _browser_mint(reason: str) -> dict[str, Any]:
|
|
||||||
log(f"[cpa] 浏览器 PKCE 铸造({reason})")
|
|
||||||
return mint_from_sso_browser(
|
|
||||||
sso_token,
|
|
||||||
page,
|
|
||||||
log=lambda m: log(f"[Debug] {m}"),
|
|
||||||
require_referrer=require_ref,
|
|
||||||
timeout_sec=browser_timeout,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 路径 A:有 page 时优先浏览器(最稳,不依赖 Python→auth.x.ai 直连)
|
|
||||||
if prefer_sso and sso_token and allow_browser and page is not None and prefer_browser:
|
|
||||||
try:
|
|
||||||
return _browser_mint("优先浏览器,绕开 Python 直连 auth.x.ai")
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
log(f"[!] 浏览器 PKCE 优先路径失败: {exc}")
|
|
||||||
log("[cpa] 继续尝试 HTTP SSO→OAuth")
|
|
||||||
|
|
||||||
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)")
|
||||||
@@ -123,35 +87,21 @@ def _mint_tokens(
|
|||||||
sso_token,
|
sso_token,
|
||||||
proxy=proxy,
|
proxy=proxy,
|
||||||
log=lambda m: log(f"[Debug] {m}"),
|
log=lambda m: log(f"[Debug] {m}"),
|
||||||
require_referrer=require_ref,
|
require_referrer=bool(cfg.get("cpa_require_referrer", True)),
|
||||||
)
|
)
|
||||||
except OAuthCodeError as exc:
|
except OAuthCodeError as exc:
|
||||||
http_err = exc
|
|
||||||
log(f"[!] SSO→OAuth 失败: {exc}")
|
log(f"[!] SSO→OAuth 失败: {exc}")
|
||||||
|
if not allow_device:
|
||||||
|
raise
|
||||||
|
log("[cpa] 回退设备码铸造(可能缺 referrer)")
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
http_err = exc
|
|
||||||
log(f"[!] SSO→OAuth 异常: {exc}")
|
log(f"[!] SSO→OAuth 异常: {exc}")
|
||||||
|
if not allow_device:
|
||||||
# HTTP 失败后:有 page+sso 再试浏览器(若优先路径没走过或当时失败)
|
raise
|
||||||
if allow_browser and page is not None and sso_token and http_err is not None:
|
log("[cpa] 回退设备码铸造(可能缺 referrer)")
|
||||||
why = "TLS/连接/超时" if _is_http_tls_failure(http_err) else "HTTP"
|
|
||||||
try:
|
|
||||||
return _browser_mint(f"{why}失败后回退")
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
log(f"[!] 浏览器 PKCE 失败: {exc}")
|
|
||||||
if not allow_device:
|
|
||||||
raise
|
|
||||||
log("[cpa] 继续回退设备码铸造(可能缺 referrer)")
|
|
||||||
elif http_err is not None and not allow_device:
|
|
||||||
raise http_err
|
|
||||||
|
|
||||||
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")
|
||||||
if not allow_device:
|
|
||||||
# 有 sso 但 browser 也没开/没 page
|
|
||||||
if http_err is not None:
|
|
||||||
raise http_err
|
|
||||||
raise RuntimeError("SSO 铸造失败,且未启用任何回退")
|
|
||||||
|
|
||||||
# 设备码回退(旧路径,通常无 referrer)
|
# 设备码回退(旧路径,通常无 referrer)
|
||||||
from oidc_mint import mint_with_browser
|
from oidc_mint import mint_with_browser
|
||||||
|
|||||||
+12
-17
@@ -85,9 +85,6 @@ DEFAULT_CONFIG = {
|
|||||||
# OIDC:优先 SSO→Authorization Code + referrer=grok-build
|
# OIDC:优先 SSO→Authorization Code + referrer=grok-build
|
||||||
"cpa_prefer_sso_oauth": True, # True=用 sso cookie 走 PKCE(必须带 referrer)
|
"cpa_prefer_sso_oauth": True, # True=用 sso cookie 走 PKCE(必须带 referrer)
|
||||||
"cpa_require_referrer": True, # True=access_token 无 referrer=grok-build 则失败
|
"cpa_require_referrer": True, # True=access_token 无 referrer=grok-build 则失败
|
||||||
"cpa_prefer_browser_mint": False, # True=有 page 时先浏览器(慢);默认关
|
|
||||||
"cpa_allow_browser_fallback": False, # True=HTTP 失败再用浏览器 PKCE;默认关(不要网页铸造)
|
|
||||||
"cpa_browser_mint_timeout_sec": 90, # 浏览器 PKCE 最长等待(仅 fallback 开启时)
|
|
||||||
"cpa_allow_device_fallback": False, # True=SSO 失败时回退设备码(通常不可用)
|
"cpa_allow_device_fallback": False, # True=SSO 失败时回退设备码(通常不可用)
|
||||||
# OIDC 铸造代理/超时
|
# OIDC 铸造代理/超时
|
||||||
"mint_proxy": "", # 铸造专用代理;空=复用 proxy
|
"mint_proxy": "", # 铸造专用代理;空=复用 proxy
|
||||||
@@ -2098,7 +2095,6 @@ def update_nsfw_settings(session, log_callback=None):
|
|||||||
|
|
||||||
|
|
||||||
def _is_curl_tls_error(exc) -> bool:
|
def _is_curl_tls_error(exc) -> bool:
|
||||||
"""curl_cffi 库损坏 / TLS / 连接超时:可尝试换后端或记失败,勿当业务错误。"""
|
|
||||||
text = str(exc or "").lower()
|
text = str(exc or "").lower()
|
||||||
return any(
|
return any(
|
||||||
n in text
|
n in text
|
||||||
@@ -2106,16 +2102,9 @@ def _is_curl_tls_error(exc) -> bool:
|
|||||||
"openssl_internal:invalid library",
|
"openssl_internal:invalid library",
|
||||||
"tls connect error",
|
"tls connect error",
|
||||||
"curl: (35)",
|
"curl: (35)",
|
||||||
"curl: (28)",
|
|
||||||
"failed to perform, curl: (35)",
|
"failed to perform, curl: (35)",
|
||||||
"failed to perform, curl: (28)",
|
|
||||||
"connection timed out",
|
|
||||||
"ssl_error_syscall",
|
"ssl_error_syscall",
|
||||||
"ssl connect error",
|
"ssl connect error",
|
||||||
"readtimeout",
|
|
||||||
"connecttimeout",
|
|
||||||
"timed out",
|
|
||||||
"timeout",
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2176,18 +2165,24 @@ def enable_nsfw_for_token(token, cf_clearance="", log_callback=None):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 仅用 curl_cffi:本机实测 std requests 对 accounts.x.ai/auth.x.ai 更易 ReadTimeout
|
|
||||||
try:
|
try:
|
||||||
session = _make_post_reg_session(proxies, prefer="curl")
|
session = _make_post_reg_session(proxies, prefer="curl")
|
||||||
try:
|
try:
|
||||||
return _run(session)
|
return _run(session)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if _is_curl_tls_error(exc):
|
if not _is_curl_tls_error(exc):
|
||||||
log(f"[Debug] set_tos/nsfw 网络/TLS 失败(不回退 requests): {exc}")
|
return False, f"异常: {exc}"
|
||||||
return False, f"网络/TLS: {exc}"
|
log(f"[Debug] curl TLS 异常,set_tos/nsfw 回退 requests: {exc}")
|
||||||
return False, f"异常: {exc}"
|
|
||||||
finally:
|
|
||||||
_close(session)
|
_close(session)
|
||||||
|
session = _make_post_reg_session(proxies, prefer="requests")
|
||||||
|
try:
|
||||||
|
return _run(session)
|
||||||
|
finally:
|
||||||
|
_close(session)
|
||||||
|
session = None
|
||||||
|
finally:
|
||||||
|
if session is not None:
|
||||||
|
_close(session)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, f"异常: {str(e)}"
|
return False, f"异常: {str(e)}"
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ from .oauth_code import (
|
|||||||
OAuthCodeError,
|
OAuthCodeError,
|
||||||
SCOPE as CODE_SCOPE,
|
SCOPE as CODE_SCOPE,
|
||||||
mint_from_sso,
|
mint_from_sso,
|
||||||
mint_from_sso_browser,
|
|
||||||
normalize_sso_cookie,
|
normalize_sso_cookie,
|
||||||
sso_to_token,
|
sso_to_token,
|
||||||
)
|
)
|
||||||
@@ -27,7 +26,6 @@ __all__ = [
|
|||||||
"mint_with_browser",
|
"mint_with_browser",
|
||||||
"shutdown_mint_browsers",
|
"shutdown_mint_browsers",
|
||||||
"mint_from_sso",
|
"mint_from_sso",
|
||||||
"mint_from_sso_browser",
|
|
||||||
"sso_to_token",
|
"sso_to_token",
|
||||||
"normalize_sso_cookie",
|
"normalize_sso_cookie",
|
||||||
"CLIENT_ID",
|
"CLIENT_ID",
|
||||||
|
|||||||
+30
-670
@@ -260,10 +260,6 @@ def _final_url(resp: Any) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
# HTTP 铸造单步超时:直连/跨境链路差时不要卡 30s,尽快让上层走浏览器
|
|
||||||
HTTP_STEP_TIMEOUT = 12
|
|
||||||
|
|
||||||
|
|
||||||
def open_authorize_page(session: Any, flow: AuthCodeFlow) -> str:
|
def open_authorize_page(session: Any, flow: AuthCodeFlow) -> str:
|
||||||
params = {
|
params = {
|
||||||
"response_type": "code",
|
"response_type": "code",
|
||||||
@@ -281,7 +277,7 @@ def open_authorize_page(session: Any, flow: AuthCodeFlow) -> str:
|
|||||||
url,
|
url,
|
||||||
headers=_browser_headers("GET", url),
|
headers=_browser_headers("GET", url),
|
||||||
allow_redirects=True,
|
allow_redirects=True,
|
||||||
timeout=HTTP_STEP_TIMEOUT,
|
timeout=30,
|
||||||
)
|
)
|
||||||
body = resp.text or ""
|
body = resp.text or ""
|
||||||
final = _final_url(resp)
|
final = _final_url(resp)
|
||||||
@@ -359,7 +355,7 @@ def approve_authorization(session: Any, consent_url: str, flow: AuthCodeFlow) ->
|
|||||||
data=body.encode("utf-8"),
|
data=body.encode("utf-8"),
|
||||||
headers=_browser_headers("POST", consent_url, NEXT_ACTION_ID),
|
headers=_browser_headers("POST", consent_url, NEXT_ACTION_ID),
|
||||||
allow_redirects=True,
|
allow_redirects=True,
|
||||||
timeout=HTTP_STEP_TIMEOUT,
|
timeout=30,
|
||||||
)
|
)
|
||||||
text = resp.text or ""
|
text = resp.text or ""
|
||||||
if resp.status_code < 200 or resp.status_code >= 300:
|
if resp.status_code < 200 or resp.status_code >= 300:
|
||||||
@@ -388,7 +384,7 @@ def exchange_auth_code(session: Any, code: str, flow: AuthCodeFlow) -> TokenResu
|
|||||||
TOKEN_URL,
|
TOKEN_URL,
|
||||||
data=urlencode(form),
|
data=urlencode(form),
|
||||||
headers=_token_headers(),
|
headers=_token_headers(),
|
||||||
timeout=HTTP_STEP_TIMEOUT,
|
timeout=30,
|
||||||
)
|
)
|
||||||
text = resp.text or ""
|
text = resp.text or ""
|
||||||
if resp.status_code < 200 or resp.status_code >= 300:
|
if resp.status_code < 200 or resp.status_code >= 300:
|
||||||
@@ -458,27 +454,47 @@ def sso_to_token(
|
|||||||
log: LogFn | None = None,
|
log: LogFn | None = None,
|
||||||
require_referrer: bool = True,
|
require_referrer: bool = True,
|
||||||
) -> TokenResult:
|
) -> TokenResult:
|
||||||
"""SSO cookie → 带 referrer=grok-build 的 OAuth token。
|
"""SSO cookie → 带 referrer=grok-build 的 OAuth token。"""
|
||||||
|
|
||||||
仅用 curl_cffi(Chrome TLS)。不再回退 std requests:
|
|
||||||
实测 requests 访问 auth.x.ai / accounts.x.ai 常 ReadTimeout,比 curl 更差。
|
|
||||||
连接/TLS/超时由上层切到浏览器 PKCE。
|
|
||||||
"""
|
|
||||||
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 为空")
|
||||||
|
|
||||||
|
# 先 curl_cffi;若遇到 OpenSSL invalid library / curl(35),自动回退 std requests
|
||||||
session = _make_session(proxy, prefer="curl")
|
session = _make_session(proxy, prefer="curl")
|
||||||
try:
|
try:
|
||||||
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
|
||||||
)
|
)
|
||||||
finally:
|
except Exception as exc: # noqa: BLE001
|
||||||
|
backend = str(getattr(session, "_cpa_http_backend", "") or "")
|
||||||
|
can_fallback = _is_curl_tls_broken(exc) or (
|
||||||
|
backend.startswith("curl_cffi") and "curl: (35)" in str(exc).lower()
|
||||||
|
)
|
||||||
|
if not can_fallback:
|
||||||
|
raise
|
||||||
|
log(f"curl TLS 异常,回退标准 requests: {_short(str(exc), 160)}")
|
||||||
try:
|
try:
|
||||||
session.close()
|
session.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
session = _make_session(proxy, prefer="requests")
|
||||||
|
try:
|
||||||
|
return _run_sso_flow(
|
||||||
|
sso, session=session, log=log, require_referrer=require_referrer
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
session.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
session = None # type: ignore[assignment]
|
||||||
|
finally:
|
||||||
|
if session is not None:
|
||||||
|
try:
|
||||||
|
session.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def mint_from_sso(
|
def mint_from_sso(
|
||||||
@@ -504,659 +520,3 @@ def mint_from_sso(
|
|||||||
"referrer": tr.referrer,
|
"referrer": tr.referrer,
|
||||||
"sso": normalize_sso_cookie(sso_cookie),
|
"sso": normalize_sso_cookie(sso_cookie),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _is_http_tls_failure(exc: BaseException | str) -> bool:
|
|
||||||
"""HTTP 层 TLS/连接/超时失败:适合改走浏览器铸造。"""
|
|
||||||
text = str(exc or "").lower()
|
|
||||||
needles = (
|
|
||||||
"unexpected_eof_while_reading",
|
|
||||||
"sslerror",
|
|
||||||
"ssleoferror",
|
|
||||||
"max retries exceeded",
|
|
||||||
"openssl_internal:invalid library",
|
|
||||||
"tls connect error",
|
|
||||||
"curl: (35)",
|
|
||||||
"curl: (28)",
|
|
||||||
"failed to perform, curl: (35)",
|
|
||||||
"failed to perform, curl: (28)",
|
|
||||||
"connection timed out",
|
|
||||||
"ssl_error_syscall",
|
|
||||||
"connection reset",
|
|
||||||
"connection aborted",
|
|
||||||
"name resolution",
|
|
||||||
"readtimeout",
|
|
||||||
"connecttimeout",
|
|
||||||
"timed out",
|
|
||||||
"timeout",
|
|
||||||
)
|
|
||||||
return any(n in text for n in needles)
|
|
||||||
|
|
||||||
|
|
||||||
def _page_eval(page: Any, js: str, *args: Any) -> Any:
|
|
||||||
"""兼容 DrissionPage page.run_js / page.run_js_loaded。"""
|
|
||||||
if page is None:
|
|
||||||
raise OAuthCodeError("page 为空,无法浏览器铸造")
|
|
||||||
last_err: Exception | None = None
|
|
||||||
for name in ("run_js", "run_js_loaded", "run_async_js"):
|
|
||||||
fn = getattr(page, name, None)
|
|
||||||
if not callable(fn):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
if args:
|
|
||||||
return fn(js, *args)
|
|
||||||
return fn(js)
|
|
||||||
except TypeError:
|
|
||||||
# 某些签名不接受额外参数
|
|
||||||
try:
|
|
||||||
return fn(js)
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
last_err = exc
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
last_err = exc
|
|
||||||
continue
|
|
||||||
raise OAuthCodeError(f"page 无法执行 JS: {last_err or 'no run_js'}")
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_sso_on_page(page: Any, sso: str, log: LogFn) -> None:
|
|
||||||
sso = normalize_sso_cookie(sso)
|
|
||||||
if not sso:
|
|
||||||
raise OAuthCodeError("sso cookie 为空")
|
|
||||||
# 先落到 accounts 域,再写 cookie,避免 set 失败
|
|
||||||
for url in ("https://accounts.x.ai/", "https://auth.x.ai/"):
|
|
||||||
try:
|
|
||||||
page.get(url)
|
|
||||||
time.sleep(0.25)
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
log(f"open {url} warn: {exc}")
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for domain in (".x.ai", "accounts.x.ai", ".accounts.x.ai", "auth.x.ai", ".auth.x.ai"):
|
|
||||||
for name in ("sso", "sso-rw"):
|
|
||||||
items.append(
|
|
||||||
{
|
|
||||||
"name": name,
|
|
||||||
"value": sso,
|
|
||||||
"domain": domain,
|
|
||||||
"path": "/",
|
|
||||||
"secure": True,
|
|
||||||
"sameSite": "None",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
set_ok = False
|
|
||||||
# 优先 CDP/DrissionPage set.cookies(可写 httpOnly 域 cookie)
|
|
||||||
for target in (page, getattr(page, "browser", None)):
|
|
||||||
if target is None:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
setter = getattr(target, "set", None)
|
|
||||||
cookies_fn = getattr(setter, "cookies", None) if setter is not None else None
|
|
||||||
if not callable(cookies_fn):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
cookies_fn(items)
|
|
||||||
set_ok = True
|
|
||||||
log(f"browser sso cookie set bulk via {type(target).__name__}")
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
n = 0
|
|
||||||
for it in items:
|
|
||||||
try:
|
|
||||||
cookies_fn(it)
|
|
||||||
n += 1
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if n:
|
|
||||||
set_ok = True
|
|
||||||
log(f"browser sso cookie set one-by-one={n}")
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# document.cookie 兜底(非 httpOnly)
|
|
||||||
set_js = r"""
|
|
||||||
(sso) => {
|
|
||||||
try {
|
|
||||||
const maxAge = 60 * 60 * 24 * 30;
|
|
||||||
const base = `; path=/; max-age=${maxAge}; SameSite=None; Secure`;
|
|
||||||
document.cookie = `sso=${sso}${base}; domain=.x.ai`;
|
|
||||||
document.cookie = `sso-rw=${sso}${base}; domain=.x.ai`;
|
|
||||||
document.cookie = `sso=${sso}${base}`;
|
|
||||||
document.cookie = `sso-rw=${sso}${base}`;
|
|
||||||
return document.cookie.includes('sso=');
|
|
||||||
} catch (e) {
|
|
||||||
return String(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
ok = _page_eval(page, set_js, sso)
|
|
||||||
log(f"browser sso document.cookie: {ok!r}")
|
|
||||||
set_ok = set_ok or (ok is True) or (ok == True) or (str(ok).lower() == "true")
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
log(f"browser sso document.cookie fail: {exc}")
|
|
||||||
|
|
||||||
if not set_ok:
|
|
||||||
raise OAuthCodeError("写入 sso cookie 失败")
|
|
||||||
|
|
||||||
|
|
||||||
def _page_html(page: Any) -> str:
|
|
||||||
try:
|
|
||||||
html = str(getattr(page, "html", "") or "")
|
|
||||||
if html:
|
|
||||||
return html
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
return str(_page_eval(page, "() => document.documentElement.outerHTML") or "")
|
|
||||||
except Exception:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_next_action_id(html: str) -> str:
|
|
||||||
"""从 consent 页 HTML 抽 Next-Action / Server Action id。"""
|
|
||||||
text = html or ""
|
|
||||||
patterns = (
|
|
||||||
r'"next-action"\s*:\s*"([a-f0-9]{20,})"',
|
|
||||||
r'"actionId"\s*:\s*"([a-f0-9]{20,})"',
|
|
||||||
r'\$ACTION_ID_([a-f0-9]{20,})',
|
|
||||||
r'next-action["\']?\s*[:=]\s*["\']([a-f0-9]{20,})',
|
|
||||||
)
|
|
||||||
for pat in patterns:
|
|
||||||
m = re.search(pat, text, re.I)
|
|
||||||
if m:
|
|
||||||
return m.group(1)
|
|
||||||
return NEXT_ACTION_ID
|
|
||||||
|
|
||||||
|
|
||||||
def _browser_click_allow(page: Any, log: LogFn) -> bool:
|
|
||||||
"""Consent「允许」必须真实点击(JS click 会导致 Invalid action)。"""
|
|
||||||
labels = ("允许", "Allow", "Authorize", "Approve", "授权")
|
|
||||||
# 1) DrissionPage 真实点击(与 device consent 同一套经验)
|
|
||||||
try:
|
|
||||||
candidates = []
|
|
||||||
for sel in ("tag:button", "css:button", "css:[role='button']", "css:input[type='submit']"):
|
|
||||||
try:
|
|
||||||
found = page.eles(sel, timeout=0.3) or []
|
|
||||||
except Exception:
|
|
||||||
found = []
|
|
||||||
for el in found:
|
|
||||||
try:
|
|
||||||
t = (getattr(el, "text", None) or el.raw_text or "").strip()
|
|
||||||
except Exception:
|
|
||||||
t = ""
|
|
||||||
if not t:
|
|
||||||
continue
|
|
||||||
compact = re.sub(r"\s+", "", t)
|
|
||||||
if compact in labels or t in labels:
|
|
||||||
candidates.append((0, el, t))
|
|
||||||
elif any(x in compact for x in labels) and "全部" not in compact and "All" not in compact:
|
|
||||||
candidates.append((1, el, t))
|
|
||||||
candidates.sort(key=lambda x: x[0])
|
|
||||||
for _, el, t in candidates:
|
|
||||||
try:
|
|
||||||
el.click() # real click
|
|
||||||
log(f"browser REAL click allow: {t!r}")
|
|
||||||
return True
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
log(f"real click {t!r} failed: {exc}")
|
|
||||||
try:
|
|
||||||
el.click(by_js=True)
|
|
||||||
log(f"browser JS click allow: {t!r}")
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
log(f"ele allow click failed: {exc}")
|
|
||||||
|
|
||||||
# 2) 表单 action=allow 后 submit(device consent 兜底同款)
|
|
||||||
try:
|
|
||||||
ret = _page_eval(
|
|
||||||
page,
|
|
||||||
r"""
|
|
||||||
() => {
|
|
||||||
const labels = new Set(['允许','Allow','Authorize','Approve','授权']);
|
|
||||||
const f = document.querySelector('form');
|
|
||||||
if (f) {
|
|
||||||
let a = f.querySelector('input[name=action]');
|
|
||||||
if (!a) {
|
|
||||||
a = document.createElement('input');
|
|
||||||
a.type = 'hidden';
|
|
||||||
a.name = 'action';
|
|
||||||
f.appendChild(a);
|
|
||||||
}
|
|
||||||
a.value = 'allow';
|
|
||||||
const btn = [...f.querySelectorAll('button,[role=button],input[type=submit]')]
|
|
||||||
.find(b => labels.has(((b.innerText||b.value||'').trim())));
|
|
||||||
if (btn) { btn.click(); return {ok:true, via:'form-btn'}; }
|
|
||||||
f.submit();
|
|
||||||
return {ok:true, via:'form-submit'};
|
|
||||||
}
|
|
||||||
// 无 form:真实派发 pointer/mouse 事件
|
|
||||||
const nodes = [...document.querySelectorAll('button,[role=button],input[type=submit],a')];
|
|
||||||
const target = nodes.find(n => {
|
|
||||||
const t = ((n.innerText||n.textContent||n.value||'').replace(/\s+/g,'')).trim();
|
|
||||||
return labels.has(t) || (t.includes('允许') && !t.includes('全部'));
|
|
||||||
});
|
|
||||||
if (!target) {
|
|
||||||
return {ok:false, texts: nodes.slice(0,10).map(n => (n.innerText||n.value||'').trim()).filter(Boolean)};
|
|
||||||
}
|
|
||||||
target.scrollIntoView({block:'center'});
|
|
||||||
target.focus();
|
|
||||||
for (const type of ['pointerdown','mousedown','pointerup','mouseup','click']) {
|
|
||||||
target.dispatchEvent(new MouseEvent(type, {bubbles:true, cancelable:true, view:window}));
|
|
||||||
}
|
|
||||||
return {ok:true, via:'mouse-events', text:(target.innerText||target.value||'').trim()};
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
if isinstance(ret, dict) and ret.get("ok"):
|
|
||||||
log(f"browser allow fallback: {ret}")
|
|
||||||
return True
|
|
||||||
log(f"browser allow button not found: {ret!r}")
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
log(f"click allow js failed: {exc}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _browser_consent_server_action(
|
|
||||||
page: Any,
|
|
||||||
consent_url: str,
|
|
||||||
flow: AuthCodeFlow,
|
|
||||||
log: LogFn,
|
|
||||||
) -> str:
|
|
||||||
"""在浏览器内 POST Next.js Server Action 批准 consent,直接拿 code。"""
|
|
||||||
html = _page_html(page)
|
|
||||||
action_id = _extract_next_action_id(html)
|
|
||||||
payload = [
|
|
||||||
{
|
|
||||||
"action": "allow",
|
|
||||||
"clientId": CLIENT_ID,
|
|
||||||
"redirectUri": REDIRECT_URI,
|
|
||||||
"scope": SCOPE,
|
|
||||||
"state": flow.state,
|
|
||||||
"codeChallenge": flow.code_challenge,
|
|
||||||
"codeChallengeMethod": "S256",
|
|
||||||
"nonce": flow.nonce,
|
|
||||||
"principalType": "User",
|
|
||||||
"principalId": "",
|
|
||||||
"referrer": GROK_REFERRER,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
body = json.dumps(payload, separators=(",", ":"))
|
|
||||||
# 用 window key 轮询,兼容 DrissionPage 对 Promise 支持不一
|
|
||||||
wrap = f"""
|
|
||||||
(() => {{
|
|
||||||
const key = '__cpa_consent_' + Date.now();
|
|
||||||
window[key] = null;
|
|
||||||
fetch({consent_url!r}, {{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {{
|
|
||||||
'Accept': 'text/x-component',
|
|
||||||
'Content-Type': 'text/plain;charset=UTF-8',
|
|
||||||
'Next-Action': {action_id!r},
|
|
||||||
'Origin': 'https://accounts.x.ai',
|
|
||||||
'Referer': {consent_url!r},
|
|
||||||
}},
|
|
||||||
body: {body!r},
|
|
||||||
credentials: 'include',
|
|
||||||
redirect: 'follow',
|
|
||||||
}}).then(async (r) => {{
|
|
||||||
const text = await r.text();
|
|
||||||
window[key] = {{status: r.status, url: r.url, text: text}};
|
|
||||||
}}).catch((e) => {{
|
|
||||||
window[key] = {{status: 0, url: '', text: String(e)}};
|
|
||||||
}});
|
|
||||||
return key;
|
|
||||||
}})()
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
key = _page_eval(page, wrap)
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
raise OAuthCodeError(f"browser consent fetch 启动失败: {exc}") from exc
|
|
||||||
log(f"browser consent server-action post action_id={action_id[:12]}…")
|
|
||||||
deadline = time.time() + 20
|
|
||||||
ret = None
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
ret = _page_eval(page, f"() => window[{key!r}]")
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
ret = _page_eval(page, f"window[{key!r}]")
|
|
||||||
except Exception:
|
|
||||||
ret = None
|
|
||||||
if ret:
|
|
||||||
break
|
|
||||||
time.sleep(0.2)
|
|
||||||
if not isinstance(ret, dict):
|
|
||||||
raise OAuthCodeError(f"browser consent 无响应: {ret!r}")
|
|
||||||
status = int(ret.get("status") or 0)
|
|
||||||
text = str(ret.get("text") or "")
|
|
||||||
final = str(ret.get("url") or "")
|
|
||||||
if status and (status < 200 or status >= 300):
|
|
||||||
raise OAuthCodeError(f"browser consent HTTP {status}: {_short(text, 240)}")
|
|
||||||
if "code=" in final:
|
|
||||||
qs = parse_qs(urlparse(final).query)
|
|
||||||
code = (qs.get("code") or [""])[0].strip()
|
|
||||||
if code:
|
|
||||||
log("browser got code from consent redirect url")
|
|
||||||
return code
|
|
||||||
# 响应体 / RSC
|
|
||||||
try:
|
|
||||||
code = parse_consent_code(text)
|
|
||||||
if code:
|
|
||||||
log("browser got code from consent server-action body")
|
|
||||||
return code
|
|
||||||
except OAuthCodeError:
|
|
||||||
pass
|
|
||||||
# 有时 code 在 text 的 redirect 串里
|
|
||||||
m = re.search(r"[?&]code=([A-Za-z0-9._~\-]+)", text)
|
|
||||||
if m:
|
|
||||||
log("browser got code from consent body regex")
|
|
||||||
return m.group(1)
|
|
||||||
raise OAuthCodeError(f"browser consent 未返回 code: {_short(text, 240)}")
|
|
||||||
|
|
||||||
|
|
||||||
def _browser_fetch_token(page: Any, code: str, flow: AuthCodeFlow, log: LogFn) -> TokenResult:
|
|
||||||
"""在浏览器上下文用 fetch 换 token,绕开 Python TLS 对 auth.x.ai 的 EOF。"""
|
|
||||||
form = {
|
|
||||||
"grant_type": "authorization_code",
|
|
||||||
"code": code,
|
|
||||||
"redirect_uri": REDIRECT_URI,
|
|
||||||
"client_id": CLIENT_ID,
|
|
||||||
"code_verifier": flow.code_verifier,
|
|
||||||
}
|
|
||||||
body = urlencode(form)
|
|
||||||
js = r"""
|
|
||||||
(tokenUrl, body, ua, ver) => {
|
|
||||||
return fetch(tokenUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Accept': '*/*',
|
|
||||||
'User-Agent': ua,
|
|
||||||
'X-Grok-Client-Version': ver,
|
|
||||||
},
|
|
||||||
body: body,
|
|
||||||
credentials: 'include',
|
|
||||||
}).then(async (r) => {
|
|
||||||
const text = await r.text();
|
|
||||||
return {status: r.status, text: text};
|
|
||||||
}).catch((e) => ({status: 0, text: String(e)}));
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
# 先到 auth 域,减少跨站限制
|
|
||||||
try:
|
|
||||||
page.get(ISSUER + "/")
|
|
||||||
time.sleep(0.3)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
ret = None
|
|
||||||
# DrissionPage 对 Promise 支持不一,做短轮询包装
|
|
||||||
wrap = r"""
|
|
||||||
(tokenUrl, body, ua, ver) => {
|
|
||||||
const key = '__cpa_token_result_' + Date.now();
|
|
||||||
window[key] = null;
|
|
||||||
fetch(tokenUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Accept': '*/*',
|
|
||||||
'User-Agent': ua,
|
|
||||||
'X-Grok-Client-Version': ver,
|
|
||||||
},
|
|
||||||
body: body,
|
|
||||||
credentials: 'include',
|
|
||||||
}).then(async (r) => {
|
|
||||||
const text = await r.text();
|
|
||||||
window[key] = {status: r.status, text: text};
|
|
||||||
}).catch((e) => {
|
|
||||||
window[key] = {status: 0, text: String(e)};
|
|
||||||
});
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
key = _page_eval(page, wrap, TOKEN_URL, body, GROK_TOKEN_UA, GROK_VERSION)
|
|
||||||
except Exception:
|
|
||||||
# 无参回退:把参数内联
|
|
||||||
key = _page_eval(
|
|
||||||
page,
|
|
||||||
f"""
|
|
||||||
(() => {{
|
|
||||||
const key = '__cpa_token_result_' + Date.now();
|
|
||||||
window[key] = null;
|
|
||||||
fetch({TOKEN_URL!r}, {{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {{
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Accept': '*/*',
|
|
||||||
'User-Agent': {GROK_TOKEN_UA!r},
|
|
||||||
'X-Grok-Client-Version': {GROK_VERSION!r},
|
|
||||||
}},
|
|
||||||
body: {body!r},
|
|
||||||
credentials: 'include',
|
|
||||||
}}).then(async (r) => {{
|
|
||||||
const text = await r.text();
|
|
||||||
window[key] = {{status: r.status, text: text}};
|
|
||||||
}}).catch((e) => {{
|
|
||||||
window[key] = {{status: 0, text: String(e)}};
|
|
||||||
}});
|
|
||||||
return key;
|
|
||||||
}})()
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
deadline = time.time() + 30
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
ret = _page_eval(page, f"() => window[{key!r}]")
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
ret = _page_eval(page, f"window[{key!r}]")
|
|
||||||
except Exception as exc:
|
|
||||||
raise OAuthCodeError(f"读取 browser token 结果失败: {exc}") from exc
|
|
||||||
if ret:
|
|
||||||
break
|
|
||||||
time.sleep(0.2)
|
|
||||||
if not isinstance(ret, dict):
|
|
||||||
raise OAuthCodeError(f"browser token 无响应: {ret!r}")
|
|
||||||
status = int(ret.get("status") or 0)
|
|
||||||
text = str(ret.get("text") or "")
|
|
||||||
if status < 200 or status >= 300:
|
|
||||||
raise OAuthCodeError(f"browser token HTTP {status}: {_short(text, 300)}")
|
|
||||||
try:
|
|
||||||
data = json.loads(text)
|
|
||||||
except Exception as e:
|
|
||||||
raise OAuthCodeError(f"browser token 非 JSON: {_short(text)}") from e
|
|
||||||
if not isinstance(data, dict) or not data.get("access_token"):
|
|
||||||
raise OAuthCodeError(f"browser token 缺少 access_token: {data!r}")
|
|
||||||
access = str(data["access_token"]).strip()
|
|
||||||
refresh = str(data.get("refresh_token") or "").strip()
|
|
||||||
if not refresh:
|
|
||||||
raise OAuthCodeError("browser token 缺少 refresh_token")
|
|
||||||
referrer = ""
|
|
||||||
try:
|
|
||||||
referrer = str(jwt_payload(access).get("referrer") or "")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return TokenResult(
|
|
||||||
access_token=access,
|
|
||||||
refresh_token=refresh,
|
|
||||||
id_token=(str(data["id_token"]).strip() if data.get("id_token") else None),
|
|
||||||
token_type=str(data.get("token_type") or "Bearer"),
|
|
||||||
expires_in=int(data.get("expires_in") or 21600),
|
|
||||||
raw=data,
|
|
||||||
referrer=referrer,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def mint_from_sso_browser(
|
|
||||||
sso_cookie: str,
|
|
||||||
page: Any,
|
|
||||||
*,
|
|
||||||
log: LogFn | None = None,
|
|
||||||
require_referrer: bool = True,
|
|
||||||
timeout_sec: float = 90.0,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""用注册浏览器完成 SSO→PKCE(绕开 Python TLS 访问 auth.x.ai 失败)。
|
|
||||||
|
|
||||||
流程:
|
|
||||||
1. 写入 sso cookie
|
|
||||||
2. 打开 authorize(referrer=grok-build)
|
|
||||||
3. 在 consent 页点击允许 / 或解析 callback code
|
|
||||||
4. 浏览器 fetch oauth2/token
|
|
||||||
"""
|
|
||||||
log = log or _noop_log
|
|
||||||
sso = normalize_sso_cookie(sso_cookie)
|
|
||||||
if not sso:
|
|
||||||
raise OAuthCodeError("sso cookie 为空")
|
|
||||||
if page is None:
|
|
||||||
raise OAuthCodeError("page 为空")
|
|
||||||
|
|
||||||
flow = new_auth_code_flow()
|
|
||||||
params = {
|
|
||||||
"response_type": "code",
|
|
||||||
"client_id": CLIENT_ID,
|
|
||||||
"redirect_uri": REDIRECT_URI,
|
|
||||||
"scope": SCOPE,
|
|
||||||
"code_challenge": flow.code_challenge,
|
|
||||||
"code_challenge_method": "S256",
|
|
||||||
"state": flow.state,
|
|
||||||
"nonce": flow.nonce,
|
|
||||||
"referrer": GROK_REFERRER,
|
|
||||||
}
|
|
||||||
auth_url = f"{AUTHORIZE_URL}?{urlencode(params)}"
|
|
||||||
log(f"browser PKCE authorize referrer={GROK_REFERRER}")
|
|
||||||
_ensure_sso_on_page(page, sso, log)
|
|
||||||
|
|
||||||
try:
|
|
||||||
page.get(auth_url)
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
raise OAuthCodeError(f"browser 打开 authorize 失败: {exc}") from exc
|
|
||||||
|
|
||||||
code = ""
|
|
||||||
deadline = time.time() + max(30.0, float(timeout_sec))
|
|
||||||
last_url = ""
|
|
||||||
consent_attempts = 0
|
|
||||||
server_action_tried = False
|
|
||||||
wait_after_click_until = 0.0
|
|
||||||
|
|
||||||
def _code_from_url(u: str) -> str:
|
|
||||||
if not u or "code=" not in u:
|
|
||||||
return ""
|
|
||||||
if not any(x in u for x in ("127.0.0.1", "localhost", "callback", "redirect")):
|
|
||||||
# 仍允许任意带 code 的 redirect
|
|
||||||
if "code=" not in u:
|
|
||||||
return ""
|
|
||||||
qs = parse_qs(urlparse(u).query)
|
|
||||||
return (qs.get("code") or [""])[0].strip()
|
|
||||||
|
|
||||||
while time.time() < deadline:
|
|
||||||
try:
|
|
||||||
url = str(getattr(page, "url", "") or "")
|
|
||||||
except Exception:
|
|
||||||
url = ""
|
|
||||||
if url and url != last_url:
|
|
||||||
log(f"browser url: {_short(url, 140)}")
|
|
||||||
last_url = url
|
|
||||||
|
|
||||||
# callback 已跳到 redirect_uri?code=
|
|
||||||
code = _code_from_url(url)
|
|
||||||
if code:
|
|
||||||
log("browser got code from redirect")
|
|
||||||
break
|
|
||||||
|
|
||||||
# consent 页:真实点击 Allow;点后短暂等待跳转;仍不行再 Server Action POST
|
|
||||||
if "/oauth2/consent" in url or "/consent" in url:
|
|
||||||
# 刚点过,先等 redirect
|
|
||||||
if wait_after_click_until and time.time() < wait_after_click_until:
|
|
||||||
time.sleep(0.3)
|
|
||||||
continue
|
|
||||||
|
|
||||||
consent_attempts += 1
|
|
||||||
if consent_attempts <= 4:
|
|
||||||
clicked = _browser_click_allow(page, log)
|
|
||||||
if clicked:
|
|
||||||
wait_after_click_until = time.time() + 4.0
|
|
||||||
time.sleep(0.6)
|
|
||||||
# 点后立刻看 url / html
|
|
||||||
try:
|
|
||||||
url2 = str(getattr(page, "url", "") or "")
|
|
||||||
except Exception:
|
|
||||||
url2 = url
|
|
||||||
code = _code_from_url(url2)
|
|
||||||
if code:
|
|
||||||
log("browser got code after allow click")
|
|
||||||
break
|
|
||||||
html = _page_html(page)
|
|
||||||
if html:
|
|
||||||
try:
|
|
||||||
code = parse_consent_code(html)
|
|
||||||
if code:
|
|
||||||
log("browser got code from consent html after click")
|
|
||||||
break
|
|
||||||
except OAuthCodeError:
|
|
||||||
pass
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 真实点击无效:浏览器内 Server Action POST(与 HTTP approve 同 payload)
|
|
||||||
if not server_action_tried:
|
|
||||||
server_action_tried = True
|
|
||||||
try:
|
|
||||||
code = _browser_consent_server_action(page, url, flow, log)
|
|
||||||
if code:
|
|
||||||
break
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
log(f"browser consent server-action failed: {exc}")
|
|
||||||
time.sleep(0.5)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "sign-in" in url or "sign-up" in url:
|
|
||||||
# cookie 可能没带上,重写一次
|
|
||||||
log("browser landed sign-in, re-inject sso")
|
|
||||||
_ensure_sso_on_page(page, sso, log)
|
|
||||||
try:
|
|
||||||
page.get(auth_url)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
time.sleep(0.8)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 中间跳转页:稍等
|
|
||||||
time.sleep(0.4)
|
|
||||||
|
|
||||||
if not code:
|
|
||||||
# 最后再扫一次页面 HTML
|
|
||||||
try:
|
|
||||||
html = _page_html(page)
|
|
||||||
if html:
|
|
||||||
try:
|
|
||||||
code = parse_consent_code(html)
|
|
||||||
except OAuthCodeError:
|
|
||||||
code = ""
|
|
||||||
except Exception:
|
|
||||||
code = ""
|
|
||||||
if not code:
|
|
||||||
raise OAuthCodeError(
|
|
||||||
f"browser PKCE 超时未拿到 code(last_url={_short(last_url, 160)})"
|
|
||||||
)
|
|
||||||
|
|
||||||
token = _browser_fetch_token(page, code, flow, log)
|
|
||||||
if token.referrer != GROK_REFERRER:
|
|
||||||
msg = f"access_token 未包含预期 referrer(got={token.referrer!r})"
|
|
||||||
if require_referrer:
|
|
||||||
raise OAuthCodeError(msg)
|
|
||||||
log(f"WARN {msg}")
|
|
||||||
else:
|
|
||||||
log("browser access_token referrer=grok-build ok")
|
|
||||||
log(f"browser token ok expires_in={token.expires_in}")
|
|
||||||
return {
|
|
||||||
"access_token": token.access_token,
|
|
||||||
"refresh_token": token.refresh_token,
|
|
||||||
"id_token": token.id_token,
|
|
||||||
"token_type": token.token_type,
|
|
||||||
"expires_in": token.expires_in,
|
|
||||||
"referrer": token.referrer,
|
|
||||||
"sso": sso,
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user