Merge pull request #55 from genz27/feat/sentinel-token-pow

feat: 实现完整的 openai-sentinel-token PoW 验证机制
This commit is contained in:
TheSmallHanCat
2026-01-10 16:44:18 +08:00
committed by GitHub

View File

@@ -1,35 +1,207 @@
"""Sora API client module""" """Sora API client module"""
import base64 import base64
import hashlib
import json
import io import io
import time import time
import random import random
import string import string
import re import re
from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any, Tuple from typing import Optional, Dict, Any, Tuple
from uuid import uuid4
from curl_cffi.requests import AsyncSession from curl_cffi.requests import AsyncSession
from curl_cffi import CurlMime from curl_cffi import CurlMime
from .proxy_manager import ProxyManager from .proxy_manager import ProxyManager
from ..core.config import config from ..core.config import config
from ..core.logger import debug_logger from ..core.logger import debug_logger
# PoW related constants
POW_MAX_ITERATION = 500000
POW_CORES = [8, 16, 24, 32]
POW_SCRIPTS = [
"https://cdn.oaistatic.com/_next/static/cXh69klOLzS0Gy2joLDRS/_ssgManifest.js?dpl=453ebaec0d44c2decab71692e1bfe39be35a24b3"
]
POW_DPL = ["prod-f501fe933b3edf57aea882da888e1a544df99840"]
POW_NAVIGATOR_KEYS = [
"registerProtocolHandlerfunction registerProtocolHandler() { [native code] }",
"storage[object StorageManager]",
"locks[object LockManager]",
"appCodeNameMozilla",
"permissions[object Permissions]",
"webdriverfalse",
"vendorGoogle Inc.",
"mediaDevices[object MediaDevices]",
"cookieEnabledtrue",
"productGecko",
"productSub20030107",
"hardwareConcurrency32",
"onLinetrue",
]
POW_DOCUMENT_KEYS = ["_reactListeningo743lnnpvdg", "location"]
POW_WINDOW_KEYS = [
"0", "window", "self", "document", "name", "location",
"navigator", "screen", "innerWidth", "innerHeight",
"localStorage", "sessionStorage", "crypto", "performance",
"fetch", "setTimeout", "setInterval", "console",
]
class SoraClient: class SoraClient:
"""Sora API client with proxy support""" """Sora API client with proxy support"""
CHATGPT_BASE_URL = "https://chatgpt.com"
SENTINEL_FLOW = "sora_2_create_task"
def __init__(self, proxy_manager: ProxyManager): def __init__(self, proxy_manager: ProxyManager):
self.proxy_manager = proxy_manager self.proxy_manager = proxy_manager
self.base_url = config.sora_base_url self.base_url = config.sora_base_url
self.timeout = config.sora_timeout self.timeout = config.sora_timeout
@staticmethod @staticmethod
def _generate_sentinel_token() -> str: def _get_pow_parse_time() -> str:
""" """Generate time string for PoW (EST timezone)"""
生成 openai-sentinel-token now = datetime.now(timezone(timedelta(hours=-5)))
根据测试文件的逻辑,传入任意随机字符即可 return now.strftime("%a %b %d %Y %H:%M:%S") + " GMT-0500 (Eastern Standard Time)"
生成10-20个字符的随机字符串字母+数字)
""" @staticmethod
length = random.randint(10, 20) def _get_pow_config(user_agent: str) -> list:
random_str = ''.join(random.choices(string.ascii_letters + string.digits, k=length)) """Generate PoW config array with browser fingerprint"""
return random_str return [
random.choice([1920 + 1080, 2560 + 1440, 1920 + 1200, 2560 + 1600]),
SoraClient._get_pow_parse_time(),
4294705152,
0, # [3] dynamic
user_agent,
random.choice(POW_SCRIPTS) if POW_SCRIPTS else "",
random.choice(POW_DPL) if POW_DPL else None,
"en-US",
"en-US,es-US,en,es",
0, # [9] dynamic
random.choice(POW_NAVIGATOR_KEYS),
random.choice(POW_DOCUMENT_KEYS),
random.choice(POW_WINDOW_KEYS),
time.perf_counter() * 1000,
str(uuid4()),
"",
random.choice(POW_CORES),
time.time() * 1000 - (time.perf_counter() * 1000),
]
@staticmethod
def _solve_pow(seed: str, difficulty: str, config_list: list) -> Tuple[str, bool]:
"""Execute PoW calculation using SHA3-512 hash collision"""
diff_len = len(difficulty) // 2
seed_encoded = seed.encode()
target_diff = bytes.fromhex(difficulty)
static_part1 = (json.dumps(config_list[:3], separators=(',', ':'), ensure_ascii=False)[:-1] + ',').encode()
static_part2 = (',' + json.dumps(config_list[4:9], separators=(',', ':'), ensure_ascii=False)[1:-1] + ',').encode()
static_part3 = (',' + json.dumps(config_list[10:], separators=(',', ':'), ensure_ascii=False)[1:]).encode()
for i in range(POW_MAX_ITERATION):
dynamic_i = str(i).encode()
dynamic_j = str(i >> 1).encode()
final_json = static_part1 + dynamic_i + static_part2 + dynamic_j + static_part3
b64_encoded = base64.b64encode(final_json)
hash_value = hashlib.sha3_512(seed_encoded + b64_encoded).digest()
if hash_value[:diff_len] <= target_diff:
return b64_encoded.decode(), True
error_token = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" + base64.b64encode(f'"{seed}"'.encode()).decode()
return error_token, False
@staticmethod
def _get_pow_token(user_agent: str) -> str:
"""Generate initial PoW token"""
config_list = SoraClient._get_pow_config(user_agent)
seed = format(random.random())
difficulty = "0fffff"
solution, _ = SoraClient._solve_pow(seed, difficulty, config_list)
return "gAAAAAC" + solution
@staticmethod
def _build_sentinel_token(
flow: str,
req_id: str,
pow_token: str,
resp: Dict[str, Any],
user_agent: str,
) -> str:
"""Build openai-sentinel-token from PoW response"""
final_pow_token = pow_token
# Check if PoW is required
proofofwork = resp.get("proofofwork", {})
if proofofwork.get("required"):
seed = proofofwork.get("seed", "")
difficulty = proofofwork.get("difficulty", "")
if seed and difficulty:
config_list = SoraClient._get_pow_config(user_agent)
solution, success = SoraClient._solve_pow(seed, difficulty, config_list)
final_pow_token = "gAAAAAB" + solution
if not success:
debug_logger.log_warning("PoW calculation failed, using error token")
token_payload = {
"p": final_pow_token,
"t": resp.get("turnstile", {}).get("dx", ""),
"c": resp.get("token", ""),
"id": req_id,
"flow": flow,
}
return json.dumps(token_payload, ensure_ascii=False, separators=(",", ":"))
async def _generate_sentinel_token(self, token: Optional[str] = None) -> str:
"""Generate openai-sentinel-token by calling /backend-api/sentinel/req and solving PoW"""
req_id = str(uuid4())
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
pow_token = self._get_pow_token(user_agent)
proxy_url = await self.proxy_manager.get_proxy_url()
# Request sentinel/req endpoint
url = f"{self.CHATGPT_BASE_URL}/backend-api/sentinel/req"
payload = {"p": pow_token, "flow": self.SENTINEL_FLOW, "id": req_id}
headers = {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Origin": "https://sora.chatgpt.com",
"Referer": "https://sora.chatgpt.com/",
"User-Agent": user_agent,
}
if token:
headers["Authorization"] = f"Bearer {token}"
async with AsyncSession() as session:
kwargs = {
"headers": headers,
"json": payload,
"timeout": 10,
"impersonate": "chrome"
}
if proxy_url:
kwargs["proxy"] = proxy_url
response = await session.post(url, **kwargs)
if response.status_code not in [200, 201]:
debug_logger.log_error(
error_message=f"Sentinel request failed: {response.status_code}",
status_code=response.status_code,
response_text=response.text
)
raise Exception(f"Sentinel request failed: {response.status_code}")
resp = response.json()
# Build final sentinel token
sentinel_token = self._build_sentinel_token(
self.SENTINEL_FLOW, req_id, pow_token, resp, user_agent
)
return sentinel_token
@staticmethod @staticmethod
def is_storyboard_prompt(prompt: str) -> bool: def is_storyboard_prompt(prompt: str) -> bool:
@@ -117,7 +289,7 @@ class SoraClient:
# 只在生成请求时添加 sentinel token # 只在生成请求时添加 sentinel token
if add_sentinel_token: if add_sentinel_token:
headers["openai-sentinel-token"] = self._generate_sentinel_token() headers["openai-sentinel-token"] = await self._generate_sentinel_token(token)
if not multipart: if not multipart:
headers["Content-Type"] = "application/json" headers["Content-Type"] = "application/json"