This commit is contained in:
dela
2026-01-26 15:04:02 +08:00
commit 4813449f9c
31 changed files with 8439 additions and 0 deletions

7
core/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
"""
OpenAI 账号注册系统 - 核心模块
包含会话管理、流程编排、Sentinel 处理等核心功能
"""
__version__ = "0.1.0"

274
core/challenge.py Normal file
View File

@@ -0,0 +1,274 @@
"""
Cloudflare Turnstile 验证码解决器
Cloudflare Turnstile 是一种新型验证码系统,用于替代传统的 reCAPTCHA
当触发时会返回 403 状态码并显示 "Just a moment" 页面
⚠️ 本模块提供预留接口,用户根据需要配置解决方案
"""
from typing import Optional, Dict, Any
from utils.logger import logger
class CloudflareSolver:
"""
Cloudflare Turnstile 验证码解决器
⚠️ 预留接口 - 用户根据实际情况选择是否实现
可能的解决方案:
1. 使用高质量住宅代理(推荐,成本较低)
2. 集成打码平台(如 2captcha, capsolver
3. 使用浏览器自动化 + undetected-chromedriver
4. 等待一段时间后重试(部分情况有效)
"""
# Turnstile 相关常量
TURNSTILE_SITE_KEY = "0x4AAAAAAADnPIDROrmt1Wwj" # OpenAI 的 Turnstile site key需要从实际页面提取
@staticmethod
def detect_challenge(response) -> bool:
"""
检测响应是否为 Cloudflare Turnstile 挑战
参数:
response: HTTP 响应对象(来自 requests 或 curl_cffi
返回:
True 如果检测到 Cloudflare 挑战,否则 False
检测特征:
- 状态码 403
- 响应体包含 "Just a moment", "Checking your browser" 等文本
- 包含 Cloudflare 相关 JavaScript
"""
if response.status_code != 403:
return False
body = response.text.lower()
cloudflare_keywords = [
"just a moment",
"checking your browser",
"cloudflare",
"cf-challenge",
"turnstile",
"ray id"
]
detected = any(keyword in body for keyword in cloudflare_keywords)
if detected:
logger.warning("Cloudflare Turnstile challenge detected")
# 尝试提取 Ray ID用于调试
ray_id = CloudflareSolver._extract_ray_id(response.text)
if ray_id:
logger.info(f"Cloudflare Ray ID: {ray_id}")
return detected
@staticmethod
async def solve(session, target_url: str, **kwargs) -> Optional[str]:
"""
解决 Cloudflare Turnstile 挑战
⚠️ 预留接口 - 用户需要根据实际需求实现
参数:
session: OAISession 实例
target_url: 触发挑战的目标 URL
**kwargs: 其他可能需要的参数(如 site_key, action 等)
返回:
cf_clearance Cookie 值 或 Turnstile response token
抛出:
NotImplementedError: 用户需要实现此方法
集成示例:
```python
# 方案 1: 使用 2captcha 打码平台
from twocaptcha import TwoCaptcha
solver = TwoCaptcha('YOUR_API_KEY')
result = solver.turnstile(
sitekey=CloudflareSolver.TURNSTILE_SITE_KEY,
url=target_url
)
return result['code']
# 方案 2: 使用 capsolver
import capsolver
capsolver.api_key = "YOUR_API_KEY"
solution = capsolver.solve({
"type": "AntiTurnstileTaskProxyLess",
"websiteURL": target_url,
"websiteKey": CloudflareSolver.TURNSTILE_SITE_KEY,
})
return solution['token']
# 方案 3: 使用浏览器自动化
from selenium import webdriver
from undetected_chromedriver import Chrome
driver = Chrome()
driver.get(target_url)
# 等待 Cloudflare 自动通过...
cf_clearance = driver.get_cookie('cf_clearance')['value']
return cf_clearance
```
"""
logger.warning(
f"Cloudflare challenge detected at {target_url}, but solver not configured"
)
raise NotImplementedError(
"❌ Cloudflare solver not implemented.\n\n"
"This is OPTIONAL. Only implement if you encounter frequent 403 errors.\n\n"
"Recommended solutions:\n"
"1. Use high-quality residential proxies (easiest)\n"
"2. Integrate captcha solving service (2captcha, capsolver)\n"
"3. Use browser automation (undetected-chromedriver)\n"
"4. Retry with different proxy/IP\n\n"
f"Target URL: {target_url}\n"
f"Site Key: {CloudflareSolver.TURNSTILE_SITE_KEY}\n\n"
"Example implementation location: core/challenge.py -> solve()"
)
@staticmethod
def _extract_ray_id(html: str) -> Optional[str]:
"""
从 Cloudflare 错误页面提取 Ray ID用于调试
Ray ID 格式示例: 84a1b2c3d4e5f678-LAX
参数:
html: Cloudflare 错误页面的 HTML 内容
返回:
Ray ID 字符串,未找到则返回 None
"""
import re
match = re.search(r'Ray ID: ([a-f0-9-]+)', html, re.IGNORECASE)
if match:
return match.group(1)
# 尝试其他格式
match = re.search(r'ray id[:\s]+([a-f0-9-]+)', html, re.IGNORECASE)
if match:
return match.group(1)
return None
@staticmethod
def should_retry(response) -> bool:
"""
判断是否应该重试请求(针对 Cloudflare 挑战)
某些情况下,简单地等待几秒后重试即可通过
参数:
response: HTTP 响应对象
返回:
True 如果建议重试,否则 False
"""
if not CloudflareSolver.detect_challenge(response):
return False
# 如果是轻量级挑战JavaScript challenge重试可能有效
# 如果是 Turnstile 验证码,重试无效,需要解决验证码
body = response.text.lower()
# JavaScript challenge 特征(可以重试)
js_challenge_keywords = ["checking your browser", "please wait"]
has_js_challenge = any(kw in body for kw in js_challenge_keywords)
# Turnstile 验证码特征(需要解决,重试无效)
turnstile_keywords = ["turnstile", "cf-turnstile"]
has_turnstile = any(kw in body for kw in turnstile_keywords)
if has_js_challenge and not has_turnstile:
logger.info("Detected JavaScript challenge, retry may work")
return True
else:
logger.warning("Detected Turnstile captcha, retry unlikely to work")
return False
@staticmethod
def get_bypass_headers() -> Dict[str, str]:
"""
获取可能帮助绕过 Cloudflare 的额外 HTTP 头
这些 Header 可以提高通过率,但不保证 100% 有效
返回:
额外的 HTTP 头字典
"""
return {
"Cache-Control": "max-age=0",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-User": "?1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Priority": "u=0, i",
}
class CaptchaSolver:
"""
通用验证码解决器(预留接口)
支持多种验证码类型:
- Cloudflare Turnstile
- reCAPTCHA v2/v3
- hCaptcha
- 图片验证码
"""
def __init__(self, api_key: Optional[str] = None, provider: str = "2captcha"):
"""
初始化验证码解决器
参数:
api_key: 打码平台 API Key
provider: 打码平台名称 ("2captcha", "capsolver", "anticaptcha")
"""
self.api_key = api_key
self.provider = provider
if not api_key:
logger.warning("CaptchaSolver initialized without API key")
async def solve_turnstile(
self,
site_key: str,
page_url: str,
**kwargs
) -> Optional[str]:
"""
解决 Turnstile 验证码(预留接口)
参数:
site_key: Turnstile site key
page_url: 页面 URL
**kwargs: 其他参数
返回:
Turnstile response token
"""
if not self.api_key:
raise ValueError("API key not configured")
logger.info(f"Solving Turnstile captcha with {self.provider}...")
# TODO: 用户集成实际的打码平台 API
raise NotImplementedError(
f"Turnstile solver not implemented for provider: {self.provider}"
)
# 导出主要接口
__all__ = [
"CloudflareSolver",
"CaptchaSolver",
]

589
core/flow.py Normal file
View File

@@ -0,0 +1,589 @@
"""
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
"""
from typing import Dict, Any, Optional
import secrets
import random
import asyncio
import json
from core.session import (
OAISession,
CloudflareBlockError,
SessionInvalidError,
RateLimitError
)
from core.sentinel import SentinelHandler
from core.challenge import CloudflareSolver
from utils.mail_box import MailHandler
from utils.crypto import generate_random_password
from utils.logger import logger
class RegisterFlow:
"""
OpenAI 账号注册流程编排器
负责协调各个组件,按照正确的顺序执行注册流程
"""
# OpenAI 相关 URL
CHATGPT_HOME = "https://chatgpt.com/"
CHATGPT_PROVIDERS = "https://chatgpt.com/api/auth/providers"
CHATGPT_CSRF = "https://chatgpt.com/api/auth/csrf"
CHATGPT_SIGNIN = "https://chatgpt.com/api/auth/signin/openai"
AUTH_CREATE_ACCOUNT = "https://auth.openai.com/create-account/password"
AUTH_REGISTER = "https://auth.openai.com/api/accounts/user/register"
AUTH_SEND_OTP = "https://auth.openai.com/api/accounts/email-otp/send"
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: 密码(可选,不提供则自动生成)
"""
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.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})"
)
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确保邮箱账户存在
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")
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,
"password": self.password,
"oai_did": self.s.oai_did,
"status": "success",
"message": "Account registered successfully"
}
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."
}
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."
}
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."
}
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)."
}
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__}"
}
async def _step1_init_session(self):
"""
Step 1: 初始化会话
访问 ChatGPT 主页和 API providers 端点,建立基础会话
"""
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}")
logger.info(f"[{self.email}] ✓ Session initialized")
async def _step2_get_csrf_token(self):
"""
Step 2: 获取 CSRF Token
CSRF Token 用于后续的 OAuth 登录流程
"""
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")
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]}...")
async def _step3_oauth_signin(self):
"""
Step 3: OAuth 登录流程
启动 OAuth 流程,跳转到 auth.openai.com
确保获取所有必要的 session cookies
"""
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, # 🔥 传入邮箱
}
payload = {
"callbackUrl": "/",
"csrfToken": self.csrf_token,
"json": "true"
}
resp = self.s.post(
self.CHATGPT_SIGNIN,
params=signin_params, # ✅ 添加 URL 参数
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")
if not auth_url:
raise RuntimeError(f"OAuth URL not found in response: {data}")
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")
async def _step4_get_sentinel_token(self):
"""
Step 4: Sentinel 握手
获取 Sentinel Token 用于提交注册信息
✅ 已集成完整的 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]}...")
except (NotImplementedError, ImportError) as 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 中
"""
logger.info(f"[{self.email}] Step 5: Submitting registration")
# 请求 Bodyusername 和 password
payload = {
"username": self.email, # ✅ 改为 username
"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({
"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
)
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}")
logger.info(f"[{self.email}] ✓ Registration info submitted successfully")
async def _step6_send_email_otp(self):
"""
Step 6: 触发邮件验证
POST /api/accounts/email-otp/send
触发 OpenAI 发送 OTP 验证码到注册邮箱
⚠️ 此步骤最容易触发 Cloudflare 403
"""
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
}
)
# 检查 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 != 200:
raise RuntimeError(
f"Email OTP send failed: {resp.status_code} - {resp.text[:200]}"
)
logger.info(f"[{self.email}] ✓ OTP email sent successfully")
async def _step7_submit_otp(self):
"""
Step 7: 提交 OTP 验证码
等待邮件接收 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
)
logger.info(f"[{self.email}] ✓ OTP received: {self.otp}")
except NotImplementedError:
logger.warning(
f"[{self.email}] ⚠️ Mail handler not configured, cannot retrieve OTP"
)
# 重新抛出异常,让调用方知道需要手动输入 OTP
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}"
)
# 提交 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
}
)
if resp.status_code != 200:
error_msg = resp.text[:200]
logger.error(f" - OTP validation failed: {resp.status_code} - {error_msg}")
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")
async def _step8_complete_profile(self):
"""
Step 8: 完成用户信息
POST /api/accounts/create_account
提交姓名和生日,完成注册
"""
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
}
)
if resp.status_code != 200:
raise RuntimeError(
f"Profile completion failed: {resp.status_code} - {resp.text[:200]}"
)
logger.info(
f"[{self.email}] ✓ Profile completed: name={name}, birthdate={birthdate}"
)
# ========== 辅助方法 ==========
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."
)
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}"
def _generate_birthdate(self) -> str:
"""
生成随机生日1980-2002 年,确保满 18 岁)
返回:
日期字符串,格式: YYYY-MM-DD
"""
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)
return f"{year}-{month:02d}-{day:02d}"
# 导出主要接口
__all__ = ["RegisterFlow"]

225
core/sentinel.py Normal file
View File

@@ -0,0 +1,225 @@
"""
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"]

306
core/session.py Normal file
View File

@@ -0,0 +1,306 @@
"""
TLS 指纹伪装会话管理模块
核心功能:
- 使用 curl_cffi 模拟 Chrome 浏览器的 TLS 指纹
- 管理关键 Cookie (oai-did, __Secure-next-auth 系列)
- 统一的错误处理 (403 Cloudflare 拦截, 409 会话冲突)
- 代理支持
"""
from curl_cffi import requests
from typing import Optional, Dict, Any
from utils.crypto import generate_oai_did
from utils.logger import logger
class CloudflareBlockError(Exception):
"""Cloudflare 拦截异常403 + Turnstile 挑战)"""
pass
class SessionInvalidError(Exception):
"""会话失效异常409 Conflict - CSRF Token 断链)"""
pass
class RateLimitError(Exception):
"""速率限制异常429 Too Many Requests"""
pass
class OAISession:
"""
OpenAI 会话管理器
使用 curl_cffi 库模拟真实 Chrome 浏览器的 TLS 指纹,绕过 OpenAI 的检测
关键特性:
- TLS 指纹伪装 (impersonate="chrome124")
- oai-did Cookie 管理(设备指纹)
- 自动错误检测和异常抛出
- 代理支持HTTP/HTTPS/SOCKS5
"""
# OpenAI 相关域名
CHATGPT_DOMAIN = "chatgpt.com"
AUTH_DOMAIN = "auth.openai.com"
API_DOMAIN = "api.openai.com"
def __init__(self, proxy: Optional[str] = None, impersonate: str = "chrome124"):
"""
初始化会话
参数:
proxy: 代理地址,支持格式:
- HTTP: "http://user:pass@ip:port"
- HTTPS: "https://user:pass@ip:port"
- SOCKS5: "socks5://user:pass@ip:port"
impersonate: 模拟的浏览器版本,可选值:
- "chrome110", "chrome120", "chrome124" (推荐)
- 需要根据实际情况测试最佳版本
"""
# 创建 curl_cffi 会话(核心!)
self.client = requests.Session(
impersonate=impersonate,
timeout=30
)
# 配置代理
if proxy:
self.client.proxies = {
"http": proxy,
"https": proxy
}
logger.info(f"Session using proxy: {self._mask_proxy(proxy)}")
else:
logger.info("Session initialized without proxy")
# 设置请求头(从真实浏览器抓包)
self._setup_headers()
# 生成并设置 oai-did Cookie关键设备指纹
self.oai_did = generate_oai_did()
self.client.cookies.set(
"oai-did",
self.oai_did,
domain=f".{self.CHATGPT_DOMAIN}"
)
logger.info(f"Session initialized with oai-did: {self.oai_did}")
def _setup_headers(self):
"""
设置 HTTP 请求头,模拟真实 Chrome 浏览器
这些 Header 从开发文档的抓包日志中提取
"""
self.client.headers.update({
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Sec-Ch-Ua": '"Chromium";v="143", "Not.A/Brand";v="24"',
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": '"Linux"',
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"DNT": "1",
"Connection": "keep-alive",
})
def get(self, url: str, **kwargs) -> requests.Response:
"""
发送 GET 请求
参数:
url: 目标 URL
**kwargs: 传递给 requests.get 的其他参数
返回:
Response 对象
抛出:
CloudflareBlockError: 遇到 Cloudflare 拦截
SessionInvalidError: 会话失效409
RateLimitError: 速率限制429
"""
try:
response = self.client.get(url, **kwargs)
return self._handle_response(response, url)
except Exception as e:
logger.error(f"GET request failed: {url} - {e}")
raise
def post(self, url: str, params=None, **kwargs) -> requests.Response:
"""
发送 POST 请求
参数:
url: 目标 URL
params: URL 查询参数(可选)
**kwargs: 传递给 requests.post 的其他参数
返回:
Response 对象
抛出:
CloudflareBlockError: 遇到 Cloudflare 拦截
SessionInvalidError: 会话失效409
RateLimitError: 速率限制429
"""
try:
response = self.client.post(url, params=params, **kwargs)
return self._handle_response(response, url)
except Exception as e:
logger.error(f"POST request failed: {url} - {e}")
raise
def _handle_response(self, response: requests.Response, url: str) -> requests.Response:
"""
统一响应处理和错误检测
参数:
response: curl_cffi 响应对象
url: 请求的 URL用于日志
返回:
原始 Response 对象(如果没有错误)
抛出:
CloudflareBlockError: 检测到 Cloudflare 挑战页面
SessionInvalidError: 检测到 409 会话冲突
RateLimitError: 检测到 429 速率限制
"""
status_code = response.status_code
# 检测 Cloudflare Turnstile 挑战403 + 特征文本)
if status_code == 403:
if self._is_cloudflare_challenge(response):
logger.error(f"Cloudflare challenge detected: {url}")
raise CloudflareBlockError(
f"Cloudflare Turnstile challenge triggered at {url}. "
"Possible solutions: use residential proxy, solve captcha, or retry later."
)
# 检测会话冲突CSRF Token 失效)
if status_code == 409:
logger.error(f"Session conflict (409): {url} - {response.text[:200]}")
raise SessionInvalidError(
f"Session invalid (409 Conflict): {response.text[:200]}. "
"This usually means CSRF token expired or cookie chain broken. "
"Need to restart registration flow."
)
# 检测速率限制
if status_code == 429:
logger.error(f"Rate limit exceeded (429): {url}")
raise RateLimitError(
f"Rate limit exceeded at {url}. "
"Recommendation: slow down requests or change IP/proxy."
)
# 记录其他错误响应4xx, 5xx
if status_code >= 400:
logger.warning(
f"HTTP {status_code} error: {url}\n"
f"Response preview: {response.text[:300]}"
)
# 记录成功响应(调试用)
if status_code < 300:
logger.debug(f"HTTP {status_code} OK: {url}")
return response
@staticmethod
def _is_cloudflare_challenge(response: requests.Response) -> bool:
"""
检测响应是否为 Cloudflare Turnstile 挑战页面
特征:
- 状态码 403
- 包含 "Just a moment""Checking your browser" 等文本
- 包含 Cloudflare 相关 JavaScript
"""
body = response.text.lower()
cloudflare_keywords = [
"just a moment",
"checking your browser",
"cloudflare",
"cf-challenge",
"ray id"
]
return any(keyword in body for keyword in cloudflare_keywords)
@staticmethod
def _mask_proxy(proxy: str) -> str:
"""
脱敏代理地址(隐藏用户名和密码)
例如: http://user:pass@1.2.3.4:8080 -> http://***:***@1.2.3.4:8080
"""
import re
return re.sub(r'://([^:]+):([^@]+)@', r'://***:***@', proxy)
def get_cookies(self) -> Dict[str, str]:
"""
获取当前所有 Cookie
返回:
Cookie 字典 {name: value}
"""
return {cookie.name: cookie.value for cookie in self.client.cookies}
def get_cookie(self, name: str) -> Optional[str]:
"""
获取指定名称的 Cookie 值
参数:
name: Cookie 名称
返回:
Cookie 值,不存在则返回 None
"""
return self.client.cookies.get(name)
def set_cookie(self, name: str, value: str, domain: str = None):
"""
设置 Cookie
参数:
name: Cookie 名称
value: Cookie 值
domain: Cookie 作用域(默认 .chatgpt.com
"""
if domain is None:
domain = f".{self.CHATGPT_DOMAIN}"
self.client.cookies.set(name, value, domain=domain)
logger.debug(f"Cookie set: {name}={value[:10]}... (domain={domain})")
def close(self):
"""关闭会话,释放资源"""
try:
self.client.close()
logger.debug("Session closed")
except Exception as e:
logger.warning(f"Error closing session: {e}")
def __enter__(self):
"""支持 with 语句上下文管理"""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""退出上下文时自动关闭"""
self.close()
# 导出主要接口
__all__ = [
"OAISession",
"CloudflareBlockError",
"SessionInvalidError",
"RateLimitError",
]