Files
grok-free-register-oss/grok_register/clearance.py
chaos d10009d639 Initial commit: grok-free-register-oss
Open-source Grok free registration CLI, xai_enroller auth pipeline,
local auth service, tests and docs.
2026-07-16 21:04:05 +08:00

396 lines
13 KiB
Python

"""Cloudflare clearance prewarm via external FlareSolverr.
This module does NOT run FlareSolverr. It only calls an already-deployed
FlareSolverr instance (default http://127.0.0.1:8191).
Proxy roles (do not mix):
- REGISTER_PROXY / HTTP_PROXY / HTTPS_PROXY
→ grok-free-register Playwright + httpx egress (this process)
- CLEARANCE_PROXY
→ optional proxy *inside* FlareSolverr's browser (must be reachable
from the FS *container*, e.g. http://privoxy:8118)
- FLARESOLVERR_URL
→ host-side URL to reach FS (http://127.0.0.1:8191)
Clearance cookies are only valid on the same egress path they were minted on.
If REGISTER_PROXY points at host Privoxy→WARP, set CLEARANCE_PROXY to the
docker-internal privoxy URL so FS solves on the same WARP exit.
"""
from __future__ import annotations
import json
import os
import threading
import time
import urllib.error
import urllib.request
from typing import Any
from urllib.parse import urlparse
# CF-fronted x.ai family roots (no path suffix; path pages share host clearance)
DEFAULT_CLEARANCE_URLS = (
"https://accounts.x.ai",
"https://x.ai",
"https://status.x.ai",
"https://console.x.ai",
"https://auth.x.ai",
)
_lock = threading.RLock()
_cache: dict[str, Any] = {
"cookies": [], # Playwright cookie dicts
"user_agent": "",
"fetched_at": 0.0,
"hosts": [],
"last_error": "",
}
def _env_bool(name: str, default: bool = False) -> bool:
raw = str(os.environ.get(name, "")).strip().lower()
if not raw:
return default
if raw in {"1", "true", "yes", "on"}:
return True
if raw in {"0", "false", "no", "off"}:
return False
return default
def _env_int(name: str, default: int) -> int:
try:
return int(str(os.environ.get(name, "")).strip() or default)
except (TypeError, ValueError):
return default
def clearance_enabled() -> bool:
return _env_bool("CLEARANCE_ENABLED", False)
def flaresolverr_url() -> str:
return (os.environ.get("FLARESOLVERR_URL") or "http://127.0.0.1:8191").strip().rstrip("/")
def clearance_timeout_sec() -> int:
return max(10, _env_int("CLEARANCE_TIMEOUT_SEC", 60))
def clearance_refresh_sec() -> int:
return max(60, _env_int("CLEARANCE_REFRESH_SEC", 3000))
def clearance_urls() -> list[str]:
raw = (os.environ.get("CLEARANCE_URLS") or "").strip()
if not raw:
return list(DEFAULT_CLEARANCE_URLS)
urls = []
for part in raw.split(","):
item = part.strip()
if not item:
continue
if "://" not in item:
item = "https://" + item
# strip path/query — host-level prewarm only
parsed = urlparse(item)
if parsed.scheme and parsed.netloc:
urls.append(f"{parsed.scheme}://{parsed.netloc}")
else:
urls.append(item.rstrip("/"))
# de-dupe preserve order
seen = set()
out = []
for u in urls:
if u not in seen:
seen.add(u)
out.append(u)
return out or list(DEFAULT_CLEARANCE_URLS)
def register_proxy_url() -> str:
"""Egress proxy for *this* process (Playwright / httpx)."""
for key in ("REGISTER_PROXY", "HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY"):
value = (os.environ.get(key) or "").strip()
if value:
return value
return ""
def clearance_proxy_url() -> str:
"""Proxy for FlareSolverr browser (container-reachable). Empty = FS direct egress."""
explicit = (os.environ.get("CLEARANCE_PROXY") or "").strip()
if explicit:
return explicit
# Do NOT auto-forward host REGISTER_PROXY into FS — 127.0.0.1 inside
# the container is not the host. Operator must set CLEARANCE_PROXY when
# minting on WARP (e.g. http://privoxy:8118).
return ""
def playwright_proxy_settings() -> dict[str, str] | None:
"""Playwright launch/context proxy dict, or None for direct."""
url = register_proxy_url()
if not url:
return None
return {"server": url}
def httpx_proxy_mounts() -> str | None:
"""Single proxy URL for httpx.AsyncClient(proxy=...), or None."""
return register_proxy_url() or None
def _host_from_url(url: str) -> str:
try:
return (urlparse(url).hostname or "").lower()
except Exception:
return ""
def _fs_post(payload: dict[str, Any], timeout: float) -> dict[str, Any]:
endpoint = f"{flaresolverr_url()}/v1"
body = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
endpoint,
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout) as response:
raw = response.read()
return json.loads(raw.decode("utf-8"))
def _cookie_to_playwright(item: dict[str, Any], fallback_host: str) -> dict[str, Any] | None:
name = str(item.get("name") or "").strip()
if not name:
return None
value = str(item.get("value") or "")
domain = str(item.get("domain") or "").strip()
if not domain:
domain = fallback_host
if domain and not domain.startswith(".") and domain.count(".") >= 1:
# keep host-only cookies host-only; CF often uses .x.ai
pass
path = str(item.get("path") or "/") or "/"
cookie: dict[str, Any] = {
"name": name,
"value": value,
"domain": domain,
"path": path,
}
if "httpOnly" in item:
cookie["httpOnly"] = bool(item.get("httpOnly"))
if "secure" in item:
cookie["secure"] = bool(item.get("secure"))
else:
cookie["secure"] = True
same_site = item.get("sameSite") or item.get("same_site")
if same_site:
# Playwright: Strict | Lax | None
normalized = str(same_site).capitalize()
if normalized.lower() == "none":
normalized = "None"
if normalized in {"Strict", "Lax", "None"}:
cookie["sameSite"] = normalized
expires = item.get("expires")
if expires is not None:
try:
exp = float(expires)
# FS may return ms or session -1
if exp > 0:
if exp > 1e12:
exp = exp / 1000.0
cookie["expires"] = exp
except (TypeError, ValueError):
pass
return cookie
def _merge_cookies(existing: list[dict[str, Any]], incoming: list[dict[str, Any]]) -> list[dict[str, Any]]:
index: dict[tuple[str, str, str], int] = {}
merged: list[dict[str, Any]] = []
for cookie in existing:
key = (
str(cookie.get("name") or ""),
str(cookie.get("domain") or ""),
str(cookie.get("path") or "/"),
)
index[key] = len(merged)
merged.append(cookie)
for cookie in incoming:
key = (
str(cookie.get("name") or ""),
str(cookie.get("domain") or ""),
str(cookie.get("path") or "/"),
)
if key in index:
merged[index[key]] = cookie
else:
index[key] = len(merged)
merged.append(cookie)
return merged
def prewarm_clearance(*, force: bool = False) -> dict[str, Any]:
"""Hit FlareSolverr for each CLEARANCE_URLS host; cache Playwright cookies.
Safe to call when disabled — returns status without network.
"""
if not clearance_enabled():
return {
"enabled": False,
"ok": True,
"skipped": True,
"message": "CLEARANCE_ENABLED=0",
"cookies": 0,
}
with _lock:
age = time.time() - float(_cache.get("fetched_at") or 0)
if (
not force
and _cache.get("cookies")
and age < clearance_refresh_sec()
):
return {
"enabled": True,
"ok": True,
"cached": True,
"age_sec": round(age, 1),
"cookies": len(_cache["cookies"]),
"hosts": list(_cache.get("hosts") or []),
"user_agent": _cache.get("user_agent") or "",
}
urls = clearance_urls()
timeout = float(clearance_timeout_sec())
fs_proxy = clearance_proxy_url()
max_timeout_ms = int(timeout * 1000)
merged: list[dict[str, Any]] = []
user_agent = ""
hosts_ok: list[str] = []
errors: list[str] = []
per_host: list[dict[str, Any]] = []
for url in urls:
host = _host_from_url(url)
payload: dict[str, Any] = {
"cmd": "request.get",
"url": url,
"maxTimeout": max_timeout_ms,
}
if fs_proxy:
payload["proxy"] = {"url": fs_proxy}
t0 = time.time()
try:
data = _fs_post(payload, timeout=timeout + 15)
elapsed = round(time.time() - t0, 2)
if str(data.get("status") or "").lower() != "ok":
errors.append(f"{host}: status={data.get('status')} msg={data.get('message')}")
per_host.append({"host": host, "ok": False, "elapsed": elapsed, "error": data.get("message")})
continue
solution = data.get("solution") if isinstance(data.get("solution"), dict) else {}
raw_cookies = solution.get("cookies") or []
pw_cookies = []
if isinstance(raw_cookies, list):
for item in raw_cookies:
if not isinstance(item, dict):
continue
converted = _cookie_to_playwright(item, host)
if converted:
pw_cookies.append(converted)
merged = _merge_cookies(merged, pw_cookies)
ua = str(solution.get("userAgent") or "").strip()
if ua:
user_agent = ua
has_cf = any(c.get("name") == "cf_clearance" for c in pw_cookies)
hosts_ok.append(host)
per_host.append(
{
"host": host,
"ok": True,
"elapsed": elapsed,
"http": solution.get("status"),
"cookies": len(pw_cookies),
"cf_clearance": has_cf,
}
)
except Exception as exc: # noqa: BLE001 — surface to operator log
elapsed = round(time.time() - t0, 2)
errors.append(f"{host}: {exc}")
per_host.append({"host": host, "ok": False, "elapsed": elapsed, "error": str(exc)})
with _lock:
if merged:
_cache["cookies"] = merged
_cache["user_agent"] = user_agent
_cache["fetched_at"] = time.time()
_cache["hosts"] = hosts_ok
_cache["last_error"] = "; ".join(errors)
elif errors:
_cache["last_error"] = "; ".join(errors)
return {
"enabled": True,
"ok": bool(merged) or not errors,
"cached": False,
"cookies": len(merged),
"hosts": hosts_ok,
"user_agent": user_agent,
"errors": errors,
"detail": per_host,
"flaresolverr": flaresolverr_url(),
"clearance_proxy": fs_proxy or "(direct)",
"register_proxy": register_proxy_url() or "(direct)",
}
def cached_playwright_cookies() -> list[dict[str, Any]]:
with _lock:
return [dict(c) for c in (_cache.get("cookies") or [])]
def cached_user_agent() -> str:
with _lock:
return str(_cache.get("user_agent") or "")
async def apply_clearance_to_context(context) -> int:
"""Inject cached CF cookies into a Playwright BrowserContext. Returns count."""
cookies = cached_playwright_cookies()
if not cookies or context is None:
return 0
try:
await context.add_cookies(cookies)
return len(cookies)
except Exception:
return 0
def format_prewarm_log(result: dict[str, Any]) -> str:
if result.get("skipped"):
return "[clearance] 未启用 (CLEARANCE_ENABLED=0)"
if result.get("cached"):
return (
f"[clearance] 复用缓存 cookies={result.get('cookies')} "
f"age={result.get('age_sec')}s hosts={','.join(result.get('hosts') or [])}"
)
detail = result.get("detail") or []
parts = []
for row in detail:
host = row.get("host")
if row.get("ok"):
cf = "cf+" if row.get("cf_clearance") else "cf-"
parts.append(f"{host}:{row.get('elapsed')}s/{cf}")
else:
parts.append(f"{host}:FAIL")
err = result.get("errors") or []
suffix = f" errors={len(err)}" if err else ""
return (
f"[clearance] 预热完成 cookies={result.get('cookies')} "
f"fs={result.get('flaresolverr')} reg_proxy={result.get('register_proxy')} "
f"fs_proxy={result.get('clearance_proxy')} | " + " ".join(parts) + suffix
)