准备邮箱
This commit is contained in:
87
docs/tempmail.md
Normal file
87
docs/tempmail.md
Normal file
@@ -0,0 +1,87 @@
|
||||
## API 接口
|
||||
|
||||
### 🔐 根管理员令牌(Root Admin Override)
|
||||
|
||||
当请求方携带与服务端环境变量 `JWT_TOKEN`完全一致的令牌时,将跳过会话 Cookie/JWT 校验,直接被识别为最高管理员(strictAdmin)
|
||||
|
||||
- 配置项:
|
||||
- `wrangler.toml` → `[vars]` → `JWT_TOKEN="你的超管令牌"`
|
||||
- 令牌携带方式(任选其一):
|
||||
- Header(标准):`Authorization: Bearer <JWT_TOKEN>`
|
||||
- Header(自定义):`X-Admin-Token: <JWT_TOKEN>`
|
||||
- Query:`?admin_token=<JWT_TOKEN>`
|
||||
|
||||
- 生效范围:
|
||||
- 所有受保护的后端接口:`/api/*`
|
||||
- 会话检查:`GET /api/session`
|
||||
- 收信回调:`POST /receive`
|
||||
- 管理页服务端访问判定(`/admin`/`/admin.html`)与未知路径的认证判断
|
||||
|
||||
- 行为说明:
|
||||
- 命中令牌后,鉴权载荷为:`{ role: 'admin', username: '__root__', userId: 0 }`
|
||||
- `strictAdmin` 判定对 `__root__` 为 true(与严格管理员等价)
|
||||
- 若未携带或不匹配,则回退到原有 Cookie/JWT 会话验证
|
||||
|
||||
- 使用示例:
|
||||
- cURL(Authorization 头):
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <JWT_TOKEN>" https://your.domain/api/mailboxes
|
||||
```
|
||||
- cURL(X-Admin-Token):
|
||||
```bash
|
||||
curl -H "X-Admin-Token: <JWT_TOKEN>" https://your.domain/api/domains
|
||||
```
|
||||
- GET(Query):
|
||||
```
|
||||
GET /api/session?admin_token=<JWT_TOKEN>
|
||||
```
|
||||
|
||||
- 风险与建议(务必阅读):
|
||||
- 严格保密 `JWT_TOKEN`,并定期更换
|
||||
|
||||
### 🎲 邮箱管理
|
||||
- `GET /api/generate` - 生成新的临时邮箱
|
||||
- 返回: `{ "email": "random@domain.com", "expires": timestamp }`
|
||||
- `GET /api/mailboxes` - 获取历史邮箱列表
|
||||
- 参数: `limit`(页面大小), `offset`(偏移量)
|
||||
- 返回: 邮箱列表数组
|
||||
- `DELETE /api/mailbox/{address}` - 删除指定邮箱
|
||||
- 返回: `{ "success": true }`
|
||||
|
||||
### 📧 邮件操作
|
||||
- `GET /api/emails?mailbox=email@domain.com` - 获取邮件列表
|
||||
- 返回: 邮件列表数组,包含发件人、主题、时间等信息
|
||||
- `GET /api/email/{id}` - 获取邮件详情
|
||||
- 返回: 完整的邮件内容,包括HTML和纯文本
|
||||
- `DELETE /api/email/{id}` - 删除单个邮件
|
||||
- 返回: `{ "success": true, "deleted": true, "message": "邮件已删除" }`
|
||||
- `DELETE /api/emails?mailbox=email@domain.com` - 清空邮箱所有邮件
|
||||
- 返回: `{ "success": true, "deletedCount": 5, "previousCount": 5 }`
|
||||
|
||||
### 🔐 认证相关
|
||||
- `POST /api/login` - 用户登录
|
||||
- 参数: `{ "username": "用户名", "password": "密码" }`
|
||||
- 返回: `{ success: true, role, can_send, mailbox_limit }` 并设置会话 Cookie
|
||||
- `POST /api/logout` - 用户退出
|
||||
- 返回: `{ "success": true }`
|
||||
|
||||
### 🔧 系统接口
|
||||
- `GET /api/domains` - 获取可用域名列表
|
||||
- 返回: 域名数组
|
||||
|
||||
### 👤 用户管理(管理后台)
|
||||
- `GET /api/users` - 获取用户列表
|
||||
- 返回: 用户数组(含 id/username/role/mailbox_limit/can_send/mailbox_count/created_at)
|
||||
- `GET /api/users/{userId}/mailboxes` - 获取指定用户的邮箱列表
|
||||
- 返回: 邮箱数组(address/created_at)
|
||||
- `POST /api/users` - 创建用户
|
||||
- 参数: `{ username, password, role }`(role: `user` | `admin`)
|
||||
- 返回: `{ success: true }`
|
||||
- `PATCH /api/users/{userId}` - 更新用户
|
||||
- 参数示例: `{ username?, password?, mailboxLimit?, can_send?, role? }`
|
||||
- 返回: `{ success: true }`
|
||||
- `DELETE /api/users/{userId}` - 删除用户
|
||||
- 返回: `{ success: true }`
|
||||
- `POST /api/users/assign` - 给用户分配邮箱
|
||||
- 参数: `{ username, address }`
|
||||
- 返回: `{ success: true }`
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# download_sdk.py
|
||||
"""下载 OpenAI Sentinel SDK"""
|
||||
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
SDK_URL = "https://sentinel.openai.com/sentinel/97790f37/sdk.js"
|
||||
OUTPUT_PATH = Path("assets/sdk.js")
|
||||
|
||||
def download_sdk():
|
||||
print(f"Downloading SDK from {SDK_URL}...")
|
||||
|
||||
OUTPUT_PATH.parent.mkdir(exist_ok=True)
|
||||
|
||||
resp = requests.get(SDK_URL)
|
||||
resp.raise_for_status()
|
||||
|
||||
with open(OUTPUT_PATH, 'wb') as f:
|
||||
f.write(resp.content)
|
||||
|
||||
print(f"✓ SDK saved to {OUTPUT_PATH}")
|
||||
print(f" Size: {len(resp.content) / 1024:.1f} KB")
|
||||
|
||||
if __name__ == '__main__':
|
||||
download_sdk()
|
||||
@@ -5,7 +5,7 @@ from typing import Dict, Optional
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.ssl_ import create_urllib3_context
|
||||
from urllib.parse import unquote
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
from .fingerprint import BrowserFingerprint
|
||||
from config import HTTP_CONFIG, DEBUG
|
||||
@@ -36,7 +36,10 @@ class HTTPClient:
|
||||
adapter = TLSAdapter()
|
||||
self.session.mount('https://', adapter)
|
||||
|
||||
self.cookies = fingerprint.get_cookies()
|
||||
# Use the session cookie jar as the single source of truth so that
|
||||
# cookies set on redirects (resp.history) are not lost.
|
||||
self.session.cookies.update(fingerprint.get_cookies())
|
||||
self.cookies = self.session.cookies
|
||||
self.csrf_token = None
|
||||
# 确保自动解压
|
||||
self.session.headers.update({
|
||||
@@ -45,9 +48,9 @@ class HTTPClient:
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
})
|
||||
|
||||
def extract_csrf_from_cookies(self) -> Optional[str]:
|
||||
"""从 cookies 中提取 CSRF token"""
|
||||
csrf_cookie = self.cookies.get('__Host-next-auth.csrf-token')
|
||||
def extract_csrf_from_cookies(self, domain: str = "auth.openai.com") -> Optional[str]:
|
||||
"""从 cookies 中提取 CSRF token(按域名)"""
|
||||
csrf_cookie = self.cookies.get('__Host-next-auth.csrf-token', domain=domain)
|
||||
|
||||
if not csrf_cookie:
|
||||
return None
|
||||
@@ -75,12 +78,10 @@ class HTTPClient:
|
||||
print(f"[HTTP] GET {url}")
|
||||
|
||||
resp = self.session.get(url, **kwargs)
|
||||
|
||||
# 更新 cookies
|
||||
self.cookies.update(resp.cookies.get_dict())
|
||||
|
||||
# 尝试提取 CSRF
|
||||
csrf = self.extract_csrf_from_cookies()
|
||||
|
||||
# 尝试提取 CSRF(按请求域名)
|
||||
domain = urlparse(url).hostname or "auth.openai.com"
|
||||
csrf = self.extract_csrf_from_cookies(domain=domain)
|
||||
if csrf:
|
||||
self.csrf_token = csrf
|
||||
|
||||
@@ -90,7 +91,7 @@ class HTTPClient:
|
||||
|
||||
return resp
|
||||
|
||||
def get_csrf_token(self) -> Optional[str]:
|
||||
def get_csrf_token(self, domain: str = "auth.openai.com") -> Optional[str]:
|
||||
"""
|
||||
获取 CSRF token
|
||||
|
||||
@@ -100,7 +101,7 @@ class HTTPClient:
|
||||
3. 如果都失败,返回 None(某些流程可能不需要)
|
||||
"""
|
||||
# 1. 从 cookie 提取
|
||||
csrf = self.extract_csrf_from_cookies()
|
||||
csrf = self.extract_csrf_from_cookies(domain=domain)
|
||||
if csrf:
|
||||
self.csrf_token = csrf
|
||||
return csrf
|
||||
@@ -110,7 +111,7 @@ class HTTPClient:
|
||||
print("[HTTP] Requesting CSRF token from /api/auth/csrf...")
|
||||
|
||||
try:
|
||||
url = "https://auth.openai.com/api/auth/csrf"
|
||||
url = f"https://{domain}/api/auth/csrf"
|
||||
|
||||
resp = self.session.get(
|
||||
url,
|
||||
@@ -119,11 +120,8 @@ class HTTPClient:
|
||||
timeout=HTTP_CONFIG['timeout']
|
||||
)
|
||||
|
||||
# 更新 cookies
|
||||
self.cookies.update(resp.cookies.get_dict())
|
||||
|
||||
# 尝试从 cookie 提取
|
||||
csrf = self.extract_csrf_from_cookies()
|
||||
csrf = self.extract_csrf_from_cookies(domain=domain)
|
||||
if csrf:
|
||||
self.csrf_token = csrf
|
||||
return csrf
|
||||
|
||||
114
modules/pow_solver.py
Normal file
114
modules/pow_solver.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import time
|
||||
import json
|
||||
import base64
|
||||
from typing import List
|
||||
|
||||
DEBUG = True
|
||||
|
||||
class ProofOfWorkSolver:
|
||||
"""解决 OpenAI Sentinel 的 Proof of Work challenge"""
|
||||
|
||||
def __init__(self):
|
||||
# FNV-1a 常量
|
||||
self.FNV_OFFSET = 2166136261
|
||||
self.FNV_PRIME = 16777619
|
||||
|
||||
def fnv1a_hash(self, data: str) -> str:
|
||||
"""FNV-1a hash 算法"""
|
||||
hash_value = self.FNV_OFFSET
|
||||
|
||||
for char in data:
|
||||
hash_value ^= ord(char)
|
||||
hash_value = (hash_value * self.FNV_PRIME) & 0xFFFFFFFF
|
||||
|
||||
# 额外的混合步骤(从 JS 代码复制)
|
||||
hash_value ^= hash_value >> 16
|
||||
hash_value = (hash_value * 2246822507) & 0xFFFFFFFF
|
||||
hash_value ^= hash_value >> 13
|
||||
hash_value = (hash_value * 3266489909) & 0xFFFFFFFF
|
||||
hash_value ^= hash_value >> 16
|
||||
|
||||
# 转为 8 位十六进制字符串
|
||||
return format(hash_value, '08x')
|
||||
|
||||
def serialize_array(self, arr: List) -> str:
|
||||
"""模拟 JS 的 T() 函数:JSON.stringify + Base64"""
|
||||
json_str = json.dumps(arr, separators=(',', ':'))
|
||||
return base64.b64encode(json_str.encode()).decode()
|
||||
|
||||
def build_fingerprint_array(self, nonce: int, elapsed_ms: int) -> List:
|
||||
"""构建指纹数组(简化版)"""
|
||||
return [
|
||||
0, # [0] screen dimensions
|
||||
"", # [1] timestamp
|
||||
0, # [2] memory
|
||||
nonce, # [3] nonce ← 关键
|
||||
"", # [4] user agent
|
||||
"", # [5] random element
|
||||
"", # [6] script src
|
||||
"", # [7] language
|
||||
"", # [8] languages
|
||||
elapsed_ms, # [9] elapsed time ← 关键
|
||||
"", # [10] random function
|
||||
"", # [11] keys
|
||||
"", # [12] window keys
|
||||
0, # [13] performance.now()
|
||||
"", # [14] uuid
|
||||
"", # [15] URL params
|
||||
0, # [16] hardware concurrency
|
||||
0 # [17] timeOrigin
|
||||
]
|
||||
|
||||
def solve(self, seed: str, difficulty: str, max_iterations: int = 10000000) -> str:
|
||||
"""
|
||||
解决 PoW challenge
|
||||
|
||||
Args:
|
||||
seed: Challenge seed
|
||||
difficulty: 目标难度(十六进制字符串)
|
||||
max_iterations: 最大尝试次数
|
||||
|
||||
Returns:
|
||||
序列化的答案(包含 nonce)
|
||||
"""
|
||||
if DEBUG:
|
||||
print(f"[PoW] Solving challenge:")
|
||||
print(f" Seed: {seed}")
|
||||
print(f" Difficulty: {difficulty}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for nonce in range(max_iterations):
|
||||
elapsed_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
# 构建指纹数组
|
||||
fingerprint = self.build_fingerprint_array(nonce, elapsed_ms)
|
||||
|
||||
# 序列化
|
||||
serialized = self.serialize_array(fingerprint)
|
||||
|
||||
# 计算 hash(seed + serialized)
|
||||
hash_input = seed + serialized
|
||||
hash_result = self.fnv1a_hash(hash_input)
|
||||
|
||||
# 检查是否满足难度要求
|
||||
# 比较方式:hash 的前 N 位(作为整数)<= difficulty(作为整数)
|
||||
difficulty_len = len(difficulty)
|
||||
hash_prefix = hash_result[:difficulty_len]
|
||||
|
||||
if hash_prefix <= difficulty:
|
||||
elapsed = time.time() - start_time
|
||||
if DEBUG:
|
||||
print(f"[PoW] ✓ Found solution in {elapsed:.2f}s")
|
||||
print(f" Nonce: {nonce}")
|
||||
print(f" Hash: {hash_result}")
|
||||
print(f" Serialized: {serialized[:100]}...")
|
||||
|
||||
# 返回 serialized + "~S" (表示成功)
|
||||
return serialized + "~S"
|
||||
|
||||
# 每 100k 次迭代打印进度
|
||||
if DEBUG and nonce > 0 and nonce % 100000 == 0:
|
||||
print(f"[PoW] Tried {nonce:,} iterations...")
|
||||
|
||||
raise Exception(f"Failed to solve PoW after {max_iterations:,} iterations")
|
||||
@@ -10,7 +10,7 @@ from .fingerprint import BrowserFingerprint
|
||||
from .sentinel_solver import SentinelSolver
|
||||
from .http_client import HTTPClient
|
||||
from config import AUTH_BASE_URL, DEBUG
|
||||
|
||||
from modules.pow_solver import ProofOfWorkSolver
|
||||
|
||||
class OpenAIRegistrar:
|
||||
"""完整的 OpenAI 注册流程"""
|
||||
@@ -19,8 +19,9 @@ class OpenAIRegistrar:
|
||||
self.fingerprint = BrowserFingerprint(session_id)
|
||||
self.solver = SentinelSolver(self.fingerprint)
|
||||
self.http_client = HTTPClient(self.fingerprint)
|
||||
self.pow_solver = ProofOfWorkSolver() # 新增
|
||||
|
||||
def _step1_init_through_chatgpt(self):
|
||||
def _step1_init_through_chatgpt(self, email: str):
|
||||
"""通过 ChatGPT web 初始化注册流程"""
|
||||
if DEBUG:
|
||||
print("\n=== STEP 1: Initializing through ChatGPT web ===")
|
||||
@@ -36,12 +37,9 @@ class OpenAIRegistrar:
|
||||
resp = self.http_client.session.get(
|
||||
chatgpt_url,
|
||||
headers=headers,
|
||||
cookies=self.http_client.cookies,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
self.http_client.cookies.update(resp.cookies.get_dict())
|
||||
|
||||
|
||||
if DEBUG:
|
||||
print(f"[1.1] Visited ChatGPT: {resp.status_code}")
|
||||
print(f" Cookies: {list(self.http_client.cookies.keys())}")
|
||||
@@ -57,12 +55,9 @@ class OpenAIRegistrar:
|
||||
resp = self.http_client.session.get(
|
||||
csrf_url,
|
||||
headers=csrf_headers,
|
||||
cookies=self.http_client.cookies,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
self.http_client.cookies.update(resp.cookies.get_dict())
|
||||
|
||||
|
||||
if DEBUG:
|
||||
print(f"[1.2] CSRF API response: {resp.status_code}")
|
||||
|
||||
@@ -103,7 +98,7 @@ class OpenAIRegistrar:
|
||||
'ext-oai-did': self.fingerprint.session_id,
|
||||
'auth_session_logging_id': auth_session_logging_id,
|
||||
'screen_hint': 'signup', # 明确指定注册
|
||||
'login_hint': '', # 先留空,让服务器决定
|
||||
'login_hint': email, # 🔥 关键:传入邮箱
|
||||
}
|
||||
|
||||
signin_data = {
|
||||
@@ -124,7 +119,6 @@ class OpenAIRegistrar:
|
||||
params=signin_params,
|
||||
data=signin_data,
|
||||
headers=signin_headers,
|
||||
cookies=self.http_client.cookies,
|
||||
allow_redirects=False,
|
||||
timeout=30
|
||||
)
|
||||
@@ -133,7 +127,6 @@ class OpenAIRegistrar:
|
||||
if DEBUG:
|
||||
print(f"[1.3] NextAuth response: {resp.status_code}")
|
||||
|
||||
self.http_client.cookies.update(resp.cookies.get_dict())
|
||||
if DEBUG:
|
||||
content_type = resp.headers.get("Content-Type", "")
|
||||
content_encoding = resp.headers.get("Content-Encoding", "")
|
||||
@@ -189,18 +182,23 @@ class OpenAIRegistrar:
|
||||
resp = self.http_client.session.get(
|
||||
oauth_url,
|
||||
headers=auth_headers,
|
||||
cookies=self.http_client.cookies,
|
||||
allow_redirects=True, # 自动跟随重定向
|
||||
timeout=30
|
||||
)
|
||||
|
||||
self.http_client.cookies.update(resp.cookies.get_dict())
|
||||
|
||||
if DEBUG:
|
||||
print(f" Final status: {resp.status_code}")
|
||||
print(f" Final URL: {resp.url if hasattr(resp, 'url') else 'N/A'}")
|
||||
print(f"[1.5] Cookies after OAuth:")
|
||||
print(f" {list(self.http_client.cookies.keys())}")
|
||||
if resp.history:
|
||||
print(" Redirect chain cookies:")
|
||||
for i, h in enumerate(resp.history, 1):
|
||||
cookies_set = list(h.cookies.keys())
|
||||
print(f" {i}. {h.status_code} {h.url} -> set {cookies_set}")
|
||||
auth_cookie_names = list(self.http_client.session.cookies.get_dict(domain='auth.openai.com').keys())
|
||||
if auth_cookie_names:
|
||||
print(f" auth.openai.com cookies: {sorted(auth_cookie_names)}")
|
||||
|
||||
# 检查必需的 cookies
|
||||
required_cookies = ['login_session', 'oai-did']
|
||||
@@ -215,6 +213,15 @@ class OpenAIRegistrar:
|
||||
if DEBUG:
|
||||
print(f" ✓ All required cookies present")
|
||||
|
||||
# 1.6 确保获取 auth.openai.com 的 CSRF(用于后续 /register 请求)
|
||||
try:
|
||||
auth_csrf = self.http_client.get_csrf_token(domain="auth.openai.com")
|
||||
if DEBUG and auth_csrf:
|
||||
print(f"[1.6] ✓ auth CSRF: {auth_csrf[:20]}...")
|
||||
except Exception as e:
|
||||
if DEBUG:
|
||||
print(f"[1.6] Warning: Failed to get auth CSRF: {e}")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -235,96 +242,349 @@ class OpenAIRegistrar:
|
||||
if DEBUG:
|
||||
print(f"✗ Sentinel initialization failed: {e}")
|
||||
raise
|
||||
|
||||
def _step3_attempt_register(self, email: str, password: str) -> Dict:
|
||||
"""尝试注册(会触发 enforcement)"""
|
||||
def _step2_5_submit_sentinel(self):
|
||||
"""Step 2.5: 提交 Sentinel token 到 OpenAI"""
|
||||
if DEBUG:
|
||||
print("\n=== STEP 3: Attempting registration ===")
|
||||
print("\n=== STEP 2.5: Submitting Sentinel token ===")
|
||||
|
||||
url = f"{AUTH_BASE_URL}/api/accounts/user/register"
|
||||
# 如果 sentinel_token 是字符串,先解析
|
||||
if isinstance(self.sentinel_token, str):
|
||||
token_data = json.loads(self.sentinel_token)
|
||||
else:
|
||||
token_data = self.sentinel_token
|
||||
|
||||
url = "https://sentinel.openai.com/backend-api/sentinel/req"
|
||||
|
||||
# 正确的 payload 格式(根据真实抓包)
|
||||
payload = {
|
||||
'username': email,
|
||||
'password': password
|
||||
"p": token_data['p'],
|
||||
"id": self.fingerprint.session_id,
|
||||
"flow": "username_password_create"
|
||||
}
|
||||
|
||||
# 添加额外的 headers
|
||||
headers = self.http_client.fingerprint.get_headers(with_sentinel=self.sentinel_token)
|
||||
headers['Accept'] = 'application/json'
|
||||
headers['Referer'] = f'{AUTH_BASE_URL}/create-account/password'
|
||||
headers['Origin'] = AUTH_BASE_URL
|
||||
headers = self.http_client.fingerprint.get_headers(host='sentinel.openai.com')
|
||||
headers['Content-Type'] = 'text/plain;charset=UTF-8' # 注意:text/plain
|
||||
headers['Origin'] = 'https://sentinel.openai.com'
|
||||
headers['Referer'] = 'https://sentinel.openai.com/backend-api/sentinel/frame.html'
|
||||
headers['Accept'] = '*/*'
|
||||
headers['Sec-Fetch-Site'] = 'same-origin'
|
||||
headers['Sec-Fetch-Mode'] = 'cors'
|
||||
headers['Sec-Fetch-Dest'] = 'empty'
|
||||
|
||||
# 添加 Datadog tracing headers(模拟真实浏览器)
|
||||
headers.update({
|
||||
'traceparent': f'00-0000000000000000{secrets.token_hex(8)}-{secrets.token_hex(8)}-01',
|
||||
'tracestate': 'dd=s:1;o:rum',
|
||||
'x-datadog-origin': 'rum',
|
||||
'x-datadog-parent-id': str(secrets.randbits(63)),
|
||||
'x-datadog-sampling-priority': '1',
|
||||
'x-datadog-trace-id': str(secrets.randbits(63))
|
||||
})
|
||||
if DEBUG:
|
||||
print(f"[2.5] POST {url}")
|
||||
|
||||
resp = self.http_client.session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
cookies=self.http_client.cookies,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if DEBUG:
|
||||
print(f" Status: {resp.status_code}")
|
||||
|
||||
if resp.status_code != 200:
|
||||
raise Exception(f"Failed to submit Sentinel token: {resp.status_code} {resp.text}")
|
||||
|
||||
try:
|
||||
data = resp.json()
|
||||
if DEBUG:
|
||||
print(f" ✓ Sentinel accepted")
|
||||
print(f" Persona: {data.get('persona')}")
|
||||
print(f" Token expires in: {data.get('expire_after')}s")
|
||||
|
||||
# 检查是否需要额外验证
|
||||
if data.get('turnstile', {}).get('required'):
|
||||
print(f" ⚠ Turnstile required")
|
||||
if data.get('proofofwork', {}).get('required'):
|
||||
print(f" ⚠ Proof of Work required (difficulty: {data.get('proofofwork', {}).get('difficulty')})")
|
||||
|
||||
# 保存 token(可能后续需要)
|
||||
self.sentinel_response = data
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
if DEBUG:
|
||||
print(f" ✗ Failed to parse response: {e}")
|
||||
print(f" Raw: {resp.text[:200]}")
|
||||
raise
|
||||
|
||||
def _step2_6_solve_pow(self):
|
||||
"""Step 2.6: 解 Proof of Work"""
|
||||
if DEBUG:
|
||||
print("\n=== STEP 2.6: Solving Proof of Work ===")
|
||||
|
||||
pow_data = self.sentinel_response.get('proofofwork', {})
|
||||
|
||||
if not pow_data.get('required'):
|
||||
if DEBUG:
|
||||
print("[2.6] PoW not required, skipping")
|
||||
return None
|
||||
|
||||
seed = pow_data.get('seed')
|
||||
difficulty = pow_data.get('difficulty')
|
||||
|
||||
if not seed or not difficulty:
|
||||
raise Exception(f"Missing PoW parameters")
|
||||
|
||||
# 解 PoW
|
||||
self.pow_answer = self.pow_solver.solve(seed, difficulty)
|
||||
|
||||
if DEBUG:
|
||||
print(f"[2.6] ✓ PoW solved: {self.pow_answer[:50]}...")
|
||||
|
||||
return self.pow_answer
|
||||
def _step2_7_submit_pow(self):
|
||||
"""Step 2.7: 提交 PoW 答案到 Sentinel"""
|
||||
if DEBUG:
|
||||
print("\n=== STEP 2.7: Submitting PoW ===")
|
||||
|
||||
url = "https://sentinel.openai.com/backend-api/sentinel/req"
|
||||
|
||||
# 再次生成 requirements token(或重用之前的)
|
||||
requirements_token = self.sentinel_token.split('"p": "')[1].split('"')[0]
|
||||
|
||||
payload = {
|
||||
"p": requirements_token,
|
||||
"id": self.fingerprint.session_id,
|
||||
"answer": self.pow_answer
|
||||
}
|
||||
|
||||
headers = self.http_client.fingerprint.get_headers(host='sentinel.openai.com')
|
||||
headers['Content-Type'] = 'text/plain;charset=UTF-8'
|
||||
headers['Origin'] = 'https://sentinel.openai.com'
|
||||
headers['Referer'] = 'https://sentinel.openai.com/backend-api/sentinel/frame.html'
|
||||
|
||||
# 补全 Sec-Fetch-* headers
|
||||
headers['Sec-Fetch-Dest'] = 'empty'
|
||||
headers['Sec-Fetch-Mode'] = 'cors'
|
||||
headers['Sec-Fetch-Site'] = 'same-origin'
|
||||
|
||||
if DEBUG:
|
||||
print(f"[2.7] POST {url}")
|
||||
print(f" Answer: {self.pow_answer[:50]}...")
|
||||
|
||||
resp = self.http_client.session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
|
||||
if DEBUG:
|
||||
print(f" Status: {resp.status_code}")
|
||||
print(f" Response: {resp.text}")
|
||||
|
||||
if resp.status_code != 200:
|
||||
# 如果是 403,打印完整响应以便调试
|
||||
if DEBUG and resp.status_code == 403:
|
||||
print(f" ✗ Cloudflare blocked")
|
||||
print(f" Cookies sent:")
|
||||
for cookie in self.http_client.session.cookies:
|
||||
if cookie.domain in ['sentinel.openai.com', '.chatgpt.com']:
|
||||
print(f" {cookie.name}={cookie.value[:30]}...")
|
||||
raise Exception(f"Failed to submit PoW: {resp.status_code}")
|
||||
|
||||
# 解析响应
|
||||
result = resp.json()
|
||||
|
||||
if DEBUG:
|
||||
print(f" ✓ PoW accepted")
|
||||
if 'token' in result:
|
||||
print(f" Token: {result['token'][:50]}...")
|
||||
if 'turnstile' in result:
|
||||
print(f" ⚠ Turnstile still required")
|
||||
|
||||
# 保存最终响应
|
||||
self.sentinel_response = result
|
||||
|
||||
# 保存 oai-sc cookie(如果返回了)
|
||||
if 'Set-Cookie' in resp.headers or any('oai-sc' in c.name for c in self.http_client.session.cookies):
|
||||
if DEBUG:
|
||||
print(f" ✓ oai-sc cookie received")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def _step3_attempt_register(self, email: str, password: str) -> Optional[Dict]:
|
||||
"""尝试注册(不验证邮箱)"""
|
||||
if DEBUG:
|
||||
print("\n=== STEP 3: Attempting registration ===")
|
||||
|
||||
# 🔍 调试:检查 oai-client-auth-session cookie 的内容
|
||||
if DEBUG:
|
||||
import base64
|
||||
import json
|
||||
|
||||
auth_session_cookie = self.http_client.cookies.get('oai-client-auth-session', '')
|
||||
if auth_session_cookie:
|
||||
try:
|
||||
# 解码 cookie(格式:base64.签名)
|
||||
cookie_data = auth_session_cookie.split('.')[0]
|
||||
decoded = base64.b64decode(cookie_data + '==') # 补齐 padding
|
||||
session_data = json.loads(decoded)
|
||||
|
||||
print(f"[3.0] oai-client-auth-session content:")
|
||||
print(f" session_id: {session_data.get('session_id', 'N/A')}")
|
||||
print(f" username: {session_data.get('username', 'N/A')}")
|
||||
print(f" email: {session_data.get('email', 'N/A')}")
|
||||
print(f" openai_client_id: {session_data.get('openai_client_id', 'N/A')}")
|
||||
|
||||
# 检查是否有 username/email 字段
|
||||
if 'username' not in session_data and 'email' not in session_data:
|
||||
print(f" ⚠ WARNING: No username/email in session!")
|
||||
print(f" This is likely why we get 'Invalid session'")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[3.0] Failed to decode oai-client-auth-session: {e}")
|
||||
else:
|
||||
print(f"[3.0] ⚠ oai-client-auth-session cookie not found!")
|
||||
|
||||
# 3.1 提取 CSRF token(从 oai-login-csrf_dev_* cookie)
|
||||
# auth_csrf_token = None
|
||||
# for cookie_name, cookie_value in self.http_client.cookies.items():
|
||||
# if cookie_name.startswith('oai-login-csrf_dev_'):
|
||||
# auth_csrf_token = cookie_value
|
||||
# if DEBUG:
|
||||
# print(f"[3.1] ✓ Extracted CSRF: {cookie_name}")
|
||||
# break
|
||||
|
||||
# if not auth_csrf_token:
|
||||
# if DEBUG:
|
||||
# print(f"[3.1] ✗ No oai-login-csrf_dev_* cookie found")
|
||||
# print(f" Available cookies: {list(self.http_client.cookies.keys())}")
|
||||
# raise Exception("No CSRF token available for Step 3")
|
||||
|
||||
# 3.2 准备注册请求(正确的 payload)
|
||||
url = "https://auth.openai.com/api/accounts/user/register"
|
||||
|
||||
payload = {
|
||||
"username": email, # 不是 "email"!
|
||||
"password": password,
|
||||
}
|
||||
|
||||
# 3.3 准备 headers(完全匹配真实请求)
|
||||
headers = self.http_client.fingerprint.get_headers(host='auth.openai.com')
|
||||
|
||||
# 添加 Sentinel token
|
||||
if hasattr(self, 'sentinel_token') and self.sentinel_token:
|
||||
headers['Openai-Sentinel-Token'] = self.sentinel_token # 注意大小写
|
||||
|
||||
headers.update({
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'Origin': 'https://auth.openai.com',
|
||||
'Referer': 'https://auth.openai.com/create-account/password', # 注意是 /password
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
'Priority': 'u=1, i',
|
||||
})
|
||||
|
||||
# 添加 Datadog tracing headers(匹配真实请求)
|
||||
import secrets
|
||||
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',
|
||||
})
|
||||
|
||||
# 3.4 准备 cookies(使用所有 auth.openai.com 的 cookies)
|
||||
# 不要手动过滤,让 requests 自动处理域名匹配
|
||||
|
||||
if DEBUG:
|
||||
print(f"[3.2] Request URL: {url}")
|
||||
print(f"[3.3] Payload: {payload}")
|
||||
print(f"[3.4] Cookies to send: {list(self.http_client.cookies.keys())}")
|
||||
|
||||
# 检查关键 cookies
|
||||
required = ['login_session', 'oai-client-auth-session', 'auth-session-minimized']
|
||||
missing = [c for c in required if c not in self.http_client.cookies]
|
||||
if missing:
|
||||
print(f" ⚠ WARNING: Missing critical cookies: {missing}")
|
||||
|
||||
# 3.5 发送注册请求
|
||||
resp = self.http_client.session.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
# 不手动指定 cookies,让 session 自动处理
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if DEBUG:
|
||||
print(f"Response status: {resp.status_code}")
|
||||
|
||||
# 更新 cookies
|
||||
self.http_client.cookies.update(resp.cookies.get_dict())
|
||||
|
||||
# 403: Enforcement 挑战(预期)
|
||||
if resp.status_code == 403:
|
||||
try:
|
||||
challenge = resp.json()
|
||||
|
||||
if DEBUG:
|
||||
print(f"✓ Got enforcement challenge!")
|
||||
print(f" Type: {challenge.get('type', 'unknown')}")
|
||||
if 'proofofwork' in challenge:
|
||||
pow_data = challenge['proofofwork']
|
||||
print(f" PoW seed: {pow_data.get('seed', 'N/A')}")
|
||||
print(f" Difficulty: {pow_data.get('difficulty', 'N/A')}")
|
||||
|
||||
return challenge
|
||||
# 3.6 处理响应
|
||||
try:
|
||||
data = resp.json()
|
||||
|
||||
except Exception as e:
|
||||
if resp.status_code == 200:
|
||||
if DEBUG:
|
||||
print(f"✗ Failed to parse challenge: {e}")
|
||||
print(resp.text[:500])
|
||||
raise
|
||||
print(f"✓ Registration successful!")
|
||||
print(f" Response: {data}")
|
||||
|
||||
# 检查是否需要邮箱验证
|
||||
continue_url = data.get('continue_url', '')
|
||||
if 'email-otp' in continue_url:
|
||||
if DEBUG:
|
||||
print(f" → Next step: Email OTP verification")
|
||||
return {
|
||||
'success': True,
|
||||
'requires_verification': True,
|
||||
'continue_url': continue_url,
|
||||
'method': data.get('method', 'GET'),
|
||||
'data': data
|
||||
}
|
||||
else:
|
||||
return {'success': True, 'data': data}
|
||||
|
||||
elif resp.status_code == 409:
|
||||
error = data.get('error', {})
|
||||
error_code = error.get('code', '')
|
||||
|
||||
if DEBUG:
|
||||
print(f"✗ 409 Conflict: {error}")
|
||||
|
||||
if error_code == 'invalid_state':
|
||||
# Session 无效
|
||||
raise Exception(f"Invalid session: {error}")
|
||||
|
||||
elif error_code == 'email_taken' or 'already' in str(error).lower():
|
||||
# 邮箱已被使用
|
||||
if DEBUG:
|
||||
print(f" Email already registered")
|
||||
return None
|
||||
|
||||
else:
|
||||
raise Exception(f"Registration conflict: {data}")
|
||||
|
||||
elif resp.status_code == 400:
|
||||
if DEBUG:
|
||||
print(f"✗ 400 Bad Request: {data}")
|
||||
raise Exception(f"Bad request: {data}")
|
||||
|
||||
else:
|
||||
if DEBUG:
|
||||
print(f"✗ Unexpected status: {data}")
|
||||
raise Exception(f"Registration failed with {resp.status_code}: {data}")
|
||||
|
||||
# 200: 成功
|
||||
elif resp.status_code == 200:
|
||||
except json.JSONDecodeError as e:
|
||||
if DEBUG:
|
||||
print("✓ Registration succeeded!")
|
||||
return {'success': True, 'data': resp.json()}
|
||||
print(f"✗ Failed to parse JSON response")
|
||||
print(f" Content: {resp.text[:500]}")
|
||||
raise Exception(f"Invalid JSON response: {e}")
|
||||
|
||||
# 409: Invalid session
|
||||
elif resp.status_code == 409:
|
||||
error = resp.json()
|
||||
except Exception as e:
|
||||
if DEBUG:
|
||||
print(f"✗ 409: {error}")
|
||||
raise Exception(f"Invalid session: {error}")
|
||||
|
||||
# 400: 参数错误
|
||||
elif resp.status_code == 400:
|
||||
error = resp.json()
|
||||
if DEBUG:
|
||||
print(f"✗ 400: {error}")
|
||||
raise Exception(f"Bad request: {error}")
|
||||
|
||||
# 其他错误
|
||||
else:
|
||||
if DEBUG:
|
||||
print(f"✗ Unexpected status: {resp.status_code}")
|
||||
print(resp.text[:500])
|
||||
raise Exception(f"Unexpected status: {resp.status_code}\n{resp.text}")
|
||||
print(f"✗ Exception: {e}")
|
||||
raise
|
||||
|
||||
|
||||
|
||||
@@ -384,10 +644,13 @@ class OpenAIRegistrar:
|
||||
|
||||
try:
|
||||
# Step 1: 通过 ChatGPT web 初始化(获取正确的 session)
|
||||
self._step1_init_through_chatgpt()
|
||||
self._step1_init_through_chatgpt(email)
|
||||
|
||||
# Step 2: 初始化 Sentinel
|
||||
self._step2_init_sentinel()
|
||||
self._step2_5_submit_sentinel()
|
||||
self._step2_6_solve_pow()
|
||||
self._step2_7_submit_pow()
|
||||
|
||||
# Step 3: 尝试注册(触发 enforcement)
|
||||
challenge = self._step3_attempt_register(email, password)
|
||||
|
||||
@@ -6,6 +6,7 @@ readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"curl-cffi>=0.14.0",
|
||||
"playwright>=1.57.0",
|
||||
"pyexecjs>=1.5.1",
|
||||
"requests>=2.32.5",
|
||||
]
|
||||
|
||||
81
uv.lock
generated
81
uv.lock
generated
@@ -8,6 +8,7 @@ version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "curl-cffi" },
|
||||
{ name = "playwright" },
|
||||
{ name = "pyexecjs" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
@@ -15,6 +16,7 @@ dependencies = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "curl-cffi", specifier = ">=0.14.0" },
|
||||
{ name = "playwright", specifier = ">=1.57.0" },
|
||||
{ name = "pyexecjs", specifier = ">=1.5.1" },
|
||||
{ name = "requests", specifier = ">=2.32.5" },
|
||||
]
|
||||
@@ -165,6 +167,45 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/7c/d2ba86b0b3e1e2830bd94163d047de122c69a8df03c5c7c36326c456ad82/curl_cffi-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:2eed50a969201605c863c4c31269dfc3e0da52916086ac54553cfa353022425c", size = 1425067, upload-time = "2025-12-16T03:25:06.454Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
@@ -174,6 +215,25 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "playwright"
|
||||
version = "1.57.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet" },
|
||||
{ name = "pyee" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/b6/e17543cea8290ae4dced10be21d5a43c360096aa2cce0aa7039e60c50df3/playwright-1.57.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:9351c1ac3dfd9b3820fe7fc4340d96c0d3736bb68097b9b7a69bd45d25e9370c", size = 41985039, upload-time = "2025-12-09T08:06:18.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/04/ef95b67e1ff59c080b2effd1a9a96984d6953f667c91dfe9d77c838fc956/playwright-1.57.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4a9d65027bce48eeba842408bcc1421502dfd7e41e28d207e94260fa93ca67e", size = 40775575, upload-time = "2025-12-09T08:06:22.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bd/5563850322a663956c927eefcf1457d12917e8f118c214410e815f2147d1/playwright-1.57.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:99104771abc4eafee48f47dac2369e0015516dc1ce8c409807d2dd440828b9a4", size = 41985042, upload-time = "2025-12-09T08:06:25.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/61/3a803cb5ae0321715bfd5247ea871d25b32c8f372aeb70550a90c5f586df/playwright-1.57.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:284ed5a706b7c389a06caa431b2f0ba9ac4130113c3a779767dda758c2497bb1", size = 45975252, upload-time = "2025-12-09T08:06:29.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/d7/b72eb59dfbea0013a7f9731878df8c670f5f35318cedb010c8a30292c118/playwright-1.57.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a1bae6c0a07839cdeaddbc0756b3b2b85e476c07945f64ece08f1f956a86f1", size = 45706917, upload-time = "2025-12-09T08:06:32.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/09/3fc9ebd7c95ee54ba6a68d5c0bc23e449f7235f4603fc60534a364934c16/playwright-1.57.0-py3-none-win32.whl", hash = "sha256:1dd93b265688da46e91ecb0606d36f777f8eadcf7fbef12f6426b20bf0c9137c", size = 36553860, upload-time = "2025-12-09T08:06:35.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/d4/dcdfd2a33096aeda6ca0d15584800443dd2be64becca8f315634044b135b/playwright-1.57.0-py3-none-win_amd64.whl", hash = "sha256:6caefb08ed2c6f29d33b8088d05d09376946e49a73be19271c8cd5384b82b14c", size = 36553864, upload-time = "2025-12-09T08:06:38.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/60/fe31d7e6b8907789dcb0584f88be741ba388413e4fbce35f1eba4e3073de/playwright-1.57.0-py3-none-win_arm64.whl", hash = "sha256:5f065f5a133dbc15e6e7c71e7bc04f258195755b1c32a432b792e28338c8335e", size = 32837940, upload-time = "2025-12-09T08:06:42.268Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.23"
|
||||
@@ -183,6 +243,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyee"
|
||||
version = "13.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyexecjs"
|
||||
version = "1.5.1"
|
||||
@@ -216,6 +288,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
|
||||
Reference in New Issue
Block a user