Files
grok-keygen-new/oidc_mint/browser_confirm.py
T
chaos 33f966ccea Initial commit: grok-register with private config and auth artifacts.
Include local config, CPA auth files, account dumps, and temp-mail deploy helpers for the private Gitea repo.
2026-07-12 08:10:44 +08:00

938 lines
31 KiB
Python

"""Approve xAI device-code in Chromium (DrissionPage).
Paths resolve relative to the grok_reg project root (parent of oidc_mint).
Proven flow (2026-07-10, free account):
1. Open verification_uri_complete (user_code prefilled)
2. Click 继续 on device page
3. Cookie banner: 全部允许 (optional)
4. 使用邮箱登录 → fill email → 下一步
5. Wait cf-turnstile-response → fill password → REAL click 登录
6. May land /account redirect or device page → 继续
7. Consent page /oauth2/device/consent → REAL click exact 允许
(by_js click causes Invalid action / empty form action)
8. /oauth2/device/done "设备已授权" + token poll SUCCESS
Hard rules:
- Token poll is source of truth
- Button match is EXACT text only (允许 ≠ 全部允许)
- Consent Allow MUST be a real click, not by_js
- Prefer headed browser + register turnstilePatch
"""
from __future__ import annotations
import os
import re
import sys
import threading
import time
from pathlib import Path
from typing import Any, Callable
from urllib.parse import urlparse
LogFn = Callable[[str], None]
def _noop_log(_: str) -> None:
return None
class BrowserConfirmError(RuntimeError):
pass
def _sleep(sec: float) -> None:
time.sleep(sec)
def create_standalone_page(
*,
proxy: str | None = None,
headless: bool = False,
log: LogFn | None = None,
) -> tuple[Any, Any]:
log = log or _noop_log
try:
from DrissionPage import Chromium, ChromiumOptions
except ImportError as e:
raise BrowserConfirmError(
"DrissionPage not installed; run inside grok_reg uv env or pip install DrissionPage"
) from e
opts = None
# Project root = parent of this package (./oidc_mint → ../)
_pkg_root = Path(__file__).resolve().parents[1]
try:
reg_file = _pkg_root / "grok_register_ttk.py"
if reg_file.is_file():
reg_dir = str(_pkg_root)
if reg_dir not in sys.path:
sys.path.insert(0, reg_dir)
try:
from grok_register_ttk import create_browser_options # type: ignore
opts = create_browser_options()
log("using register create_browser_options (turnstilePatch)")
except Exception as e: # noqa: BLE001
log(f"register browser options unavailable: {e}")
opts = None
except Exception as e: # noqa: BLE001
log(f"register options probe failed: {e}")
opts = None
if opts is None:
opts = ChromiumOptions()
opts.auto_port()
opts.set_timeouts(base=2)
for flag in (
"--disable-gpu",
"--no-sandbox",
"--disable-dev-shm-usage",
"--mute-audio",
"--no-first-run",
"--disable-background-networking",
"--window-size=1280,900",
):
opts.set_argument(flag)
ext = str(_pkg_root / "turnstilePatch")
if os.path.isdir(ext):
try:
opts.add_extension(ext)
log(f"added extension {ext}")
except Exception as e: # noqa: BLE001
log(f"extension add failed: {e}")
if headless:
try:
opts.headless(True)
except Exception:
opts.set_argument("--headless=new")
log("headless=True (may hit Cloudflare / break real clicks)")
else:
try:
opts.headless(False)
except Exception:
pass
log(f"headed browser DISPLAY={os.environ.get('DISPLAY', '')!r}")
for cand in (
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
):
if os.path.isfile(cand):
try:
opts.set_browser_path(cand)
except Exception:
pass
break
from .proxyutil import proxy_for_chromium, proxy_log_label, resolve_proxy
# explicit / runtime config first; env only as fallback
proxy = resolve_proxy(proxy)
chrome_proxy = proxy_for_chromium(proxy)
if chrome_proxy:
opts.set_argument(f"--proxy-server={chrome_proxy}")
log(f"browser proxy={proxy_log_label(proxy)} (chromium {chrome_proxy})")
else:
log("browser proxy=(none)")
browser = Chromium(opts)
page = browser.latest_tab
log("standalone chromium started")
return browser, page
def close_standalone(browser: Any) -> None:
try:
browser.quit()
except Exception:
pass
# ── mint browser reuse (per-thread) ──
_mint_tls = threading.local()
def _mint_tls_get() -> dict[str, Any]:
d = getattr(_mint_tls, "state", None)
if d is None:
d = {"browser": None, "page": None, "served": 0, "proxy": None, "headless": None}
_mint_tls.state = d
return d
def clear_page_session(page: Any, browser: Any | None = None, log: LogFn | None = None) -> None:
"""Blank page + wipe storage/cookies for reuse between mint jobs."""
log = log or _noop_log
try:
if page is not None:
try:
page.get("about:blank")
except Exception:
pass
for js in (
"try{localStorage.clear()}catch(e){}",
"try{sessionStorage.clear()}catch(e){}",
):
try:
page.run_js(js)
except Exception:
pass
for target in (page, browser):
if target is None:
continue
try:
target.set.cookies.clear() # type: ignore[attr-defined]
log("mint session cookies cleared")
break
except Exception:
try:
# older API
cks = target.cookies()
if isinstance(cks, list):
for c in cks:
try:
target.set.cookies.remove(c) # type: ignore[attr-defined]
except Exception:
pass
except Exception:
pass
except Exception as e:
log(f"clear_page_session: {e}")
def normalize_cookies(cookies: Any) -> list[dict[str, Any]]:
"""Normalize DrissionPage / browser cookie list to settable dicts.
Also clones SSO-like cookies onto accounts.x.ai / auth.x.ai domains so
device-auth can skip secondary login when possible.
"""
out: list[dict[str, Any]] = []
if not cookies:
return out
if isinstance(cookies, dict):
for k, v in cookies.items():
if k and v is not None:
out.append({"name": str(k), "value": str(v), "domain": ".x.ai", "path": "/"})
cookies = out
out = []
if not isinstance(cookies, (list, tuple)):
return out
for c in cookies:
if not isinstance(c, dict):
continue
name = c.get("name") or c.get("Name")
value = c.get("value") or c.get("Value")
if not name or value is None:
continue
domain = str(c.get("domain") or c.get("Domain") or ".x.ai")
path = str(c.get("path") or c.get("Path") or "/")
item = {
"name": str(name),
"value": str(value),
"domain": domain,
"path": path,
}
for src, dst in (
("expiry", "expiry"),
("expires", "expiry"),
("secure", "secure"),
("httpOnly", "httpOnly"),
("sameSite", "sameSite"),
):
if src in c and c[src] is not None:
item[dst] = c[src]
out.append(item)
# Expand SSO cookies to xAI account hosts (register browser is often on grok.com)
sso_names = {"sso", "sso-rw", "cf_clearance", "sso_jwt", "__cf_bm"}
extras: list[dict[str, Any]] = []
seen = {(i["name"], i["domain"], i["path"]) for i in out}
for item in list(out):
n = item["name"]
if n not in sso_names and not n.startswith("sso"):
continue
for dom in (".x.ai", "accounts.x.ai", ".accounts.x.ai", "auth.x.ai", ".auth.x.ai"):
key = (n, dom, item["path"])
if key in seen:
continue
clone = dict(item)
clone["domain"] = dom
extras.append(clone)
seen.add(key)
out.extend(extras)
return out
def inject_cookies(page: Any, cookies: Any, log: LogFn | None = None) -> int:
"""Inject cookies into page/browser. Returns count attempted."""
log = log or _noop_log
items = normalize_cookies(cookies)
if not items or page is None:
return 0
for url in (
"https://accounts.x.ai/",
"https://auth.x.ai/",
"https://grok.com/",
):
try:
page.get(url)
_sleep(0.4)
except Exception:
continue
n = 0
for target_name, target in (("page", page), ("browser", getattr(page, "browser", None))):
if target is None:
continue
try:
target.set.cookies(items) # type: ignore[attr-defined]
n = len(items)
log(f"injected cookies bulk via {target_name}={n}")
break
except Exception as e:
log(f"bulk set via {target_name} failed: {e}")
if n == 0:
for item in items:
ok = False
for target in (page, getattr(page, "browser", None)):
if target is None:
continue
try:
target.set.cookies(item) # type: ignore[attr-defined]
ok = True
break
except Exception:
continue
if ok:
n += 1
log(f"injected cookies one-by-one={n}/{len(items)}")
# JS document.cookie for non-httpOnly SSO cookies (best effort)
try:
js_items = [
c
for c in items
if (not c.get("httpOnly")) and c.get("name") in {"sso", "sso-rw", "cf_clearance"}
]
if js_items:
page.get("https://accounts.x.ai/")
for c in js_items:
name = str(c["name"])
val = str(c["value"])
# avoid quote breakage
if "'" in name or "'" in val:
continue
page.run_js(
"document.cookie='"
+ name
+ "="
+ val
+ "; path=/; domain=.x.ai; Secure; SameSite=None'"
)
log(f"js cookie fallback applied={len(js_items)}")
except Exception as e:
log(f"js cookie fallback: {e}")
return n
def acquire_mint_browser(
*,
proxy: str | None = None,
headless: bool = False,
reuse: bool = True,
recycle_every: int = 15,
log: LogFn | None = None,
) -> tuple[Any, Any, bool]:
"""Return (browser, page, owned). owned=True means caller must close if not reusing.
When reuse=True, browser is kept in thread-local and cleared between jobs.
"""
log = log or _noop_log
st = _mint_tls_get()
if reuse and st.get("browser") is not None:
# recycle if proxy/headless changed or served enough
need_recycle = (
st.get("proxy") != (proxy or None)
or st.get("headless") != headless
or (recycle_every > 0 and int(st.get("served") or 0) >= recycle_every)
)
if not need_recycle:
page = st.get("page")
browser = st.get("browser")
clear_page_session(page, browser, log=log)
log(f"mint browser reused served={st.get('served')}")
return browser, page, False
log("mint browser recycle (proxy/headless/served threshold)")
try:
close_standalone(st.get("browser"))
except Exception:
pass
st["browser"] = None
st["page"] = None
st["served"] = 0
browser, page = create_standalone_page(proxy=proxy, headless=headless, log=log)
if reuse:
st["browser"] = browser
st["page"] = page
st["proxy"] = proxy or None
st["headless"] = headless
st["served"] = 0
return browser, page, False
return browser, page, True
def release_mint_browser(
*,
owned: bool,
success: bool = True,
force_quit: bool = False,
log: LogFn | None = None,
) -> None:
log = log or _noop_log
st = _mint_tls_get()
if force_quit or owned:
browser = st.get("browser") if not owned else None
# if owned, caller passes via closing create path — handle both
if owned:
# owned browser not in tls
return
if browser is not None:
close_standalone(browser)
st["browser"] = None
st["page"] = None
st["served"] = 0
log("mint browser quit")
return
if success:
st["served"] = int(st.get("served") or 0) + 1
else:
# fail: drop browser to avoid dirty state
if st.get("browser") is not None:
close_standalone(st.get("browser"))
st["browser"] = None
st["page"] = None
st["served"] = 0
log("mint browser dropped after failure")
def shutdown_mint_browsers() -> None:
st = getattr(_mint_tls, "state", None)
if not st:
return
if st.get("browser") is not None:
close_standalone(st.get("browser"))
st["browser"] = None
st["page"] = None
st["served"] = 0
def _page_url(page: Any) -> str:
try:
return page.url or ""
except Exception:
return ""
def _visible_text(page: Any) -> str:
try:
t = page.run_js(
"return (document.body && (document.body.innerText || document.body.textContent)) || '';"
)
if isinstance(t, str) and t.strip():
return t
except Exception:
pass
try:
raw = getattr(page, "raw_text", None)
if callable(raw):
t = raw()
if isinstance(t, str) and t.strip():
return t
if isinstance(raw, str) and raw.strip():
return raw
except Exception:
pass
return ""
def _norm(s: str) -> str:
return re.sub(r"\s+", " ", (s or "").strip())
def _find_button_exact(page: Any, label: str) -> Any | None:
try:
for el in page.eles("tag:button") or []:
try:
if _norm(el.text or "") == label:
return el
except Exception:
continue
except Exception:
pass
try:
return page.ele(f"xpath://button[normalize-space(.)='{label}']", timeout=0.3)
except Exception:
return None
def _click_exact(
page: Any,
labels: list[str],
log: LogFn,
*,
real: bool = False,
) -> str | None:
"""Click button by EXACT visible text. real=True uses physical click (needed for consent)."""
for label in labels:
el = _find_button_exact(page, label)
if not el:
continue
try:
if real:
try:
el.scroll.to_see()
except Exception:
pass
el.click()
log(f"clicked REAL exact {label!r}")
else:
el.click(by_js=True)
log(f"clicked JS exact {label!r}")
return label
except Exception as e:
log(f"click {label!r} failed: {e}")
if real:
try:
el.click(by_js=True)
log(f"clicked JS fallback exact {label!r}")
return label
except Exception as e2:
log(f"js fallback {label!r} failed: {e2}")
return None
def _wait_turnstile(page: Any, log: LogFn, timeout: float = 45.0) -> bool:
"""Wait/click Cloudflare Turnstile on the mint browser page."""
deadline = time.time() + timeout
clicked = False
while time.time() < deadline:
try:
el = page.ele("css:input[name='cf-turnstile-response']", timeout=0.3)
if el is not None:
v = (el.attr("value") or "").strip()
if len(v) > 20:
log(f"turnstile ready len={len(v)}")
return True
except Exception:
pass
# Mimic register-machine: shadow-root checkbox click
try:
challenge_input = page.ele("@name=cf-turnstile-response", timeout=0.2)
if challenge_input is not None:
wrapper = challenge_input.parent()
iframe = None
try:
iframe = wrapper.shadow_root.ele("tag:iframe")
except Exception:
iframe = None
if iframe is not None:
try:
iframe.run_js(
"""
window.dtp = 1;
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
let sx = getRandomInt(800, 1200);
let sy = getRandomInt(400, 700);
Object.defineProperty(MouseEvent.prototype, 'screenX', { value: sx });
Object.defineProperty(MouseEvent.prototype, 'screenY', { value: sy });
"""
)
except Exception:
pass
try:
body_sr = iframe.ele("tag:body").shadow_root
btn = body_sr.ele("tag:input")
if btn is not None:
btn.click()
if not clicked:
log("clicked turnstile shadow checkbox")
clicked = True
except Exception:
pass
except Exception:
pass
if not clicked:
try:
page.run_js(
"""
const nodes = Array.from(document.querySelectorAll('div,span,iframe')).filter((n) => {
const txt = (n.className || '') + ' ' + (n.id || '') + ' ' + (n.getAttribute?.('src') || '');
return String(txt).toLowerCase().includes('turnstile');
});
if (nodes.length && typeof nodes[0].click === 'function') nodes[0].click();
"""
)
clicked = True
log("clicked turnstile container via JS")
except Exception:
pass
_sleep(0.9)
log("turnstile not ready")
return False
def _fill(page, selector, value, log, label=""):
"""找到 selector 输入框,清空并填入 value;成功返回 True。"""
try:
el = page.ele(selector, timeout=3)
except Exception:
el = None
if not el:
return False
try:
try:
el.clear()
except Exception:
pass
el.input(value)
if label:
log(f"filled {label}")
return True
except Exception as exc:
log(f"fill {label} failed: {exc}")
return False
def approve_device_code(
page: Any,
*,
verification_uri_complete: str,
email: str,
password: str,
user_code: str = "",
timeout_sec: float = 240.0,
stop_event: threading.Event | None = None,
log: LogFn | None = None,
) -> None:
log = log or _noop_log
if page is None:
raise BrowserConfirmError("page is None")
email = (email or "").strip()
password = password or ""
if not email or not password:
raise BrowserConfirmError("email/password required")
if not user_code and "user_code=" in (verification_uri_complete or ""):
try:
user_code = verification_uri_complete.split("user_code=", 1)[1].split("&", 1)[0]
except Exception:
user_code = ""
log(f"open device url: {verification_uri_complete}")
try:
page.get(verification_uri_complete, timeout=60)
except TypeError:
page.get(verification_uri_complete)
_sleep(2.0)
deadline = time.time() + timeout_sec
phase = "device"
login_attempts = 0
last_url = ""
while time.time() < deadline:
if stop_event is not None and stop_event.is_set():
log("stop_event set — leave browser loop")
return
url = _page_url(page)
text = _visible_text(page)
if url != last_url:
log(f"url: {url[:180]}")
last_url = url
snip = _norm(text)[:160]
if snip:
log(f"visible: {snip}")
# Done page
if "device/done" in url or "设备已授权" in text or "device authorized" in text.lower():
log("device done page — waiting for token poll")
_sleep(1.5)
continue
if "Invalid action" in text:
log("Invalid action — reopen device uri")
page.get(verification_uri_complete)
_sleep(2.0)
phase = "device"
continue
# Consent page — REAL click exact 允许
if "/consent" in url or "授权 Grok Build" in text or "Authorize Grok Build" in text:
phase = "consent"
# Prefer real click; React needs it to set form action=allow
if _click_exact(page, ["允许", "Allow", "Authorize", "Approve"], log, real=True):
_sleep(2.5)
continue
# last resort: set action and submit
try:
page.run_js(
"""
const f=document.querySelector('form');
if(!f) return;
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')].find(b=>((b.innerText||'').trim())==='允许'||(b.innerText||'').trim()==='Allow');
if(btn) btn.click(); else f.submit();
"""
)
log("consent form submit via JS fallback")
_sleep(2.5)
except Exception as e:
log(f"consent fallback failed: {e}")
continue
# Device code entry
if page.ele("css:input[name='user_code']", timeout=0.3) and "consent" not in url:
phase = "device"
if user_code:
try:
uc = page.ele("css:input[name='user_code']")
cur = (uc.value or "") if uc else ""
if user_code.replace("-", "") not in cur.replace("-", ""):
uc.clear()
uc.input(user_code)
log("filled user_code")
except Exception:
pass
if _click_exact(page, ["继续", "Continue"], log, real=False):
_sleep(2.0)
continue
try:
el = page.ele("css:button[type='submit']", timeout=0.5)
if el:
el.click(by_js=True)
log("clicked device submit")
_sleep(2.0)
continue
except Exception:
pass
# Account redirect
if "正在重定向" in text or ("/account" in url and "sign-in" not in url):
if _click_exact(page, ["继续", "Continue"], log, real=False):
_sleep(2.0)
continue
# Cookie banner (exact labels only)
if "全部允许" in text or "隐私偏好" in text:
_click_exact(page, ["全部允许", "全部拒绝"], log, real=False)
_sleep(0.5)
# Sign-in chooser
if "使用邮箱登录" in text or "Continue with email" in text:
if _click_exact(page, ["使用邮箱登录", "Continue with email", "Sign in with email"], log, real=False):
_sleep(1.5)
phase = "email"
continue
# Email only step
if page.ele("css:input[type='email']", timeout=0.3) and not page.ele(
"css:input[type='password']", timeout=0.2
):
phase = "email"
_fill(page, "css:input[type='email']", email, log, "email")
if _click_exact(page, ["下一步", "Next", "Continue", "继续"], log, real=False):
_sleep(1.8)
continue
# Password login
if page.ele("css:input[type='password']", timeout=0.3):
phase = "password"
if login_attempts >= 5:
_sleep(1.0)
continue
login_attempts += 1
log(f"login attempt {login_attempts}")
_fill(page, "css:input[type='email']", email, log, "email")
_wait_turnstile(page, log, 25)
_fill(page, "css:input[type='password']", password, log, "password")
_wait_turnstile(page, log, 12)
# REAL click login helps form submit
if not _click_exact(page, ["登录", "Sign in", "Log in"], log, real=True):
try:
el = page.ele("css:button[type='submit']", timeout=0.5) or page.ele(
"css:button[data-testid='sign-in-submit']", timeout=0.5
)
if el:
el.click()
log("clicked login submit real")
except Exception as e:
log(f"login submit fail: {e}")
# wait navigation
for _ in range(24):
if stop_event is not None and stop_event.is_set():
return
_sleep(0.5)
if not page.ele("css:input[type='password']", timeout=0.2):
break
if "sign-in" not in _page_url(page):
break
continue
_sleep(1.0)
if stop_event is not None and stop_event.is_set():
log("browser finished via stop_event")
return
log(f"browser loop ended phase={phase} login_attempts={login_attempts}")
def mint_with_browser(
*,
email: str,
password: str,
page: Any | None = None,
proxy: str | None = None,
headless: bool = False,
browser_timeout_sec: float = 240.0,
poll_log: LogFn | None = None,
cancel: Callable[[], bool] | None = None,
force_standalone: bool = True,
cookies: Any | None = None,
reuse_browser: bool = True,
recycle_every: int = 15,
) -> dict[str, Any]:
"""Request device code, approve in browser, poll tokens.
force_standalone=True (default): do not reuse the *register* tab.
Mint workers may still reuse their *own* Chromium via reuse_browser.
cookies: optional register-browser cookie list to skip re-login.
"""
from .oauth_device import OAuthDeviceError, poll_device_token, request_device_code
from .proxyutil import proxy_log_label, resolve_proxy, set_runtime_proxy
log = poll_log or _noop_log
own_browser = None
owned = False
work_page = None if force_standalone else page
resolved = resolve_proxy(proxy)
set_runtime_proxy(resolved or None)
success = False
try:
last_err: BaseException | None = None
sess = None
for attempt in range(1, 4):
try:
sess = request_device_code(proxy=resolved or None)
last_err = None
break
except BaseException as e: # noqa: BLE001
last_err = e
log(f"request_device_code attempt {attempt}/3 failed: {e}")
_sleep(1.5 * attempt)
if sess is None:
raise last_err or RuntimeError("request_device_code failed")
log(
f"device user_code={sess.user_code} expires_in={sess.expires_in} "
f"proxy={proxy_log_label(resolved) or '(none)'}"
)
if work_page is None:
own_browser, work_page, owned = acquire_mint_browser(
proxy=resolved or None,
headless=headless,
reuse=reuse_browser,
recycle_every=recycle_every,
log=log,
)
if owned:
# non-reuse path: track for finally close
pass
# Cookie inject before opening device URL (skip secondary login when possible)
if cookies:
n = inject_cookies(work_page, cookies, log=log)
log(f"cookie inject count={n}")
try:
work_page.get("https://accounts.x.ai/")
_sleep(1.0)
url = _page_url(work_page)
text = _visible_text(work_page)
snip = _norm(text)[:120]
log(f"post-inject session url={url[:120]} visible={snip}")
except Exception as e:
log(f"post-inject check: {e}")
stop_event = threading.Event()
token_box: dict[str, Any] = {}
err_box: dict[str, BaseException] = {}
def _poll() -> None:
try:
time.sleep(2)
tr = poll_device_token(
sess.device_code,
interval=max(sess.interval, 5),
expires_in=min(sess.expires_in, int(browser_timeout_sec) + 60),
log=log,
cancel=cancel,
proxy=resolved or None,
)
token_box["token"] = tr
stop_event.set()
log("token poll SUCCESS — stop_event set")
except BaseException as e: # noqa: BLE001
err_box["err"] = e
stop_event.set()
t = threading.Thread(target=_poll, name="oauth-poll", daemon=True)
t.start()
try:
approve_device_code(
work_page,
verification_uri_complete=sess.verification_uri_complete,
email=email,
password=password,
user_code=sess.user_code,
timeout_sec=browser_timeout_sec,
stop_event=stop_event,
log=log,
)
except BrowserConfirmError as e:
log(f"browser confirm warning: {e}")
t.join(timeout=max(browser_timeout_sec, 60) + 30)
if "token" in token_box:
tr = token_box["token"]
success = True
return {
"access_token": tr.access_token,
"refresh_token": tr.refresh_token,
"id_token": tr.id_token,
"token_type": tr.token_type,
"expires_in": tr.expires_in,
"user_code": sess.user_code,
}
if "err" in err_box:
raise err_box["err"]
raise OAuthDeviceError("token poll thread ended without result")
finally:
if own_browser is not None:
if owned:
close_standalone(own_browser)
else:
release_mint_browser(owned=False, success=success, log=log)