清理代码
This commit is contained in:
465
core/flow.py
465
core/flow.py
@@ -1,32 +1,13 @@
|
||||
"""
|
||||
OpenAI 账号注册流程编排模块
|
||||
|
||||
完整的注册流程实现,包含以下步骤:
|
||||
1. 初始化会话(访问主页 + API providers)
|
||||
2. 获取 CSRF Token
|
||||
3. OAuth 流程(跳转到 auth.openai.com)
|
||||
4. Sentinel 握手(获取 Token)
|
||||
5. 提交注册信息(邮箱 + 密码)
|
||||
6. 触发邮件验证(可能遇到 Cloudflare 403)
|
||||
7. 提交 OTP 验证码
|
||||
8. 完成用户信息(姓名 + 生日)
|
||||
|
||||
参考文档: /home/carry/myprj/gptAutoPlus/docs/开发文档.md
|
||||
"""
|
||||
"""OpenAI 账号注册流程编排模块"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
import secrets
|
||||
import random
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from core.session import (
|
||||
OAISession,
|
||||
CloudflareBlockError,
|
||||
SessionInvalidError,
|
||||
RateLimitError
|
||||
)
|
||||
from core.sentinel import SentinelHandler
|
||||
from core.session import OAISession, CloudflareBlockError, SessionInvalidError, RateLimitError
|
||||
from core.sentinel_handler import SentinelHandler
|
||||
from core.challenge import CloudflareSolver
|
||||
from utils.mail_box import MailHandler
|
||||
from utils.crypto import generate_random_password
|
||||
@@ -34,13 +15,8 @@ from utils.logger import logger
|
||||
|
||||
|
||||
class RegisterFlow:
|
||||
"""
|
||||
OpenAI 账号注册流程编排器
|
||||
"""OpenAI 账号注册流程编排器"""
|
||||
|
||||
负责协调各个组件,按照正确的顺序执行注册流程
|
||||
"""
|
||||
|
||||
# OpenAI 相关 URL
|
||||
CHATGPT_HOME = "https://chatgpt.com/"
|
||||
CHATGPT_PROVIDERS = "https://chatgpt.com/api/auth/providers"
|
||||
CHATGPT_CSRF = "https://chatgpt.com/api/auth/csrf"
|
||||
@@ -52,96 +28,47 @@ class RegisterFlow:
|
||||
AUTH_VALIDATE_OTP = "https://auth.openai.com/api/accounts/email-otp/validate"
|
||||
AUTH_COMPLETE_PROFILE = "https://auth.openai.com/api/accounts/create_account"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session: OAISession,
|
||||
config,
|
||||
email: Optional[str] = None,
|
||||
password: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
初始化注册流程
|
||||
|
||||
参数:
|
||||
session: OAISession 实例(已配置 TLS 指纹和代理)
|
||||
config: AppConfig 配置对象
|
||||
email: 注册邮箱(可选,不提供则自动生成)
|
||||
password: 密码(可选,不提供则自动生成)
|
||||
"""
|
||||
def __init__(self, session: OAISession, config, email: Optional[str] = None, password: Optional[str] = None):
|
||||
self.s = session
|
||||
self.config = config
|
||||
self.email = email or self._generate_email()
|
||||
self.password = password or generate_random_password()
|
||||
|
||||
# 初始化子模块
|
||||
self.sentinel = SentinelHandler(session)
|
||||
self.mail = MailHandler.create(
|
||||
config.mail.to_dict() if config.mail.enabled else None
|
||||
)
|
||||
self.mail = MailHandler.create(config.mail.to_dict() if config.mail.enabled else None)
|
||||
self.cloudflare_solver = CloudflareSolver()
|
||||
|
||||
# 流程状态
|
||||
self.csrf_token: Optional[str] = None
|
||||
self.sentinel_token: Optional[Dict[str, Any]] = None
|
||||
self.otp: Optional[str] = None
|
||||
|
||||
logger.info(
|
||||
f"RegisterFlow initialized for {self.email} "
|
||||
f"(oai-did: {self.s.oai_did})"
|
||||
)
|
||||
logger.info(f"RegisterFlow initialized for {self.email} (oai-did: {self.s.oai_did})")
|
||||
|
||||
async def run(self) -> Dict[str, Any]:
|
||||
"""
|
||||
执行完整注册流程
|
||||
|
||||
返回:
|
||||
注册结果字典,包含:
|
||||
- email: 注册邮箱
|
||||
- password: 密码
|
||||
- status: 状态 ("success", "failed", "pending_otp", etc.)
|
||||
- error: 错误信息(如果失败)
|
||||
- message: 额外信息
|
||||
"""
|
||||
"""执行完整注册流程"""
|
||||
try:
|
||||
logger.info(f"[{self.email}] Starting registration flow")
|
||||
|
||||
# Step 0: 如果使用 CloudMail,确保邮箱账户存在
|
||||
# Step 0: CloudMail 邮箱账户
|
||||
if self.config.mail.enabled and self.config.mail.type == "cloudmail":
|
||||
try:
|
||||
from utils.mail_box import CloudMailHandler
|
||||
if isinstance(self.mail, CloudMailHandler):
|
||||
logger.info(f"[{self.email}] Step 0: Ensuring email account exists in CloudMail")
|
||||
logger.info(f"[{self.email}] Step 0: Ensuring email account exists")
|
||||
await self.mail.ensure_email_exists(self.email)
|
||||
logger.info(f"[{self.email}] ✓ Email account ready")
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.email}] Failed to create CloudMail account: {e}")
|
||||
# 继续执行,可能邮箱已经存在
|
||||
|
||||
# Step 1: 初始化会话
|
||||
await self._step1_init_session()
|
||||
|
||||
# Step 2: 获取 CSRF Token
|
||||
await self._step2_get_csrf_token()
|
||||
|
||||
# Step 3: OAuth 流程
|
||||
await self._step3_oauth_signin()
|
||||
|
||||
# Step 4: Sentinel 握手
|
||||
await self._step4_get_sentinel_token()
|
||||
|
||||
# Step 5: 提交注册信息
|
||||
await self._step5_submit_registration()
|
||||
|
||||
# Step 6: 触发邮件验证
|
||||
await self._step6_send_email_otp()
|
||||
|
||||
# Step 7: 提交 OTP
|
||||
await self._step7_submit_otp()
|
||||
|
||||
# Step 8: 完成用户信息
|
||||
await self._step8_complete_profile()
|
||||
|
||||
# 注册成功
|
||||
logger.success(f"[{self.email}] Registration completed successfully! ✅")
|
||||
return {
|
||||
"email": self.email,
|
||||
@@ -153,437 +80,197 @@ class RegisterFlow:
|
||||
|
||||
except CloudflareBlockError as e:
|
||||
logger.error(f"[{self.email}] Cloudflare blocked: {e}")
|
||||
return {
|
||||
"email": self.email,
|
||||
"password": self.password,
|
||||
"status": "cloudflare_blocked",
|
||||
"error": str(e),
|
||||
"message": "Encountered Cloudflare challenge. Consider using residential proxy or solver."
|
||||
}
|
||||
return {"email": self.email, "password": self.password, "status": "cloudflare_blocked", "error": str(e)}
|
||||
|
||||
except SessionInvalidError as e:
|
||||
logger.error(f"[{self.email}] Session invalid (409): {e}")
|
||||
return {
|
||||
"email": self.email,
|
||||
"password": self.password,
|
||||
"status": "session_invalid",
|
||||
"error": str(e),
|
||||
"message": "Session conflict. CSRF token expired. Retry recommended."
|
||||
}
|
||||
logger.error(f"[{self.email}] Session invalid: {e}")
|
||||
return {"email": self.email, "password": self.password, "status": "session_invalid", "error": str(e)}
|
||||
|
||||
except RateLimitError as e:
|
||||
logger.error(f"[{self.email}] Rate limited (429): {e}")
|
||||
return {
|
||||
"email": self.email,
|
||||
"password": self.password,
|
||||
"status": "rate_limited",
|
||||
"error": str(e),
|
||||
"message": "Rate limit exceeded. Wait and retry with different IP."
|
||||
}
|
||||
logger.error(f"[{self.email}] Rate limited: {e}")
|
||||
return {"email": self.email, "password": self.password, "status": "rate_limited", "error": str(e)}
|
||||
|
||||
except NotImplementedError as e:
|
||||
logger.warning(f"[{self.email}] Feature not implemented: {e}")
|
||||
return {
|
||||
"email": self.email,
|
||||
"password": self.password,
|
||||
"status": "pending_manual",
|
||||
"error": str(e),
|
||||
"message": "Partial success. User needs to complete manual steps (Sentinel or OTP)."
|
||||
}
|
||||
return {"email": self.email, "password": self.password, "status": "pending_manual", "error": str(e)}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"[{self.email}] Unexpected error during registration")
|
||||
return {
|
||||
"email": self.email,
|
||||
"password": self.password,
|
||||
"status": "failed",
|
||||
"error": str(e),
|
||||
"message": f"Registration failed: {type(e).__name__}"
|
||||
}
|
||||
logger.exception(f"[{self.email}] Unexpected error")
|
||||
return {"email": self.email, "password": self.password, "status": "failed", "error": str(e)}
|
||||
|
||||
async def _step1_init_session(self):
|
||||
"""
|
||||
Step 1: 初始化会话
|
||||
|
||||
访问 ChatGPT 主页和 API providers 端点,建立基础会话
|
||||
"""
|
||||
"""Step 1: 初始化会话"""
|
||||
logger.info(f"[{self.email}] Step 1: Initializing session")
|
||||
|
||||
# 访问主页
|
||||
resp = self.s.get(self.CHATGPT_HOME)
|
||||
logger.debug(f" - GET {self.CHATGPT_HOME}: {resp.status_code}")
|
||||
|
||||
# 获取 auth providers
|
||||
resp = self.s.get(self.CHATGPT_PROVIDERS)
|
||||
logger.debug(f" - GET {self.CHATGPT_PROVIDERS}: {resp.status_code}")
|
||||
|
||||
self.s.get(self.CHATGPT_HOME)
|
||||
self.s.get(self.CHATGPT_PROVIDERS)
|
||||
logger.info(f"[{self.email}] ✓ Session initialized")
|
||||
|
||||
async def _step2_get_csrf_token(self):
|
||||
"""
|
||||
Step 2: 获取 CSRF Token
|
||||
|
||||
CSRF Token 用于后续的 OAuth 登录流程
|
||||
"""
|
||||
"""Step 2: 获取 CSRF Token"""
|
||||
logger.info(f"[{self.email}] Step 2: Getting CSRF token")
|
||||
|
||||
resp = self.s.get(self.CHATGPT_CSRF)
|
||||
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"Failed to get CSRF token: {resp.status_code}")
|
||||
|
||||
data = resp.json()
|
||||
self.csrf_token = data.get("csrfToken")
|
||||
|
||||
self.csrf_token = resp.json().get("csrfToken")
|
||||
if not self.csrf_token:
|
||||
raise RuntimeError(f"CSRF token not found in response: {data}")
|
||||
|
||||
logger.info(f"[{self.email}] ✓ CSRF token obtained: {self.csrf_token[:20]}...")
|
||||
raise RuntimeError("CSRF token not found")
|
||||
logger.info(f"[{self.email}] ✓ CSRF token obtained")
|
||||
|
||||
async def _step3_oauth_signin(self):
|
||||
"""
|
||||
Step 3: OAuth 登录流程
|
||||
|
||||
启动 OAuth 流程,跳转到 auth.openai.com
|
||||
确保获取所有必要的 session cookies
|
||||
"""
|
||||
"""Step 3: OAuth 登录流程"""
|
||||
logger.info(f"[{self.email}] Step 3: Starting OAuth flow")
|
||||
|
||||
# 生成 auth_session_logging_id
|
||||
import uuid
|
||||
auth_session_logging_id = str(uuid.uuid4())
|
||||
|
||||
# 发起 OAuth signin 请求(添加关键参数)
|
||||
signin_params = {
|
||||
'prompt': 'login',
|
||||
'ext-oai-did': self.s.oai_did,
|
||||
'auth_session_logging_id': auth_session_logging_id,
|
||||
'screen_hint': 'signup', # 🔥 明确指定注册
|
||||
'login_hint': self.email, # 🔥 传入邮箱
|
||||
'auth_session_logging_id': str(uuid.uuid4()),
|
||||
'screen_hint': 'signup',
|
||||
'login_hint': self.email,
|
||||
}
|
||||
|
||||
payload = {
|
||||
"callbackUrl": "/",
|
||||
"csrfToken": self.csrf_token,
|
||||
"json": "true"
|
||||
}
|
||||
payload = {"callbackUrl": "/", "csrfToken": self.csrf_token, "json": "true"}
|
||||
|
||||
resp = self.s.post(
|
||||
self.CHATGPT_SIGNIN,
|
||||
params=signin_params, # ✅ 添加 URL 参数
|
||||
params=signin_params,
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"OAuth signin failed: {resp.status_code} - {resp.text[:200]}")
|
||||
|
||||
data = resp.json()
|
||||
auth_url = data.get("url")
|
||||
raise RuntimeError(f"OAuth signin failed: {resp.status_code}")
|
||||
|
||||
auth_url = resp.json().get("url")
|
||||
if not auth_url:
|
||||
raise RuntimeError(f"OAuth URL not found in response: {data}")
|
||||
raise RuntimeError("OAuth URL not found")
|
||||
|
||||
logger.debug(f" - OAuth URL: {auth_url[:100]}...")
|
||||
|
||||
# 访问 OAuth 跳转链接(建立 auth.openai.com 会话)
|
||||
# 这一步会设置关键的 cookies: login_session, oai-client-auth-session, auth_provider 等
|
||||
resp = self.s.get(auth_url, allow_redirects=True)
|
||||
logger.debug(f" - GET OAuth URL: {resp.status_code}")
|
||||
logger.debug(f" - Final URL after redirects: {resp.url}")
|
||||
|
||||
# 检查关键 cookies 是否已设置
|
||||
important_cookies = ["login_session", "oai-client-auth-session"]
|
||||
cookies_status = {
|
||||
cookie: cookie in self.s.client.cookies
|
||||
for cookie in important_cookies
|
||||
}
|
||||
logger.debug(f" - Cookies status: {cookies_status}")
|
||||
|
||||
# 访问注册页面
|
||||
resp = self.s.get(self.AUTH_CREATE_ACCOUNT)
|
||||
logger.debug(f" - GET create-account page: {resp.status_code}")
|
||||
|
||||
logger.info(f"[{self.email}] ✓ OAuth flow completed, redirected to auth.openai.com")
|
||||
self.s.get(auth_url, allow_redirects=True)
|
||||
self.s.get(self.AUTH_CREATE_ACCOUNT)
|
||||
logger.info(f"[{self.email}] ✓ OAuth flow completed")
|
||||
|
||||
async def _step4_get_sentinel_token(self):
|
||||
"""
|
||||
Step 4: Sentinel 握手
|
||||
|
||||
获取 Sentinel Token 用于提交注册信息
|
||||
✅ 已集成完整的 Sentinel 解决方案
|
||||
"""
|
||||
"""Step 4: Sentinel 握手"""
|
||||
logger.info(f"[{self.email}] Step 4: Getting Sentinel token")
|
||||
|
||||
try:
|
||||
self.sentinel_token = await self.sentinel.get_token(
|
||||
flow="username_password_create"
|
||||
)
|
||||
logger.info(f"[{self.email}] ✓ Sentinel token obtained: {str(self.sentinel_token)[:50]}...")
|
||||
|
||||
self.sentinel_token = await self.sentinel.get_token(flow="username_password_create")
|
||||
logger.info(f"[{self.email}] ✓ Sentinel token obtained")
|
||||
except (NotImplementedError, ImportError) as e:
|
||||
logger.error(
|
||||
f"[{self.email}] ❌ Sentinel solver not available: {e}"
|
||||
)
|
||||
# 重新抛出异常,让调用方知道需要修复
|
||||
logger.error(f"[{self.email}] Sentinel solver not available: {e}")
|
||||
raise
|
||||
|
||||
async def _step5_submit_registration(self):
|
||||
"""
|
||||
Step 5: 提交注册信息
|
||||
|
||||
POST /api/accounts/user/register
|
||||
提交用户名(邮箱)、密码,Sentinel Token 放在 Header 中
|
||||
"""
|
||||
"""Step 5: 提交注册信息"""
|
||||
logger.info(f"[{self.email}] Step 5: Submitting registration")
|
||||
|
||||
# 请求 Body:username 和 password
|
||||
payload = {
|
||||
"username": self.email, # ✅ 改为 username
|
||||
"password": self.password
|
||||
}
|
||||
payload = {"username": self.email, "password": self.password}
|
||||
|
||||
# Sentinel Token 作为 Header 传递(JSON 字符串格式)
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Origin": "https://auth.openai.com",
|
||||
"Referer": self.AUTH_CREATE_ACCOUNT,
|
||||
"Openai-Sentinel-Token": json.dumps(self.sentinel_token), # ✅ 注意大小写
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"Priority": "u=1, i",
|
||||
}
|
||||
|
||||
# 添加 Datadog tracing headers(模拟真实浏览器)
|
||||
headers.update({
|
||||
"Openai-Sentinel-Token": json.dumps(self.sentinel_token),
|
||||
"X-Datadog-Trace-Id": str(secrets.randbits(63)),
|
||||
"X-Datadog-Parent-Id": str(secrets.randbits(63)),
|
||||
"X-Datadog-Sampling-Priority": "1",
|
||||
"X-Datadog-Origin": "rum",
|
||||
"Traceparent": f"00-0000000000000000{secrets.token_hex(8)}-{secrets.token_hex(8)}-01",
|
||||
"Tracestate": "dd=s:1;o:rum",
|
||||
})
|
||||
}
|
||||
|
||||
resp = self.s.post(
|
||||
self.AUTH_REGISTER,
|
||||
json=payload,
|
||||
headers=headers
|
||||
)
|
||||
resp = self.s.post(self.AUTH_REGISTER, json=payload, headers=headers)
|
||||
|
||||
if resp.status_code != 200:
|
||||
error_msg = resp.text[:300]
|
||||
logger.error(f" - Registration failed: {resp.status_code} - {error_msg}")
|
||||
|
||||
# 检查常见错误
|
||||
if "email already exists" in resp.text.lower():
|
||||
raise ValueError(f"Email already registered: {self.email}")
|
||||
elif "invalid token" in resp.text.lower():
|
||||
raise ValueError("Invalid Sentinel token. Check your Sentinel solver.")
|
||||
else:
|
||||
raise RuntimeError(f"Registration submission failed: {resp.status_code} - {error_msg}")
|
||||
raise RuntimeError(f"Registration failed: {resp.status_code}")
|
||||
|
||||
logger.info(f"[{self.email}] ✓ Registration info submitted successfully")
|
||||
logger.info(f"[{self.email}] ✓ Registration submitted")
|
||||
|
||||
async def _step6_send_email_otp(self):
|
||||
"""
|
||||
Step 6: 触发邮件验证
|
||||
|
||||
POST /api/accounts/email-otp/send
|
||||
触发 OpenAI 发送 OTP 验证码到注册邮箱
|
||||
|
||||
⚠️ 此步骤最容易触发 Cloudflare 403
|
||||
"""
|
||||
"""Step 6: 触发邮件验证"""
|
||||
logger.info(f"[{self.email}] Step 6: Sending email OTP")
|
||||
|
||||
resp = self.s.post(
|
||||
self.AUTH_SEND_OTP,
|
||||
json={},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Referer": self.AUTH_CREATE_ACCOUNT
|
||||
}
|
||||
headers={"Content-Type": "application/json", "Referer": self.AUTH_CREATE_ACCOUNT}
|
||||
)
|
||||
|
||||
# 检查 Cloudflare 拦截
|
||||
if resp.status_code == 403:
|
||||
if CloudflareSolver.detect_challenge(resp):
|
||||
logger.error(f"[{self.email}] ⚠️ Cloudflare challenge detected at OTP send")
|
||||
raise CloudflareBlockError(
|
||||
"Cloudflare Turnstile challenge triggered when sending OTP. "
|
||||
"Recommendations: use residential proxy or integrate captcha solver."
|
||||
)
|
||||
if resp.status_code == 403 and CloudflareSolver.detect_challenge(resp):
|
||||
raise CloudflareBlockError("Cloudflare challenge triggered")
|
||||
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(
|
||||
f"Email OTP send failed: {resp.status_code} - {resp.text[:200]}"
|
||||
)
|
||||
raise RuntimeError(f"OTP send failed: {resp.status_code}")
|
||||
|
||||
logger.info(f"[{self.email}] ✓ OTP email sent successfully")
|
||||
logger.info(f"[{self.email}] ✓ OTP email sent")
|
||||
|
||||
async def _step7_submit_otp(self):
|
||||
"""
|
||||
Step 7: 提交 OTP 验证码
|
||||
|
||||
等待邮件接收 OTP,然后提交验证
|
||||
|
||||
⚠️ 用户需要配置邮箱服务
|
||||
"""
|
||||
"""Step 7: 提交 OTP 验证码"""
|
||||
logger.info(f"[{self.email}] Step 7: Waiting for OTP")
|
||||
|
||||
try:
|
||||
# 等待 OTP(最多 5 分钟)
|
||||
self.otp = await self.mail.wait_for_otp(
|
||||
email=self.email,
|
||||
timeout=300
|
||||
)
|
||||
self.otp = await self.mail.wait_for_otp(email=self.email, timeout=300)
|
||||
logger.info(f"[{self.email}] ✓ OTP received: {self.otp}")
|
||||
|
||||
except NotImplementedError:
|
||||
logger.warning(
|
||||
f"[{self.email}] ⚠️ Mail handler not configured, cannot retrieve OTP"
|
||||
)
|
||||
# 重新抛出异常,让调用方知道需要手动输入 OTP
|
||||
logger.warning(f"[{self.email}] Mail handler not configured")
|
||||
raise
|
||||
|
||||
except TimeoutError:
|
||||
logger.error(f"[{self.email}] ⏱ Timeout waiting for OTP")
|
||||
raise TimeoutError(
|
||||
f"Timeout waiting for OTP email (5 minutes). "
|
||||
f"Please check email: {self.email}"
|
||||
)
|
||||
raise TimeoutError(f"Timeout waiting for OTP: {self.email}")
|
||||
|
||||
# 提交 OTP
|
||||
payload = {"code": self.otp}
|
||||
resp = self.s.post(
|
||||
self.AUTH_VALIDATE_OTP,
|
||||
json=payload,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Referer": self.AUTH_CREATE_ACCOUNT
|
||||
}
|
||||
json={"code": self.otp},
|
||||
headers={"Content-Type": "application/json", "Referer": self.AUTH_CREATE_ACCOUNT}
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
error_msg = resp.text[:200]
|
||||
logger.error(f" - OTP validation failed: {resp.status_code} - {error_msg}")
|
||||
raise RuntimeError(f"OTP validation failed: {resp.status_code}")
|
||||
|
||||
if "invalid code" in resp.text.lower():
|
||||
raise ValueError(f"Invalid OTP code: {self.otp}")
|
||||
else:
|
||||
raise RuntimeError(f"OTP validation failed: {resp.status_code} - {error_msg}")
|
||||
|
||||
logger.info(f"[{self.email}] ✓ OTP validated successfully")
|
||||
logger.info(f"[{self.email}] ✓ OTP validated")
|
||||
|
||||
async def _step8_complete_profile(self):
|
||||
"""
|
||||
Step 8: 完成用户信息
|
||||
|
||||
POST /api/accounts/create_account
|
||||
提交姓名和生日,完成注册
|
||||
"""
|
||||
"""Step 8: 完成用户信息"""
|
||||
logger.info(f"[{self.email}] Step 8: Completing profile")
|
||||
|
||||
name = self._generate_name()
|
||||
birthdate = self._generate_birthdate()
|
||||
|
||||
payload = {
|
||||
"name": name,
|
||||
"birthdate": birthdate
|
||||
}
|
||||
|
||||
resp = self.s.post(
|
||||
self.AUTH_COMPLETE_PROFILE,
|
||||
json=payload,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Referer": self.AUTH_CREATE_ACCOUNT
|
||||
}
|
||||
json={"name": name, "birthdate": birthdate},
|
||||
headers={"Content-Type": "application/json", "Referer": self.AUTH_CREATE_ACCOUNT}
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(
|
||||
f"Profile completion failed: {resp.status_code} - {resp.text[:200]}"
|
||||
)
|
||||
raise RuntimeError(f"Profile completion failed: {resp.status_code}")
|
||||
|
||||
logger.info(
|
||||
f"[{self.email}] ✓ Profile completed: name={name}, birthdate={birthdate}"
|
||||
)
|
||||
|
||||
# ========== 辅助方法 ==========
|
||||
logger.info(f"[{self.email}] ✓ Profile completed")
|
||||
|
||||
def _generate_email(self) -> str:
|
||||
"""
|
||||
生成随机邮箱
|
||||
|
||||
如果启用了 CloudMail 且配置了域名,使用 CloudMail 域名
|
||||
否则使用 example.com(需要用户替换)
|
||||
|
||||
返回:
|
||||
邮箱地址
|
||||
"""
|
||||
"""生成随机邮箱"""
|
||||
random_part = secrets.token_hex(8)
|
||||
|
||||
# 如果使用 CloudMail 且配置了域名,使用真实域名
|
||||
if self.config.mail.enabled and self.config.mail.type == "cloudmail":
|
||||
domain = self.config.mail.cloudmail_domain
|
||||
if domain:
|
||||
return f"user_{random_part}@{domain}"
|
||||
|
||||
# 默认使用 example.com(用户应该替换为实际域名)
|
||||
logger.warning(
|
||||
f"Using example.com domain. Please configure MAIL_CLOUDMAIL_DOMAIN "
|
||||
f"or replace with your actual domain."
|
||||
)
|
||||
logger.warning("Using example.com domain. Configure MAIL_CLOUDMAIL_DOMAIN.")
|
||||
return f"user_{random_part}@example.com"
|
||||
|
||||
def _generate_name(self) -> str:
|
||||
"""
|
||||
生成随机姓名
|
||||
|
||||
返回:
|
||||
姓名字符串
|
||||
"""
|
||||
first_names = [
|
||||
"James", "John", "Robert", "Michael", "William",
|
||||
"David", "Richard", "Joseph", "Thomas", "Charles",
|
||||
"Mary", "Patricia", "Jennifer", "Linda", "Elizabeth",
|
||||
"Barbara", "Susan", "Jessica", "Sarah", "Karen"
|
||||
]
|
||||
|
||||
last_names = [
|
||||
"Smith", "Johnson", "Williams", "Brown", "Jones",
|
||||
"Garcia", "Miller", "Davis", "Rodriguez", "Martinez",
|
||||
"Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson",
|
||||
"Thomas", "Taylor", "Moore", "Jackson", "Martin"
|
||||
]
|
||||
|
||||
first = random.choice(first_names)
|
||||
last = random.choice(last_names)
|
||||
|
||||
return f"{first} {last}"
|
||||
"""生成随机姓名"""
|
||||
first_names = ["James", "John", "Robert", "Michael", "William", "David", "Mary", "Patricia", "Jennifer", "Linda"]
|
||||
last_names = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Wilson", "Anderson"]
|
||||
return f"{random.choice(first_names)} {random.choice(last_names)}"
|
||||
|
||||
def _generate_birthdate(self) -> str:
|
||||
"""
|
||||
生成随机生日(1980-2002 年,确保满 18 岁)
|
||||
|
||||
返回:
|
||||
日期字符串,格式: YYYY-MM-DD
|
||||
"""
|
||||
"""生成随机生日 (1980-2002)"""
|
||||
year = random.randint(1980, 2002)
|
||||
month = random.randint(1, 12)
|
||||
|
||||
# 避免 2 月 29/30/31 日等无效日期
|
||||
if month == 2:
|
||||
day = random.randint(1, 28)
|
||||
elif month in [4, 6, 9, 11]:
|
||||
day = random.randint(1, 30)
|
||||
else:
|
||||
day = random.randint(1, 31)
|
||||
|
||||
day = random.randint(1, 28) if month == 2 else random.randint(1, 30) if month in [4, 6, 9, 11] else random.randint(1, 31)
|
||||
return f"{year}-{month:02d}-{day:02d}"
|
||||
|
||||
|
||||
# 导出主要接口
|
||||
__all__ = ["RegisterFlow"]
|
||||
|
||||
225
core/sentinel.py
225
core/sentinel.py
@@ -1,225 +0,0 @@
|
||||
"""
|
||||
Sentinel 反爬机制处理器
|
||||
|
||||
Sentinel 是 OpenAI 用于防止自动化注册的安全机制,包括:
|
||||
- Proof of Work (PoW) 挑战
|
||||
- 设备指纹验证
|
||||
- 行为分析
|
||||
|
||||
✅ 已集成完整的 Sentinel 解决方案(使用 reference/ 下的代码)
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from utils.logger import logger
|
||||
from utils.fingerprint import BrowserFingerprint
|
||||
|
||||
|
||||
class SentinelHandler:
|
||||
"""
|
||||
Sentinel 反爬机制处理器
|
||||
|
||||
✅ 已集成用户的 Sentinel 解决方案
|
||||
使用 reference/ 目录下的完整实现
|
||||
"""
|
||||
|
||||
# Sentinel API 端点(从开发文档提取)
|
||||
SENTINEL_API = "https://chatgpt.com/_next/static/chunks/sentinel.js"
|
||||
SENTINEL_TOKEN_ENDPOINT = "https://api.openai.com/sentinel/token"
|
||||
|
||||
def __init__(self, session):
|
||||
"""
|
||||
初始化 Sentinel 处理器
|
||||
|
||||
参数:
|
||||
session: OAISession 实例(需要使用其 Cookie 和代理)
|
||||
"""
|
||||
from core.session import OAISession
|
||||
self.session: OAISession = session
|
||||
self.oai_did = session.oai_did
|
||||
|
||||
# 初始化浏览器指纹
|
||||
self.fingerprint = BrowserFingerprint(session_id=self.oai_did)
|
||||
|
||||
# 延迟导入 SentinelSolver(避免循环导入)
|
||||
self._solver = None
|
||||
|
||||
logger.info("SentinelHandler initialized")
|
||||
|
||||
def _get_solver(self):
|
||||
"""延迟初始化 Sentinel 求解器"""
|
||||
if self._solver is None:
|
||||
try:
|
||||
from reference.sentinel_solver import SentinelSolver
|
||||
self._solver = SentinelSolver(self.fingerprint)
|
||||
logger.debug("SentinelSolver initialized successfully")
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import SentinelSolver: {e}")
|
||||
raise ImportError(
|
||||
"Sentinel solver not found. Please check reference/ directory."
|
||||
) from e
|
||||
return self._solver
|
||||
|
||||
async def get_token(
|
||||
self,
|
||||
flow: str = "username_password_create",
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取 Sentinel Token
|
||||
|
||||
✅ 已实现 - 使用 reference/ 下的完整解决方案
|
||||
|
||||
参数:
|
||||
flow: 注册流程类型,常见值:
|
||||
- "username_password_create" (注册流程)
|
||||
- "username_password_create__auto" (自动注册)
|
||||
**kwargs: 其他可能需要的参数
|
||||
|
||||
返回:
|
||||
Sentinel Token 字典 {"p": "...", "t": "...", "c": "...", "id": "...", "flow": "..."}
|
||||
|
||||
实现说明:
|
||||
使用 reference/sentinel_solver.py 生成 requirements token
|
||||
返回完整的 JSON 对象(用于 Header)
|
||||
"""
|
||||
logger.info(f"Generating Sentinel token for flow='{flow}'")
|
||||
|
||||
try:
|
||||
# 获取求解器
|
||||
solver = self._get_solver()
|
||||
|
||||
# 生成 requirements token
|
||||
token_dict = solver.generate_requirements_token()
|
||||
|
||||
# 构建完整 token
|
||||
# 格式: {"p": "gAAAAAC...", "t": "...", "c": "...", "id": "uuid", "flow": "..."}
|
||||
token_dict["flow"] = flow
|
||||
token_dict["id"] = self.oai_did
|
||||
|
||||
# 验证必需字段
|
||||
if "p" not in token_dict or not token_dict["p"]:
|
||||
raise ValueError("Generated token missing 'p' field")
|
||||
|
||||
logger.success(f"Sentinel token generated: {str(token_dict)[:50]}...")
|
||||
return token_dict
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"Sentinel solver not available: {e}")
|
||||
raise NotImplementedError(
|
||||
"❌ Sentinel solver import failed.\n\n"
|
||||
"Please ensure:\n"
|
||||
"1. reference/ directory exists with sentinel_solver.py\n"
|
||||
"2. sdk/sdk.js file exists\n"
|
||||
"3. Node.js is installed and available in PATH\n\n"
|
||||
f"Error: {e}"
|
||||
) from e
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to generate Sentinel token: {e}")
|
||||
raise RuntimeError(f"Sentinel token generation failed: {e}") from e
|
||||
|
||||
async def solve_enforcement(
|
||||
self,
|
||||
enforcement_config: Dict[str, Any]
|
||||
) -> str:
|
||||
"""
|
||||
解决完整的 enforcement 挑战(PoW + Turnstile)
|
||||
|
||||
✅ 已实现 - 使用 reference/sentinel_solver.py
|
||||
|
||||
参数:
|
||||
enforcement_config: 服务器返回的挑战配置
|
||||
{
|
||||
'proofofwork': {
|
||||
'seed': '...',
|
||||
'difficulty': '0003a',
|
||||
'token': '...', # cached token
|
||||
'turnstile': {
|
||||
'dx': '...' # VM bytecode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
返回:
|
||||
完整的 Sentinel token (JSON string)
|
||||
"""
|
||||
logger.info("Solving enforcement challenge...")
|
||||
|
||||
try:
|
||||
solver = self._get_solver()
|
||||
token_json = solver.solve_enforcement(enforcement_config)
|
||||
|
||||
logger.success(f"Enforcement solved: {token_json[:50]}...")
|
||||
return token_json
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to solve enforcement: {e}")
|
||||
raise RuntimeError(f"Enforcement solving failed: {e}") from e
|
||||
|
||||
def _build_payload(self, flow: str) -> Dict[str, Any]:
|
||||
"""
|
||||
构建 Sentinel 请求 Payload
|
||||
|
||||
参考开发文档中的请求格式:
|
||||
{
|
||||
"p": "gAAAAAB...", # Proof of Work 答案
|
||||
"id": "a1b2c3d4-...", # oai-did
|
||||
"flow": "username_password_create__auto"
|
||||
}
|
||||
|
||||
参数:
|
||||
flow: 注册流程类型
|
||||
|
||||
返回:
|
||||
Payload 字典
|
||||
"""
|
||||
return {
|
||||
"p": "gAAAAAB_PLACEHOLDER_POW_ANSWER",
|
||||
"id": self.oai_did,
|
||||
"flow": flow
|
||||
}
|
||||
|
||||
async def verify_token(self, token: str) -> bool:
|
||||
"""
|
||||
验证 Sentinel Token 是否有效(可选功能)
|
||||
|
||||
参数:
|
||||
token: 待验证的 Sentinel Token
|
||||
|
||||
返回:
|
||||
True 如果有效,否则 False
|
||||
"""
|
||||
if not token or token == "placeholder_token":
|
||||
logger.warning("Received placeholder token, validation skipped")
|
||||
return False
|
||||
|
||||
# Token 基本格式检查
|
||||
if not token.startswith("gAAAAA"):
|
||||
logger.warning(f"Invalid token format: {token[:20]}...")
|
||||
return False
|
||||
|
||||
logger.info(f"Token validation: {token[:20]}... (length={len(token)})")
|
||||
return True
|
||||
|
||||
def get_sentinel_script(self) -> Optional[str]:
|
||||
"""
|
||||
获取 Sentinel JavaScript 代码(可选,用于分析)
|
||||
|
||||
返回:
|
||||
Sentinel JS 代码内容,失败则返回 None
|
||||
"""
|
||||
try:
|
||||
response = self.session.get(self.SENTINEL_API)
|
||||
if response.status_code == 200:
|
||||
logger.info(f"Sentinel script fetched: {len(response.text)} bytes")
|
||||
return response.text
|
||||
else:
|
||||
logger.error(f"Failed to fetch Sentinel script: {response.status_code}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching Sentinel script: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# 导出主要接口
|
||||
__all__ = ["SentinelHandler"]
|
||||
7
core/sentinel/__init__.py
Normal file
7
core/sentinel/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Sentinel 反爬机制解决方案
|
||||
"""
|
||||
from .solver import SentinelSolver
|
||||
from .js_executor import JSExecutor
|
||||
|
||||
__all__ = ["SentinelSolver", "JSExecutor"]
|
||||
257
core/sentinel/js_executor.py
Normal file
257
core/sentinel/js_executor.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""JavaScript 执行引擎封装
|
||||
|
||||
说明:
|
||||
- OpenAI 的 `assets/sdk.js` 是浏览器脚本,包含多段 anti-debug 代码与 DOM 初始化逻辑。
|
||||
- 直接用 PyExecJS/ExecJS 在 Node 环境执行时,常见表现是「compile 成功但 call 卡死」。
|
||||
- 这里改为通过 `node -` 运行一段自包含脚本:先做轻量净化 + browser shim,再执行目标函数并强制输出结果。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from config import load_config
|
||||
|
||||
|
||||
class JSExecutor:
|
||||
"""通过 Node.js 执行 Sentinel SDK 内部逻辑(支持 async Turnstile VM)"""
|
||||
|
||||
def __init__(self):
|
||||
# 加载配置
|
||||
config = load_config()
|
||||
self.debug = config.sentinel_debug
|
||||
|
||||
# 解析 SDK 路径
|
||||
sdk_path = Path(config.sentinel_sdk_path)
|
||||
if not sdk_path.is_absolute():
|
||||
# 相对路径:相对于项目根目录
|
||||
project_root = Path(sys.argv[0]).parent.resolve()
|
||||
sdk_path = project_root / sdk_path
|
||||
|
||||
self._sdk_path = sdk_path
|
||||
self._sdk_code: str = ""
|
||||
self._load_sdk()
|
||||
|
||||
def _load_sdk(self) -> None:
|
||||
if not self._sdk_path.exists():
|
||||
raise FileNotFoundError(f"SDK not found at {self._sdk_path}")
|
||||
|
||||
sdk_code = self._sdk_path.read_text(encoding="utf-8")
|
||||
sdk_code = self._sanitize_sdk(sdk_code)
|
||||
sdk_code = self._inject_internal_exports(sdk_code)
|
||||
|
||||
self._sdk_code = sdk_code
|
||||
|
||||
if self.debug:
|
||||
print("[JSExecutor] SDK loaded successfully (sanitized)")
|
||||
|
||||
def _sanitize_sdk(self, sdk_code: str) -> str:
|
||||
"""移除会在 Node 环境中导致卡死/超慢的 anti-debug 片段。"""
|
||||
# 1) 删除少量已知的顶层 anti-debug 直接调用(独占一行)
|
||||
sdk_code = re.sub(r"(?m)^\s*[rugkU]\(\);\s*$", "", sdk_code)
|
||||
sdk_code = re.sub(r"(?m)^\s*o\(\);\s*$", "", sdk_code)
|
||||
|
||||
# 2) 删除 `Pt(),` 这种逗号表达式里的 anti-debug 调用(避免语法破坏)
|
||||
sdk_code = re.sub(r"\bPt\(\),\s*", "", sdk_code)
|
||||
sdk_code = re.sub(r"\bPt\(\);\s*", "", sdk_code)
|
||||
|
||||
# 3) 删除 class 字段初始化里的 anti-debug 调用:`return n(), "" + Math.random();`
|
||||
sdk_code = re.sub(
|
||||
r'return\s+n\(\),\s*""\s*\+\s*Math\.random\(\)\s*;',
|
||||
'return "" + Math.random();',
|
||||
sdk_code,
|
||||
)
|
||||
|
||||
# 4) 删除类似 `if ((e(), cond))` 的逗号 anti-debug 调用(保留 cond)
|
||||
# 仅处理极短标识符,避免误伤正常逻辑;保留 Turnstile VM 的 `vt()`。
|
||||
def _strip_comma_call(match: re.Match[str]) -> str:
|
||||
fn = match.group(1)
|
||||
if fn == "vt":
|
||||
return match.group(0)
|
||||
return "("
|
||||
|
||||
sdk_code = re.sub(
|
||||
r"\(\s*([A-Za-z_$][A-Za-z0-9_$]{0,2})\(\)\s*,",
|
||||
_strip_comma_call,
|
||||
sdk_code,
|
||||
)
|
||||
return sdk_code
|
||||
|
||||
def _inject_internal_exports(self, sdk_code: str) -> str:
|
||||
"""把 SDK 内部对象导出到 `SentinelSDK` 上,便于在外部调用。"""
|
||||
# SDK 末尾一般是:
|
||||
# (t.init = un),
|
||||
# (t.token = an),
|
||||
# t
|
||||
# );
|
||||
pattern = re.compile(
|
||||
r"\(\s*t\.init\s*=\s*un\s*\)\s*,\s*\(\s*t\.token\s*=\s*an\s*\)\s*,\s*t\s*\)",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
replacement = (
|
||||
"(t.init = un),"
|
||||
"(t.token = an),"
|
||||
"(t.__O = O),"
|
||||
"(t.__P = P),"
|
||||
"(t.__bt = bt),"
|
||||
"(t.__kt = kt),"
|
||||
"(t.__Kt = Kt),"
|
||||
"t)"
|
||||
)
|
||||
|
||||
new_code, n = pattern.subn(replacement, sdk_code, count=1)
|
||||
if n != 1:
|
||||
raise RuntimeError("Failed to patch SDK exports; SDK format may have changed.")
|
||||
return new_code
|
||||
|
||||
def _node_script(self, payload: Dict[str, Any], entry: str) -> str:
|
||||
payload_json = json.dumps(payload, ensure_ascii=False)
|
||||
|
||||
shim = r"""
|
||||
// --- minimal browser shims for Node ---
|
||||
if (typeof globalThis.window !== "object") globalThis.window = globalThis;
|
||||
if (!window.top) window.top = window;
|
||||
if (!window.location) window.location = { href: "https://auth.openai.com/create-account/password", search: "", pathname: "/create-account/password", origin: "https://auth.openai.com" };
|
||||
if (!window.addEventListener) window.addEventListener = function(){};
|
||||
if (!window.removeEventListener) window.removeEventListener = function(){};
|
||||
if (!window.postMessage) window.postMessage = function(){};
|
||||
if (!window.__sentinel_token_pending) window.__sentinel_token_pending = [];
|
||||
if (!window.__sentinel_init_pending) window.__sentinel_init_pending = [];
|
||||
|
||||
if (typeof globalThis.document !== "object") globalThis.document = {};
|
||||
if (!document.scripts) document.scripts = [];
|
||||
if (!document.cookie) document.cookie = "";
|
||||
if (!document.documentElement) document.documentElement = { getAttribute: () => null };
|
||||
if (!document.currentScript) document.currentScript = null;
|
||||
if (!document.body) document.body = { appendChild: function(){}, getAttribute: () => null };
|
||||
if (!document.createElement) document.createElement = function(tag){
|
||||
return {
|
||||
tagName: String(tag||"").toUpperCase(),
|
||||
style: {},
|
||||
setAttribute: function(){},
|
||||
getAttribute: function(){ return null; },
|
||||
addEventListener: function(){},
|
||||
removeEventListener: function(){},
|
||||
src: "",
|
||||
contentWindow: { postMessage: function(){}, addEventListener: function(){}, removeEventListener: function(){} },
|
||||
};
|
||||
};
|
||||
|
||||
if (typeof globalThis.navigator !== "object") globalThis.navigator = { userAgent: "ua", language: "en-US", languages: ["en-US","en"], hardwareConcurrency: 8 };
|
||||
if (typeof globalThis.screen !== "object") globalThis.screen = { width: 1920, height: 1080 };
|
||||
if (typeof globalThis.btoa !== "function") globalThis.btoa = (str) => Buffer.from(str, "binary").toString("base64");
|
||||
if (typeof globalThis.atob !== "function") globalThis.atob = (b64) => Buffer.from(b64, "base64").toString("binary");
|
||||
window.btoa = globalThis.btoa;
|
||||
window.atob = globalThis.atob;
|
||||
"""
|
||||
|
||||
wrapper = f"""
|
||||
const __payload = {payload_json};
|
||||
|
||||
function __makeSolver(configArray) {{
|
||||
const solver = new SentinelSDK.__O();
|
||||
solver.sid = configArray?.[14];
|
||||
// 强制使用 Python 传入的 configArray,避免依赖真实浏览器对象
|
||||
solver.getConfig = () => configArray;
|
||||
return solver;
|
||||
}}
|
||||
|
||||
async function __entry() {{
|
||||
{entry}
|
||||
}}
|
||||
|
||||
(async () => {{
|
||||
try {{
|
||||
const result = await __entry();
|
||||
process.stdout.write(JSON.stringify({{ ok: true, result }}), () => process.exit(0));
|
||||
}} catch (err) {{
|
||||
const msg = (err && (err.stack || err.message)) ? (err.stack || err.message) : String(err);
|
||||
process.stdout.write(JSON.stringify({{ ok: false, error: msg }}), () => process.exit(1));
|
||||
}}
|
||||
}})();
|
||||
"""
|
||||
return "\n".join([shim, self._sdk_code, wrapper])
|
||||
|
||||
def _run_node(self, payload: Dict[str, Any], entry: str, timeout_s: int = 30) -> Any:
|
||||
script = self._node_script(payload, entry)
|
||||
|
||||
if self.debug:
|
||||
print("[JSExecutor] Running Node worker...")
|
||||
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["node", "-"],
|
||||
input=script,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=timeout_s,
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
raise RuntimeError("Node.js not found on PATH (required for Sentinel SDK execution).") from e
|
||||
except subprocess.TimeoutExpired as e:
|
||||
raise TimeoutError(f"Node worker timed out after {timeout_s}s") from e
|
||||
|
||||
stdout = (proc.stdout or "").strip()
|
||||
if not stdout:
|
||||
raise RuntimeError(f"Node worker produced no output (stderr={proc.stderr!r})")
|
||||
|
||||
try:
|
||||
obj = json.loads(stdout)
|
||||
except json.JSONDecodeError as e:
|
||||
raise RuntimeError(f"Node worker returned non-JSON output: {stdout[:200]!r}") from e
|
||||
|
||||
if not obj.get("ok"):
|
||||
raise RuntimeError(obj.get("error", "Unknown JS error"))
|
||||
|
||||
return obj.get("result")
|
||||
|
||||
def solve_pow(self, seed: str, difficulty: str, config_array: list) -> str:
|
||||
if self.debug:
|
||||
print(f"[JSExecutor] Solving PoW: seed={seed[:10]}..., difficulty={difficulty}")
|
||||
|
||||
result = self._run_node(
|
||||
{"seed": seed, "difficulty": difficulty, "configArray": config_array},
|
||||
entry="return __makeSolver(__payload.configArray)._generateAnswerSync(__payload.seed, __payload.difficulty);",
|
||||
timeout_s=60,
|
||||
)
|
||||
|
||||
if self.debug and isinstance(result, str):
|
||||
print(f"[JSExecutor] PoW solved: {result[:50]}...")
|
||||
|
||||
return result
|
||||
|
||||
def generate_requirements(self, seed: str, config_array: list) -> str:
|
||||
result = self._run_node(
|
||||
{"seed": seed, "configArray": config_array},
|
||||
entry=(
|
||||
"const solver = __makeSolver(__payload.configArray);\n"
|
||||
"solver.requirementsSeed = __payload.seed;\n"
|
||||
"return solver._generateRequirementsTokenAnswerBlocking();"
|
||||
),
|
||||
timeout_s=30,
|
||||
)
|
||||
return result
|
||||
|
||||
def execute_turnstile(self, dx_bytecode: str, xor_key: str) -> str:
|
||||
if self.debug:
|
||||
print("[JSExecutor] Executing Turnstile VM...")
|
||||
|
||||
result = self._run_node(
|
||||
{"dx": dx_bytecode, "xorKey": xor_key},
|
||||
entry=(
|
||||
"SentinelSDK.__kt(__payload.xorKey);\n"
|
||||
"return await SentinelSDK.__bt(__payload.dx);"
|
||||
),
|
||||
timeout_s=30,
|
||||
)
|
||||
|
||||
if self.debug and isinstance(result, str):
|
||||
print(f"[JSExecutor] Turnstile result: {result[:50]}...")
|
||||
|
||||
return result
|
||||
115
core/sentinel/solver.py
Normal file
115
core/sentinel/solver.py
Normal file
@@ -0,0 +1,115 @@
|
||||
# modules/sentinel_solver.py
|
||||
"""Sentinel 挑战求解器"""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from typing import Dict, Optional
|
||||
|
||||
from core.sentinel.js_executor import JSExecutor
|
||||
from utils.fingerprint import BrowserFingerprint
|
||||
from config import load_config
|
||||
|
||||
|
||||
class SentinelSolver:
|
||||
"""协调指纹生成和 JS 执行,生成完整的 Sentinel tokens"""
|
||||
|
||||
def __init__(self, fingerprint: BrowserFingerprint):
|
||||
self.fingerprint = fingerprint
|
||||
self.js_executor = JSExecutor()
|
||||
|
||||
# 加载配置
|
||||
config = load_config()
|
||||
self.debug = config.sentinel_debug
|
||||
|
||||
def generate_requirements_token(self) -> Dict[str, str]:
|
||||
"""
|
||||
生成 requirements token(初始化时需要)
|
||||
|
||||
Returns:
|
||||
{'p': 'gAAAAAC...', 'id': 'uuid'}
|
||||
"""
|
||||
if self.debug:
|
||||
print("[Solver] Generating requirements token...")
|
||||
|
||||
# 生成随机 seed
|
||||
req_seed = str(uuid.uuid4())
|
||||
|
||||
# 获取指纹配置
|
||||
config_array = self.fingerprint.get_config_array()
|
||||
|
||||
# 调用 JS 求解
|
||||
answer = self.js_executor.generate_requirements(req_seed, config_array)
|
||||
|
||||
token = {
|
||||
'p': f'gAAAAAC{answer}',
|
||||
'id': self.fingerprint.session_id,
|
||||
}
|
||||
|
||||
if self.debug:
|
||||
print(f"[Solver] Requirements token: {token['p'][:30]}...")
|
||||
|
||||
return token
|
||||
|
||||
def solve_enforcement(self, enforcement_config: Dict) -> str:
|
||||
"""
|
||||
解决完整的 enforcement 挑战(PoW + Turnstile)
|
||||
|
||||
Args:
|
||||
enforcement_config: 服务器返回的挑战配置
|
||||
{
|
||||
'proofofwork': {
|
||||
'seed': '...',
|
||||
'difficulty': '0003a',
|
||||
'token': '...', # cached token
|
||||
'turnstile': {
|
||||
'dx': '...' # VM bytecode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Returns:
|
||||
完整的 Sentinel token (JSON string)
|
||||
"""
|
||||
if self.debug:
|
||||
print("[Solver] Solving enforcement challenge...")
|
||||
|
||||
pow_data = enforcement_config.get('proofofwork', {})
|
||||
|
||||
# 1. 解决 PoW
|
||||
seed = pow_data['seed']
|
||||
difficulty = pow_data['difficulty']
|
||||
|
||||
config_array = self.fingerprint.get_config_array()
|
||||
pow_answer = self.js_executor.solve_pow(seed, difficulty, config_array)
|
||||
|
||||
# 2. 执行 Turnstile(如果有)
|
||||
turnstile_result = None
|
||||
turnstile_data = pow_data.get('turnstile')
|
||||
|
||||
if turnstile_data and turnstile_data.get('dx'):
|
||||
dx_bytecode = turnstile_data['dx']
|
||||
xor_key = self.fingerprint.session_id # 通常用 session ID 作为密钥
|
||||
|
||||
turnstile_result = self.js_executor.execute_turnstile(dx_bytecode, xor_key)
|
||||
|
||||
# 3. 构建最终 token
|
||||
sentinel_token = {
|
||||
# enforcement token 前缀为 gAAAAAB(requirements 为 gAAAAAC)
|
||||
'p': f'gAAAAAB{pow_answer}',
|
||||
'id': self.fingerprint.session_id,
|
||||
'flow': 'username_password_create',
|
||||
}
|
||||
|
||||
# 添加可选字段
|
||||
if turnstile_result:
|
||||
sentinel_token['t'] = turnstile_result
|
||||
|
||||
if pow_data.get('token'):
|
||||
sentinel_token['c'] = pow_data['token']
|
||||
|
||||
token_json = json.dumps(sentinel_token)
|
||||
|
||||
if self.debug:
|
||||
print(f"[Solver] Sentinel token generated: {token_json[:80]}...")
|
||||
|
||||
return token_json
|
||||
95
core/sentinel_handler.py
Normal file
95
core/sentinel_handler.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Sentinel 反爬机制处理器"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from utils.logger import logger
|
||||
from utils.fingerprint import BrowserFingerprint
|
||||
|
||||
|
||||
class SentinelHandler:
|
||||
"""Sentinel 反爬机制处理器"""
|
||||
|
||||
SENTINEL_API = "https://chatgpt.com/_next/static/chunks/sentinel.js"
|
||||
SENTINEL_TOKEN_ENDPOINT = "https://api.openai.com/sentinel/token"
|
||||
|
||||
def __init__(self, session):
|
||||
from core.session import OAISession
|
||||
self.session: OAISession = session
|
||||
self.oai_did = session.oai_did
|
||||
self.fingerprint = BrowserFingerprint(session_id=self.oai_did)
|
||||
self._solver = None
|
||||
logger.info("SentinelHandler initialized")
|
||||
|
||||
def _get_solver(self):
|
||||
"""延迟初始化 Sentinel 求解器"""
|
||||
if self._solver is None:
|
||||
try:
|
||||
from core.sentinel import SentinelSolver
|
||||
self._solver = SentinelSolver(self.fingerprint)
|
||||
logger.debug("SentinelSolver initialized")
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import SentinelSolver: {e}")
|
||||
raise ImportError("Sentinel solver not found. Check core/sentinel/ directory.") from e
|
||||
return self._solver
|
||||
|
||||
async def get_token(self, flow: str = "username_password_create", **kwargs) -> Dict[str, Any]:
|
||||
"""获取 Sentinel Token"""
|
||||
logger.info(f"Generating Sentinel token for flow='{flow}'")
|
||||
|
||||
try:
|
||||
solver = self._get_solver()
|
||||
token_dict = solver.generate_requirements_token()
|
||||
token_dict["flow"] = flow
|
||||
token_dict["id"] = self.oai_did
|
||||
|
||||
if "p" not in token_dict or not token_dict["p"]:
|
||||
raise ValueError("Generated token missing 'p' field")
|
||||
|
||||
logger.success(f"Sentinel token generated: {str(token_dict)[:50]}...")
|
||||
return token_dict
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"Sentinel solver not available: {e}")
|
||||
raise NotImplementedError(f"Sentinel solver import failed: {e}") from e
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to generate Sentinel token: {e}")
|
||||
raise RuntimeError(f"Sentinel token generation failed: {e}") from e
|
||||
|
||||
async def solve_enforcement(self, enforcement_config: Dict[str, Any]) -> str:
|
||||
"""解决 enforcement 挑战(PoW + Turnstile)"""
|
||||
logger.info("Solving enforcement challenge...")
|
||||
|
||||
try:
|
||||
solver = self._get_solver()
|
||||
token_json = solver.solve_enforcement(enforcement_config)
|
||||
logger.success(f"Enforcement solved: {token_json[:50]}...")
|
||||
return token_json
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to solve enforcement: {e}")
|
||||
raise RuntimeError(f"Enforcement solving failed: {e}") from e
|
||||
|
||||
async def verify_token(self, token: str) -> bool:
|
||||
"""验证 Sentinel Token 格式"""
|
||||
if not token or token == "placeholder_token":
|
||||
return False
|
||||
if not token.startswith("gAAAAA"):
|
||||
logger.warning(f"Invalid token format: {token[:20]}...")
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_sentinel_script(self) -> Optional[str]:
|
||||
"""获取 Sentinel JavaScript 代码"""
|
||||
try:
|
||||
response = self.session.get(self.SENTINEL_API)
|
||||
if response.status_code == 200:
|
||||
logger.info(f"Sentinel script fetched: {len(response.text)} bytes")
|
||||
return response.text
|
||||
logger.error(f"Failed to fetch Sentinel script: {response.status_code}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching Sentinel script: {e}")
|
||||
return None
|
||||
|
||||
|
||||
__all__ = ["SentinelHandler"]
|
||||
Reference in New Issue
Block a user