| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- """OAuth helpers for the zhuce6 ChatGPT platform."""
- from __future__ import annotations
- import base64
- import hashlib
- import json
- import secrets
- import time
- import urllib.parse
- from dataclasses import dataclass
- from typing import Any
- from curl_cffi import requests as cffi_requests
- from .constants import (
- OAUTH_AUTH_URL,
- OAUTH_CLIENT_ID,
- OPENAI_IMPERSONATE,
- OPENAI_SEC_CH_UA,
- OPENAI_SEC_CH_UA_MOBILE,
- OPENAI_SEC_CH_UA_PLATFORM,
- OPENAI_USER_AGENT,
- OAUTH_REDIRECT_URI,
- OAUTH_SCOPE,
- OAUTH_TOKEN_URL,
- )
- def _b64url_no_pad(raw: bytes) -> str:
- return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
- def _sha256_b64url_no_pad(value: str) -> str:
- return _b64url_no_pad(hashlib.sha256(value.encode("ascii")).digest())
- def _random_state(nbytes: int = 16) -> str:
- return secrets.token_urlsafe(nbytes)
- def _pkce_verifier() -> str:
- return secrets.token_urlsafe(64)
- def _parse_callback_url(callback_url: str) -> dict[str, str]:
- candidate = callback_url.strip()
- if not candidate:
- return {"code": "", "state": "", "error": "", "error_description": ""}
- if "://" not in candidate:
- if candidate.startswith("?"):
- candidate = f"http://localhost{candidate}"
- elif "=" in candidate:
- candidate = f"http://localhost/?{candidate}"
- else:
- candidate = f"http://{candidate}"
- parsed = urllib.parse.urlparse(candidate)
- query = urllib.parse.parse_qs(parsed.query, keep_blank_values=True)
- fragment = urllib.parse.parse_qs(parsed.fragment, keep_blank_values=True)
- for key, values in fragment.items():
- if key not in query or not query[key]:
- query[key] = values
- def get1(key: str) -> str:
- return str((query.get(key, [""])[0] or "")).strip()
- return {
- "code": get1("code"),
- "state": get1("state"),
- "error": get1("error"),
- "error_description": get1("error_description"),
- }
- def _jwt_claims_no_verify(id_token: str) -> dict[str, Any]:
- if not id_token or id_token.count(".") < 2:
- return {}
- payload_b64 = id_token.split(".")[1]
- pad = "=" * ((4 - (len(payload_b64) % 4)) % 4)
- try:
- payload = base64.urlsafe_b64decode((payload_b64 + pad).encode("ascii"))
- return json.loads(payload.decode("utf-8"))
- except Exception:
- return {}
- def _to_int(value: Any) -> int:
- try:
- return int(value)
- except (TypeError, ValueError):
- return 0
- def _post_form(
- url: str,
- data: dict[str, str],
- timeout: int = 30,
- proxy_url: str | None = None,
- ) -> dict[str, Any]:
- proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
- response = cffi_requests.post(
- url,
- data=data,
- headers={
- "Content-Type": "application/x-www-form-urlencoded",
- "Accept": "application/json",
- "User-Agent": OPENAI_USER_AGENT,
- "sec-ch-ua": OPENAI_SEC_CH_UA,
- "sec-ch-ua-mobile": OPENAI_SEC_CH_UA_MOBILE,
- "sec-ch-ua-platform": OPENAI_SEC_CH_UA_PLATFORM,
- },
- timeout=timeout,
- proxies=proxies,
- impersonate=OPENAI_IMPERSONATE,
- )
- if response.status_code != 200:
- raise RuntimeError(f"token exchange failed: {response.status_code}: {response.text}")
- return response.json()
- @dataclass(frozen=True)
- class OAuthStart:
- auth_url: str
- state: str
- code_verifier: str
- redirect_uri: str
- def generate_oauth_url(
- *,
- redirect_uri: str = OAUTH_REDIRECT_URI,
- scope: str = OAUTH_SCOPE,
- client_id: str = OAUTH_CLIENT_ID,
- ) -> OAuthStart:
- state = _random_state()
- code_verifier = _pkce_verifier()
- code_challenge = _sha256_b64url_no_pad(code_verifier)
- import uuid as _uuid
- device_id = str(_uuid.uuid4())
- params = {
- "client_id": client_id,
- "scope": scope,
- "response_type": "code",
- "redirect_uri": redirect_uri,
- "audience": "https://api.openai.com/v1",
- "device_id": device_id,
- "prompt": "login",
- "ext-oai-did": device_id,
- "ext-passkey-client-capabilities": "1111",
- "screen_hint": "signup",
- "state": state,
- "code_challenge": code_challenge,
- "code_challenge_method": "S256",
- }
- auth_url = f"{OAUTH_AUTH_URL}?{urllib.parse.urlencode(params)}"
- return OAuthStart(
- auth_url=auth_url,
- state=state,
- code_verifier=code_verifier,
- redirect_uri=redirect_uri,
- )
- def submit_callback_url(
- *,
- callback_url: str,
- expected_state: str,
- code_verifier: str,
- redirect_uri: str = OAUTH_REDIRECT_URI,
- client_id: str = OAUTH_CLIENT_ID,
- token_url: str = OAUTH_TOKEN_URL,
- proxy_url: str | None = None,
- ) -> str:
- callback = _parse_callback_url(callback_url)
- if callback["error"]:
- raise RuntimeError(f"oauth error: {callback['error']}: {callback['error_description']}".strip())
- if not callback["code"]:
- raise ValueError("callback url missing ?code=")
- if not callback["state"]:
- raise ValueError("callback url missing ?state=")
- if callback["state"] != expected_state:
- raise ValueError("state mismatch")
- token_resp = _post_form(
- token_url,
- {
- "grant_type": "authorization_code",
- "client_id": client_id,
- "code": callback["code"],
- "redirect_uri": redirect_uri,
- "code_verifier": code_verifier,
- },
- proxy_url=proxy_url,
- )
- access_token = str(token_resp.get("access_token") or "").strip()
- refresh_token = str(token_resp.get("refresh_token") or "").strip()
- id_token = str(token_resp.get("id_token") or "").strip()
- expires_in = _to_int(token_resp.get("expires_in"))
- claims = _jwt_claims_no_verify(id_token)
- email = str(claims.get("email") or "").strip()
- auth_claims = claims.get("https://api.openai.com/auth") or {}
- account_id = str(auth_claims.get("chatgpt_account_id") or "").strip()
- now = int(time.time())
- expired_rfc3339 = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now + max(expires_in, 0)))
- now_rfc3339 = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now))
- config = {
- "id_token": id_token,
- "access_token": access_token,
- "refresh_token": refresh_token,
- "account_id": account_id,
- "last_refresh": now_rfc3339,
- "email": email,
- "type": "codex",
- "expired": expired_rfc3339,
- }
- return json.dumps(config, ensure_ascii=False, separators=(",", ":"))
- class OAuthManager:
- def __init__(
- self,
- client_id: str = OAUTH_CLIENT_ID,
- auth_url: str = OAUTH_AUTH_URL,
- token_url: str = OAUTH_TOKEN_URL,
- redirect_uri: str = OAUTH_REDIRECT_URI,
- scope: str = OAUTH_SCOPE,
- proxy_url: str | None = None,
- ) -> None:
- self.client_id = client_id
- self.auth_url = auth_url
- self.token_url = token_url
- self.redirect_uri = redirect_uri
- self.scope = scope
- self.proxy_url = proxy_url
- def start_oauth(self) -> OAuthStart:
- return generate_oauth_url(
- redirect_uri=self.redirect_uri,
- scope=self.scope,
- client_id=self.client_id,
- )
- def handle_callback(self, callback_url: str, expected_state: str, code_verifier: str) -> dict[str, Any]:
- return json.loads(
- submit_callback_url(
- callback_url=callback_url,
- expected_state=expected_state,
- code_verifier=code_verifier,
- redirect_uri=self.redirect_uri,
- client_id=self.client_id,
- token_url=self.token_url,
- proxy_url=self.proxy_url,
- )
- )
|