Open-source Grok free registration CLI, xai_enroller auth pipeline, local auth service, tests and docs.
207 lines
6.8 KiB
Python
Executable File
207 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""通过 SSH 导出不含密码的注册会话;兼容历史裸 SSO 记录。"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
|
|
COOKIE_FIELDS = frozenset(
|
|
{
|
|
"name",
|
|
"value",
|
|
"url",
|
|
"domain",
|
|
"path",
|
|
"expires",
|
|
"httpOnly",
|
|
"secure",
|
|
"sameSite",
|
|
}
|
|
)
|
|
MAX_RECORD_BYTES = 256 * 1024
|
|
LEGACY_COOKIE_SCOPE = {
|
|
"name": "sso",
|
|
"domain": "accounts.x.ai",
|
|
"path": "/",
|
|
"secure": True,
|
|
"httpOnly": True,
|
|
"sameSite": "Lax",
|
|
}
|
|
|
|
|
|
def _decode_json_line(raw, label):
|
|
if len(raw) > MAX_RECORD_BYTES:
|
|
raise ValueError(f"invalid {label} record")
|
|
try:
|
|
document = json.loads(raw.decode("utf-8"))
|
|
email = document["email"]
|
|
cookies = document["cookies"]
|
|
except (UnicodeDecodeError, TypeError, ValueError, KeyError) as exc:
|
|
raise ValueError(f"invalid {label} record") from exc
|
|
if not isinstance(email, str) or not email or not isinstance(cookies, list) or not cookies:
|
|
raise ValueError(f"invalid {label} record")
|
|
try:
|
|
email.encode("utf-8")
|
|
except UnicodeEncodeError as exc:
|
|
raise ValueError(f"invalid {label} record") from exc
|
|
normalized = []
|
|
for cookie in cookies:
|
|
if not isinstance(cookie, dict):
|
|
raise ValueError(f"invalid {label} record")
|
|
filtered = {key: cookie[key] for key in COOKIE_FIELDS if key in cookie}
|
|
if not all(
|
|
isinstance(filtered.get(key), str) and filtered[key]
|
|
for key in ("name", "value")
|
|
):
|
|
raise ValueError(f"invalid {label} record")
|
|
scope = filtered.get("domain") or filtered.get("url")
|
|
if not isinstance(scope, str) or not scope:
|
|
raise ValueError(f"invalid {label} record")
|
|
try:
|
|
filtered["name"].encode("utf-8")
|
|
filtered["value"].encode("utf-8")
|
|
scope.encode("utf-8")
|
|
except UnicodeEncodeError as exc:
|
|
raise ValueError(f"invalid {label} record") from exc
|
|
normalized.append(filtered)
|
|
return {"email": email, "cookies": normalized}
|
|
|
|
|
|
def _complete_lines(data):
|
|
"""Return newline-terminated records and the unconsumed trailing bytes."""
|
|
parts = data.split(b"\n")
|
|
return parts[:-1], parts[-1]
|
|
|
|
|
|
def _read_complete_file(path):
|
|
if not path.exists():
|
|
return []
|
|
lines, _incomplete = _complete_lines(path.read_bytes())
|
|
return [line for line in lines if line]
|
|
|
|
|
|
def load_snapshots(path, *, raw_lines=None):
|
|
snapshots = {}
|
|
scopes = {}
|
|
lines = _read_complete_file(path) if raw_lines is None else raw_lines
|
|
for line in lines:
|
|
document = _decode_json_line(line, "session")
|
|
email = document["email"]
|
|
cookies = document["cookies"]
|
|
if email in snapshots:
|
|
continue
|
|
snapshots[email] = {"email": email, "cookies": cookies}
|
|
for cookie in cookies:
|
|
name = cookie.get("name")
|
|
domain = cookie.get("domain")
|
|
if name in {"sso", "sso-rw"} and domain:
|
|
scopes[(name, domain)] = {
|
|
"name": name,
|
|
"domain": domain,
|
|
"path": cookie.get("path", "/"),
|
|
"secure": bool(cookie.get("secure", True)),
|
|
"httpOnly": bool(cookie.get("httpOnly", True)),
|
|
"sameSite": cookie.get("sameSite", "Lax"),
|
|
}
|
|
return snapshots, scopes
|
|
|
|
|
|
def export_sessions(sessions_path, accounts_path, *, session_lines=None):
|
|
snapshots, scopes = load_snapshots(sessions_path, raw_lines=session_lines)
|
|
for document in snapshots.values():
|
|
yield document
|
|
if not accounts_path.exists():
|
|
return
|
|
legacy_scopes = list(scopes.values()) or [LEGACY_COOKIE_SCOPE]
|
|
for raw in _read_complete_file(accounts_path):
|
|
try:
|
|
email, _password, sso = raw.decode("utf-8").rsplit(":", 2)
|
|
except (UnicodeDecodeError, ValueError) as exc:
|
|
raise ValueError("invalid account record") from exc
|
|
if not email or not sso or email in snapshots:
|
|
continue
|
|
cookies = [{**scope, "value": sso} for scope in legacy_scopes]
|
|
yield {"email": email, "cookies": cookies}
|
|
|
|
|
|
def _write_document(document):
|
|
payload = json.dumps(document, separators=(",", ":"))
|
|
if len(payload.encode("utf-8")) > MAX_RECORD_BYTES:
|
|
raise ValueError("invalid session record")
|
|
print(payload, flush=False)
|
|
|
|
|
|
def export_and_follow(sessions_path, accounts_path, *, poll_seconds=0.25):
|
|
"""Emit a complete snapshot, then losslessly follow the same JSONL fd."""
|
|
sessions_path.parent.mkdir(parents=True, exist_ok=True)
|
|
if not sessions_path.exists():
|
|
fd = os.open(sessions_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
os.close(fd)
|
|
os.chmod(sessions_path, 0o600)
|
|
stream = sessions_path.open("rb")
|
|
|
|
with stream:
|
|
opened = os.fstat(stream.fileno())
|
|
initial_lines, pending = _complete_lines(stream.read())
|
|
if len(pending) > MAX_RECORD_BYTES:
|
|
raise ValueError("invalid session record")
|
|
for document in export_sessions(
|
|
sessions_path,
|
|
accounts_path,
|
|
session_lines=[line for line in initial_lines if line],
|
|
):
|
|
_write_document(document)
|
|
sys.stdout.flush()
|
|
|
|
while True:
|
|
chunk = stream.read()
|
|
if chunk:
|
|
complete, pending = _complete_lines(pending + chunk)
|
|
if len(pending) > MAX_RECORD_BYTES:
|
|
raise ValueError("invalid session record")
|
|
for raw in complete:
|
|
if raw:
|
|
_write_document(_decode_json_line(raw, "session"))
|
|
sys.stdout.flush()
|
|
continue
|
|
|
|
try:
|
|
current = sessions_path.stat()
|
|
except FileNotFoundError:
|
|
return 3
|
|
if (
|
|
current.st_dev != opened.st_dev
|
|
or current.st_ino != opened.st_ino
|
|
or current.st_size < stream.tell()
|
|
):
|
|
return 3
|
|
time.sleep(poll_seconds)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--follow", action="store_true")
|
|
parser.add_argument("sessions_path", type=Path)
|
|
parser.add_argument("accounts_path", type=Path)
|
|
args = parser.parse_args()
|
|
try:
|
|
if args.follow:
|
|
raise SystemExit(export_and_follow(args.sessions_path, args.accounts_path))
|
|
for document in export_sessions(args.sessions_path, args.accounts_path):
|
|
_write_document(document)
|
|
except ValueError:
|
|
raise SystemExit(4) from None
|
|
except BrokenPipeError:
|
|
try:
|
|
sys.stdout.close()
|
|
finally:
|
|
raise SystemExit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|