feat: xAI Grok auto-registration tool
- DrissionPage + Chrome browser automation - Temporary email via mail.tm API - Verification code auto-extraction - Turnstile CAPTCHA bypass via extension - SSO cookie extraction and persistence
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
__pycache__/
|
||||||
|
.venv/
|
||||||
|
sso.txt
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,568 @@
|
|||||||
|
from pprint import pprint
|
||||||
|
from DrissionPage import Chromium, ChromiumOptions
|
||||||
|
from DrissionPage.items import ChromiumTab, MixTab
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
|
||||||
|
PAYMENT_INFO = {
|
||||||
|
'guid': None,
|
||||||
|
'muid': None,
|
||||||
|
'sid': None,
|
||||||
|
'hcaptcha_response': None,
|
||||||
|
'portal_session_id': None,
|
||||||
|
'session_api_key': None,
|
||||||
|
'payment_user_agent': None,
|
||||||
|
'stripeJsId': None,
|
||||||
|
'api_key': None,
|
||||||
|
'setup_intent_id': None,
|
||||||
|
'setup_intent_client_secret': None,
|
||||||
|
'acct_id': None,
|
||||||
|
'hcaptcha_duration': None,
|
||||||
|
'payment_method_id': None,
|
||||||
|
'setup_intent_status': None,
|
||||||
|
'page_load_time': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
HCAPTCHA_INFO = {
|
||||||
|
'response': None,
|
||||||
|
'duration': None,
|
||||||
|
'updated_at': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
CAPTURED_MESSAGES = []
|
||||||
|
CAPTURED_MESSAGE_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
DATA_EVENTS = {
|
||||||
|
'hcaptcha_updated': threading.Event(),
|
||||||
|
'guid_updated': threading.Event(),
|
||||||
|
'muid_updated': threading.Event(),
|
||||||
|
'sid_updated': threading.Event(),
|
||||||
|
'portal_session_updated': threading.Event(),
|
||||||
|
'session_api_key_updated': threading.Event(),
|
||||||
|
'acct_id_updated': threading.Event(),
|
||||||
|
'api_key_updated': threading.Event(),
|
||||||
|
'stripe_js_id_updated': threading.Event(),
|
||||||
|
'payment_user_agent_updated': threading.Event(),
|
||||||
|
'setup_intent_updated': threading.Event(),
|
||||||
|
'payment_method_updated': threading.Event()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
BACKGROUND_THREAD_RUNNING = True
|
||||||
|
|
||||||
|
def init_browser():
|
||||||
|
co = ChromiumOptions()
|
||||||
|
co.incognito()
|
||||||
|
c = Chromium(co)
|
||||||
|
c.set.timeouts(9000)
|
||||||
|
return c, c.latest_tab
|
||||||
|
|
||||||
|
def background_data_collector(tab):
|
||||||
|
global PAYMENT_INFO, HCAPTCHA_INFO, CAPTURED_MESSAGES, DATA_EVENTS, BACKGROUND_THREAD_RUNNING
|
||||||
|
|
||||||
|
|
||||||
|
while BACKGROUND_THREAD_RUNNING:
|
||||||
|
try:
|
||||||
|
|
||||||
|
for log in tab.console.steps(timeout=1):
|
||||||
|
if log.text.startswith('捕获postMessage:'):
|
||||||
|
message_content = log.text.replace('捕获postMessage:', '', 1).strip()
|
||||||
|
try:
|
||||||
|
message_data = json.loads(message_content)
|
||||||
|
|
||||||
|
with CAPTURED_MESSAGE_LOCK:
|
||||||
|
CAPTURED_MESSAGES.append(message_data)
|
||||||
|
|
||||||
|
if (isinstance(message_data, dict) and message_data.get('type') == 'execute'
|
||||||
|
and message_data.get('channel') == 'hcaptcha-invisible'):
|
||||||
|
body = message_data.get('body', {})
|
||||||
|
if isinstance(body, dict) and 'response' in body:
|
||||||
|
HCAPTCHA_INFO['response'] = body['response']
|
||||||
|
if 'duration' in body:
|
||||||
|
HCAPTCHA_INFO['duration'] = body['duration']
|
||||||
|
HCAPTCHA_INFO['updated_at'] = time.time()
|
||||||
|
print(f"[后台线程] 获取到新的hCaptcha响应: {HCAPTCHA_INFO['response'][:30]}... 持续时间: {HCAPTCHA_INFO['duration']}")
|
||||||
|
|
||||||
|
PAYMENT_INFO['hcaptcha_response'] = HCAPTCHA_INFO['response']
|
||||||
|
PAYMENT_INFO['hcaptcha_duration'] = HCAPTCHA_INFO['duration']
|
||||||
|
|
||||||
|
DATA_EVENTS['hcaptcha_updated'].set()
|
||||||
|
|
||||||
|
if isinstance(message_data, dict) and 'originatingScript' in message_data and 'payload' in message_data:
|
||||||
|
payload = message_data.get('payload', {})
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
if 'guid' in payload and not PAYMENT_INFO['guid']:
|
||||||
|
PAYMENT_INFO['guid'] = payload['guid']
|
||||||
|
print(f"[后台线程] 提取到GUID: {payload['guid']}")
|
||||||
|
DATA_EVENTS['guid_updated'].set()
|
||||||
|
|
||||||
|
if 'muid' in payload and not PAYMENT_INFO['muid']:
|
||||||
|
PAYMENT_INFO['muid'] = payload['muid']
|
||||||
|
print(f"[后台线程] 提取到MUID: {payload['muid']}")
|
||||||
|
DATA_EVENTS['muid_updated'].set()
|
||||||
|
|
||||||
|
if 'sid' in payload and not PAYMENT_INFO['sid']:
|
||||||
|
PAYMENT_INFO['sid'] = payload['sid']
|
||||||
|
print(f"[后台线程] 提取到SID: {payload['sid']}")
|
||||||
|
DATA_EVENTS['sid_updated'].set()
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[后台线程] 数据收集出错: {e}")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def wait_for_hcaptcha(timeout=60):
|
||||||
|
print(f"等待hCaptcha响应,最多等待{timeout}秒...")
|
||||||
|
return DATA_EVENTS['hcaptcha_updated'].wait(timeout)
|
||||||
|
|
||||||
|
def wait_for_payment_info(fields, timeout=60):
|
||||||
|
if isinstance(fields, str):
|
||||||
|
fields = [fields]
|
||||||
|
|
||||||
|
print(f"等待支付信息字段: {', '.join(fields)},最多等待{timeout}秒...")
|
||||||
|
|
||||||
|
events_to_wait = []
|
||||||
|
for field in fields:
|
||||||
|
event_name = f"{field}_updated"
|
||||||
|
if event_name in DATA_EVENTS:
|
||||||
|
events_to_wait.append(DATA_EVENTS[event_name])
|
||||||
|
|
||||||
|
if not events_to_wait:
|
||||||
|
print(f"警告: 没有找到与字段 {', '.join(fields)} 对应的事件")
|
||||||
|
return False
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
all_set = True
|
||||||
|
for event in events_to_wait:
|
||||||
|
remaining_time = timeout - (time.time() - start_time)
|
||||||
|
if remaining_time <= 0:
|
||||||
|
all_set = False
|
||||||
|
break
|
||||||
|
if not event.wait(remaining_time):
|
||||||
|
all_set = False
|
||||||
|
break
|
||||||
|
|
||||||
|
return all_set
|
||||||
|
|
||||||
|
def wait_for_specific_data(check_func, timeout=60, check_interval=0.5):
|
||||||
|
print(f"等待特定条件满足,最多等待{timeout}秒...")
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
if check_func():
|
||||||
|
return True
|
||||||
|
time.sleep(check_interval)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def login_cccc(tab, email, password):
|
||||||
|
tab.get('cccccccccccc')
|
||||||
|
print(f'登录页面加载完成,准备使用账号 {email} 登录')
|
||||||
|
|
||||||
|
# 通过Google登录
|
||||||
|
tab.ele('tag:button@@text():Google').click()
|
||||||
|
tab.wait.load_start()
|
||||||
|
print('Google登录页面加载完成')
|
||||||
|
|
||||||
|
# 输入邮箱
|
||||||
|
tab.ele('#identifierId').input(email)
|
||||||
|
tab.ele('tag:button@@text():下一步').click()
|
||||||
|
print('邮箱账号输入完成')
|
||||||
|
|
||||||
|
# 输入密码
|
||||||
|
password_input = tab.ele('#password')
|
||||||
|
password_input.wait.clickable()
|
||||||
|
password_input.input(password)
|
||||||
|
tab.ele('tag:button@@text():下一步').click()
|
||||||
|
tab.wait.load_start()
|
||||||
|
print('密码输入完成')
|
||||||
|
|
||||||
|
# 点击继续按钮(如果存在)
|
||||||
|
try:
|
||||||
|
tab.ele('tag:button@@text():Continue').click()
|
||||||
|
tab.wait.load_start()
|
||||||
|
print('继续按钮点击完成')
|
||||||
|
except:
|
||||||
|
print('继续按钮不存在')
|
||||||
|
|
||||||
|
tab.wait.load_start()
|
||||||
|
print('跳转完成')
|
||||||
|
|
||||||
|
def handle_billing_page(tab: ChromiumTab, c: Chromium):
|
||||||
|
global PAYMENT_INFO, HCAPTCHA_INFO, CAPTURED_MESSAGES, DATA_EVENTS, BACKGROUND_THREAD_RUNNING
|
||||||
|
|
||||||
|
try:
|
||||||
|
for key in PAYMENT_INFO:
|
||||||
|
PAYMENT_INFO[key] = None
|
||||||
|
PAYMENT_INFO['page_load_time'] = int(time.time())
|
||||||
|
|
||||||
|
HCAPTCHA_INFO['response'] = None
|
||||||
|
HCAPTCHA_INFO['duration'] = None
|
||||||
|
HCAPTCHA_INFO['updated_at'] = 0
|
||||||
|
|
||||||
|
with CAPTURED_MESSAGE_LOCK:
|
||||||
|
CAPTURED_MESSAGES.clear()
|
||||||
|
|
||||||
|
for event in DATA_EVENTS.values():
|
||||||
|
event.clear()
|
||||||
|
|
||||||
|
BACKGROUND_THREAD_RUNNING = True
|
||||||
|
|
||||||
|
tab.listen.start([
|
||||||
|
"p/session/live_"
|
||||||
|
])
|
||||||
|
|
||||||
|
manage_button = tab.ele('tag:button@@text():Manage')
|
||||||
|
manage_button.wait.clickable()
|
||||||
|
manage_button.click()
|
||||||
|
tab.wait.load_start()
|
||||||
|
|
||||||
|
tab.wait(10)
|
||||||
|
|
||||||
|
tab.console.start()
|
||||||
|
|
||||||
|
res = tab.listen.wait(1)
|
||||||
|
redirect_url = res.url # type: ignore
|
||||||
|
response = res.response # type: ignore
|
||||||
|
data = response.raw_body
|
||||||
|
|
||||||
|
bps_match = re.search(r'portal_session_id":"(bps_[^&]+)"', data)
|
||||||
|
if bps_match:
|
||||||
|
portal_session_id = bps_match.group(1)
|
||||||
|
PAYMENT_INFO['portal_session_id'] = portal_session_id
|
||||||
|
print(f"通过正则提取到的Portal Session ID: {portal_session_id}")
|
||||||
|
DATA_EVENTS['portal_session_updated'].set()
|
||||||
|
|
||||||
|
ek_match = re.search(r'session_api_key":"(ek_live_[^&]+)"', data)
|
||||||
|
if ek_match:
|
||||||
|
session_api_key = ek_match.group(1)
|
||||||
|
PAYMENT_INFO['session_api_key'] = session_api_key
|
||||||
|
print(f"通过正则提取到的Session API Key: {session_api_key}")
|
||||||
|
DATA_EVENTS['session_api_key_updated'].set()
|
||||||
|
|
||||||
|
acct_match = re.search(r'id":"(acct_[^&]+)"', data)
|
||||||
|
if acct_match:
|
||||||
|
acct_id = acct_match.group(1)
|
||||||
|
PAYMENT_INFO['acct_id'] = acct_id
|
||||||
|
DATA_EVENTS['acct_id_updated'].set()
|
||||||
|
|
||||||
|
js_script = '''
|
||||||
|
(function() {
|
||||||
|
let messages = [];
|
||||||
|
|
||||||
|
function captureMessage(event) {
|
||||||
|
console.log('捕获postMessage:', event.data);
|
||||||
|
messages.push({
|
||||||
|
source: event.source ? 'iframe' : 'window',
|
||||||
|
origin: event.origin,
|
||||||
|
data: event.data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', captureMessage, false);
|
||||||
|
})();
|
||||||
|
'''
|
||||||
|
|
||||||
|
tab.run_js(js_script, as_expr=True)
|
||||||
|
|
||||||
|
js_script = '''
|
||||||
|
(function() {
|
||||||
|
let messages = [];
|
||||||
|
|
||||||
|
function captureMessage(event) {
|
||||||
|
console.log('捕获postMessage:', event.data);
|
||||||
|
messages.push({
|
||||||
|
source: event.source ? 'iframe' : 'window',
|
||||||
|
origin: event.origin,
|
||||||
|
data: event.data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', captureMessage, false);
|
||||||
|
})();
|
||||||
|
'''
|
||||||
|
|
||||||
|
tab.run_js(js_script, as_expr=True)
|
||||||
|
|
||||||
|
data_thread = threading.Thread(target=background_data_collector, args=(tab,), daemon=True)
|
||||||
|
data_thread.start()
|
||||||
|
|
||||||
|
add_payment_button = tab.ele('tag:a@@text():添加支付方式')
|
||||||
|
add_payment_button.wait.clickable()
|
||||||
|
add_payment_button.click()
|
||||||
|
tab.wait.load_start()
|
||||||
|
|
||||||
|
print("等待收集消息...")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
def get_hcaptcha_info():
|
||||||
|
return HCAPTCHA_INFO
|
||||||
|
|
||||||
|
try:
|
||||||
|
if CAPTURED_MESSAGE_LOCK.acquire(timeout=2): # 最多等待2秒
|
||||||
|
try:
|
||||||
|
collected_messages = list(CAPTURED_MESSAGES)
|
||||||
|
print(f"已收集到 {len(collected_messages)} 条消息")
|
||||||
|
finally:
|
||||||
|
CAPTURED_MESSAGE_LOCK.release()
|
||||||
|
else:
|
||||||
|
print("警告:获取消息超时,继续执行")
|
||||||
|
collected_messages = []
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取消息时出错: {e}")
|
||||||
|
collected_messages = []
|
||||||
|
|
||||||
|
if not all([PAYMENT_INFO['guid'], PAYMENT_INFO['muid'], PAYMENT_INFO['sid']]):
|
||||||
|
for message_data in collected_messages:
|
||||||
|
if isinstance(message_data, dict) and 'originatingScript' in message_data and 'payload' in message_data:
|
||||||
|
payload = message_data.get('payload', {})
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
if 'guid' in payload and not PAYMENT_INFO['guid']:
|
||||||
|
PAYMENT_INFO['guid'] = payload['guid']
|
||||||
|
print(f"提取到GUID: {payload['guid']}")
|
||||||
|
DATA_EVENTS['guid_updated'].set()
|
||||||
|
if 'muid' in payload and not PAYMENT_INFO['muid']:
|
||||||
|
PAYMENT_INFO['muid'] = payload['muid']
|
||||||
|
print(f"提取到MUID: {payload['muid']}")
|
||||||
|
DATA_EVENTS['muid_updated'].set()
|
||||||
|
if 'sid' in payload and not PAYMENT_INFO['sid']:
|
||||||
|
PAYMENT_INFO['sid'] = payload['sid']
|
||||||
|
print(f"提取到SID: {payload['sid']}")
|
||||||
|
DATA_EVENTS['sid_updated'].set()
|
||||||
|
|
||||||
|
frames = tab.get_frames()
|
||||||
|
for frame in frames: # type: ignore
|
||||||
|
frame_url =frame.url
|
||||||
|
print(f"Frame URL: {frame_url}")
|
||||||
|
|
||||||
|
if 'apiKey' in frame_url:
|
||||||
|
api_key_match = re.search(r'apiKey]=([^&]+)', frame_url)
|
||||||
|
if api_key_match:
|
||||||
|
api_key = api_key_match.group(1)
|
||||||
|
PAYMENT_INFO['api_key'] = api_key
|
||||||
|
DATA_EVENTS['api_key_updated'].set()
|
||||||
|
|
||||||
|
if 'link-auth-modal-inner' in frame_url:
|
||||||
|
stripe_js_id_match = re.search(r'stripeJsId=([^&]+)', frame_url)
|
||||||
|
if stripe_js_id_match:
|
||||||
|
stripe_js_id = stripe_js_id_match.group(1)
|
||||||
|
print(f"提取到的Stripe JS ID: {stripe_js_id}")
|
||||||
|
PAYMENT_INFO['stripeJsId'] = stripe_js_id
|
||||||
|
DATA_EVENTS['stripe_js_id_updated'].set()
|
||||||
|
|
||||||
|
if PAYMENT_INFO['api_key'] is None:
|
||||||
|
print('无法从URL中提取apiKey')
|
||||||
|
|
||||||
|
if 'stripeJsId' not in PAYMENT_INFO or PAYMENT_INFO['stripeJsId'] is None:
|
||||||
|
print('无法从URL中提取stripeJsId')
|
||||||
|
|
||||||
|
|
||||||
|
tmp_tab: MixTab = c.new_tab()
|
||||||
|
res = tmp_tab.get("https://js.stripe.com/v3/.deploy_status_henson.json")
|
||||||
|
print(res, tmp_tab.json, tmp_tab.url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
deploy_status = tab.json
|
||||||
|
if deploy_status and 'deployedRevisions' in deploy_status and len(deploy_status['deployedRevisions']) > 0:
|
||||||
|
first_revision = deploy_status['deployedRevisions'][0]
|
||||||
|
revision_prefix = first_revision[:10]
|
||||||
|
payment_user_agent = f"stripe.js/{revision_prefix}; stripe-js-v3/{revision_prefix}; payment-element"
|
||||||
|
PAYMENT_INFO['payment_user_agent'] = payment_user_agent
|
||||||
|
print(f"Payment User Agent: {payment_user_agent}")
|
||||||
|
DATA_EVENTS['payment_user_agent_updated'].set()
|
||||||
|
else:
|
||||||
|
print("无法获取deployedRevisions信息")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"提取deployedRevisions信息时出错: {e}")
|
||||||
|
|
||||||
|
print("\n提取的支付信息:")
|
||||||
|
print(f"GUID: {PAYMENT_INFO['guid']}")
|
||||||
|
print(f"MUID: {PAYMENT_INFO['muid']}")
|
||||||
|
print(f"SID: {PAYMENT_INFO['sid']}")
|
||||||
|
print(f"hCaptcha响应: {PAYMENT_INFO['hcaptcha_response']}")
|
||||||
|
print(f"API Key: {PAYMENT_INFO['api_key']}")
|
||||||
|
print(f"Payment User Agent: {PAYMENT_INFO['payment_user_agent']}")
|
||||||
|
print(f"Stripe JS ID: {PAYMENT_INFO['stripeJsId']}")
|
||||||
|
print(f"Portal Session ID: {PAYMENT_INFO['portal_session_id']}")
|
||||||
|
print(f"Session API Key: {PAYMENT_INFO['session_api_key']}")
|
||||||
|
print(f"Account ID: {PAYMENT_INFO['acct_id']}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
portal_session_id = PAYMENT_INFO['portal_session_id']
|
||||||
|
session_api_key = PAYMENT_INFO['session_api_key']
|
||||||
|
|
||||||
|
setup_intent_url = f"https://billing.stripe.com/v1/billing_portal/sessions/{portal_session_id}/setup_intents/"
|
||||||
|
query_params = {"include_only[]": ["id", "object", "client_secret", "payment_method_types"]}
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {session_api_key}",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"stripe-account": PAYMENT_INFO['acct_id'],
|
||||||
|
"stripe-livemode": "true",
|
||||||
|
"stripe-version": "2025-03-01.dashboard"
|
||||||
|
}
|
||||||
|
|
||||||
|
latest_info = get_hcaptcha_info()
|
||||||
|
if latest_info['response']:
|
||||||
|
PAYMENT_INFO['hcaptcha_response'] = latest_info['response']
|
||||||
|
PAYMENT_INFO['hcaptcha_duration'] = latest_info['duration']
|
||||||
|
print(f"使用最新的hCaptcha响应,更新于 {int(time.time() - latest_info['updated_at'])} 秒前")
|
||||||
|
|
||||||
|
response = tab.post( # type: ignore
|
||||||
|
url=setup_intent_url,
|
||||||
|
params=query_params,
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
setup_intent = response.json()
|
||||||
|
if setup_intent and 'id' in setup_intent:
|
||||||
|
setup_intent_id = setup_intent['id']
|
||||||
|
setup_intent_client_secret = setup_intent.get('client_secret')
|
||||||
|
PAYMENT_INFO['setup_intent_id'] = setup_intent_id
|
||||||
|
PAYMENT_INFO['setup_intent_client_secret'] = setup_intent_client_secret
|
||||||
|
DATA_EVENTS['setup_intent_updated'].set()
|
||||||
|
|
||||||
|
print(f"\n成功获取Setup Intent:")
|
||||||
|
print(f"Setup Intent ID: {setup_intent_id}")
|
||||||
|
print(f"Setup Intent Client Secret: {setup_intent_client_secret}")
|
||||||
|
else:
|
||||||
|
print(f"\n无法从响应中获取setup_intent信息")
|
||||||
|
print(f"响应内容: {tab.json}")
|
||||||
|
else:
|
||||||
|
print(f"\n获取setup_intent失败,状态码: {response.status_code}")
|
||||||
|
print(f"响应内容: {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n获取setup_intent时出错: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
latest_info = get_hcaptcha_info()
|
||||||
|
if latest_info['response']:
|
||||||
|
PAYMENT_INFO['hcaptcha_response'] = latest_info['response']
|
||||||
|
PAYMENT_INFO['hcaptcha_duration'] = latest_info['duration']
|
||||||
|
print(f"使用最新的hCaptcha响应,更新于 {int(time.time() - latest_info['updated_at'])} 秒前")
|
||||||
|
|
||||||
|
time_on_page = int(time.time()) - PAYMENT_INFO['page_load_time']
|
||||||
|
|
||||||
|
card_data = {
|
||||||
|
"type": "card",
|
||||||
|
"card[number]": "5154 cccccccccccccc",
|
||||||
|
"card[cvc]": "ccc",
|
||||||
|
"card[exp_year]": "cc",
|
||||||
|
"card[exp_month]": "cc",
|
||||||
|
"allow_redisplay": "unspecified",
|
||||||
|
"billing_details[address][country]": "cc",
|
||||||
|
"pasted_fields": "number",
|
||||||
|
"payment_user_agent": PAYMENT_INFO.get('payment_user_agent'),
|
||||||
|
"referrer": "https://billing.stripe.com",
|
||||||
|
"time_on_page": str(time_on_page),
|
||||||
|
"client_attribution_metadata[client_session_id]": PAYMENT_INFO.get('stripeJsId'),
|
||||||
|
"client_attribution_metadata[merchant_integration_source]": "elements",
|
||||||
|
"client_attribution_metadata[merchant_integration_subtype]": "payment-element",
|
||||||
|
"client_attribution_metadata[merchant_integration_version]": "2021",
|
||||||
|
"client_attribution_metadata[payment_intent_creation_flow]": "standard",
|
||||||
|
"client_attribution_metadata[payment_method_selection_flow]": "merchant_specified",
|
||||||
|
"guid": PAYMENT_INFO.get('guid'),
|
||||||
|
"muid": PAYMENT_INFO.get('muid'),
|
||||||
|
"sid": PAYMENT_INFO.get('sid'),
|
||||||
|
"key": PAYMENT_INFO.get('api_key'),
|
||||||
|
}
|
||||||
|
|
||||||
|
if PAYMENT_INFO.get('hcaptcha_response'):
|
||||||
|
card_data["radar_options[hcaptcha_token]"] = PAYMENT_INFO.get('hcaptcha_response')
|
||||||
|
|
||||||
|
payment_methods_url = "https://api.stripe.com/v1/payment_methods"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\n正在提交卡片信息...")
|
||||||
|
card_response = tab.post( # type: ignore
|
||||||
|
url=payment_methods_url,
|
||||||
|
data=card_data,
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if card_response.status_code == 200:
|
||||||
|
payment_method = card_response.json()
|
||||||
|
if payment_method and 'id' in payment_method:
|
||||||
|
payment_method_id = payment_method['id']
|
||||||
|
PAYMENT_INFO['payment_method_id'] = payment_method_id
|
||||||
|
DATA_EVENTS['payment_method_updated'].set()
|
||||||
|
|
||||||
|
print(f"\n成功创建支付方式:")
|
||||||
|
print(f"Payment Method ID: {payment_method_id}")
|
||||||
|
print(f"Card Brand: {payment_method.get('card', {}).get('brand')}")
|
||||||
|
print(f"Last4: {payment_method.get('card', {}).get('last4')}")
|
||||||
|
else:
|
||||||
|
print(f"\n无法从响应中获取payment_method信息")
|
||||||
|
print(f"响应内容: {tab.json}")
|
||||||
|
else:
|
||||||
|
print(f"\n提交卡片信息失败,状态码: {card_response.status_code}")
|
||||||
|
print(f"响应内容: {card_response.text}")
|
||||||
|
|
||||||
|
if 'payment_method_id' in PAYMENT_INFO and 'setup_intent_id' in PAYMENT_INFO:
|
||||||
|
latest_info = get_hcaptcha_info()
|
||||||
|
if latest_info['response']:
|
||||||
|
PAYMENT_INFO['hcaptcha_response'] = latest_info['response']
|
||||||
|
PAYMENT_INFO['hcaptcha_duration'] = latest_info['duration']
|
||||||
|
print(f"使用最新的hCaptcha响应,更新于 {int(time.time() - latest_info['updated_at'])} 秒前")
|
||||||
|
|
||||||
|
confirm_url = f"https://billing.stripe.com/v1/billing_portal/sessions/{PAYMENT_INFO['portal_session_id']}/setup_intents/{PAYMENT_INFO['setup_intent_id']}/confirm"
|
||||||
|
confirm_params = {"include_only[]": ["id", "status", "client_secret", "payment_method"]}
|
||||||
|
confirm_data = {
|
||||||
|
"payment_method": PAYMENT_INFO['payment_method_id'],
|
||||||
|
"return_url": f"{redirect_url}/payment-methods/return?in_flow=false&make_customer_default=true"
|
||||||
|
}
|
||||||
|
|
||||||
|
if PAYMENT_INFO.get('hcaptcha_response'):
|
||||||
|
confirm_data["passive_captcha_token"] = PAYMENT_INFO.get('hcaptcha_response')
|
||||||
|
|
||||||
|
confirm_headers = {
|
||||||
|
"Authorization": f"Bearer {PAYMENT_INFO['session_api_key']}",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"stripe-account": PAYMENT_INFO['acct_id'],
|
||||||
|
"stripe-livemode": "true",
|
||||||
|
"stripe-version": "2025-03-01.dashboard"
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\n正在确认setup_intent...")
|
||||||
|
confirm_response = tab.post( # type: ignore
|
||||||
|
url=confirm_url,
|
||||||
|
params=confirm_params,
|
||||||
|
data=confirm_data,
|
||||||
|
headers=confirm_headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if confirm_response.status_code == 200:
|
||||||
|
confirm_result = confirm_response.json()
|
||||||
|
print(f"\n确认结果: {confirm_result.get('status')}")
|
||||||
|
PAYMENT_INFO['setup_intent_status'] = confirm_result.get('status')
|
||||||
|
DATA_EVENTS['setup_intent_updated'].set()
|
||||||
|
else:
|
||||||
|
print(f"\n确认setup_intent失败,状态码: {confirm_response.status_code}")
|
||||||
|
print(f"响应内容: {confirm_response.text}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n提交卡片信息或确认setup_intent时出错: {e}")
|
||||||
|
|
||||||
|
return PAYMENT_INFO
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f'添加支付方式按钮不存在: {e}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# 辅助函数,用于等待特定数据
|
||||||
|
def wait_for_data(data_type, timeout=60):
|
||||||
|
if data_type in DATA_EVENTS:
|
||||||
|
print(f"等待{data_type}数据,最多等待{timeout}秒...")
|
||||||
|
return DATA_EVENTS[data_type].wait(timeout)
|
||||||
|
else:
|
||||||
|
print(f"警告: 未知的数据类型 {data_type}")
|
||||||
|
return False
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,160 @@
|
|||||||
|
import requests
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
|
|
||||||
|
BASE_URL = "https://api.mail.tm"
|
||||||
|
|
||||||
|
|
||||||
|
def _random_string(length=12):
|
||||||
|
return "".join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(length))
|
||||||
|
|
||||||
|
|
||||||
|
def get_email_and_token():
|
||||||
|
try:
|
||||||
|
# 1. 获取可用域名
|
||||||
|
r = requests.get(f"{BASE_URL}/domains", timeout=30)
|
||||||
|
r.raise_for_status()
|
||||||
|
domains = r.json().get("hydra:member", [])
|
||||||
|
if not domains:
|
||||||
|
print("[Error] Mail.tm 没有可用域名")
|
||||||
|
return None, None
|
||||||
|
domain = domains[0]["domain"]
|
||||||
|
|
||||||
|
# 2. 生成随机邮箱和密码
|
||||||
|
local_part = _random_string(10)
|
||||||
|
email = f"{local_part}@{domain}"
|
||||||
|
password = _random_string(16)
|
||||||
|
|
||||||
|
# 3. 创建账户
|
||||||
|
r = requests.post(
|
||||||
|
f"{BASE_URL}/accounts",
|
||||||
|
json={"address": email, "password": password},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
# 4. 获取 token
|
||||||
|
r = requests.post(
|
||||||
|
f"{BASE_URL}/token",
|
||||||
|
json={"address": email, "password": password},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
token = r.json().get("token")
|
||||||
|
if not token:
|
||||||
|
print("[Error] 无法获取 Mail.tm token")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
print(f"[*] 已获取临时邮箱: {email}")
|
||||||
|
return email, token
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Error] 获取邮箱失败: {e}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_code(subject, text, html_text=""):
|
||||||
|
"""从邮件文本中提取验证码。优先匹配 xAI/常见服务的验证码格式。"""
|
||||||
|
text = text or ""
|
||||||
|
html_text = html_text or ""
|
||||||
|
subject = subject or ""
|
||||||
|
full_text = f"{subject}\n{text}\n{html_text}"
|
||||||
|
|
||||||
|
# 1. xAI 主题自带验证码格式: EB5-XJA xAI confirmation code
|
||||||
|
m = re.search(r"\b([A-Z0-9]{3}-[A-Z0-9]{3})\b.*confirmation\s*code", subject, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
return m.group(1).upper()
|
||||||
|
|
||||||
|
# 2. 通用 xAI/OpenAI 验证码格式(字母数字混合)
|
||||||
|
patterns = [
|
||||||
|
# xAI 正文: code below to validate ... EB5-XJA
|
||||||
|
r"code\s+below\s+to\s+validate.*?\n\s*([A-Z0-9]{3}-[A-Z0-9]{3})\s*\n",
|
||||||
|
# 通用字母数字验证码: ABC-123, 123-ABC, A1B-2C3
|
||||||
|
r"\b([A-Z0-9]{3}-[A-Z0-9]{3})\b",
|
||||||
|
# verification code is 123456
|
||||||
|
r"(?:verification\s*code|code\s*is)[:\s]+(\d{6})",
|
||||||
|
# 中文验证码
|
||||||
|
r"(?:验证码|代码|确认码)[:\s为]+(\d{6})",
|
||||||
|
# code: 123456
|
||||||
|
r"(?:code|验证码)[:\s]+(\d{6})",
|
||||||
|
# 6位数字带上下文
|
||||||
|
r"\b(\d{6})\b(?:\s*(?:is your|作为您的)\s*(?:verification\s*code|code|验证码))?",
|
||||||
|
]
|
||||||
|
|
||||||
|
for pat in patterns:
|
||||||
|
m = re.search(pat, full_text, re.IGNORECASE | re.DOTALL)
|
||||||
|
if m:
|
||||||
|
code = m.group(1)
|
||||||
|
if code:
|
||||||
|
return code.upper() if "-" in code else code
|
||||||
|
|
||||||
|
# 3. 兜底:仅在纯文本中匹配 6 位数字,避开 HTML 颜色代码
|
||||||
|
if text:
|
||||||
|
for m in re.finditer(r"(?<!#)\b(\d{6})\b", text):
|
||||||
|
code = m.group(1)
|
||||||
|
if not code.startswith("20"):
|
||||||
|
return code
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_verification_email(subject, sender):
|
||||||
|
"""判断是否为验证码邮件。"""
|
||||||
|
keywords = ["verification", "code", "验证码", "确认码", "xai", "x.ai", "openai", "grok"]
|
||||||
|
text = f"{subject} {sender}".lower()
|
||||||
|
return any(k in text for k in keywords)
|
||||||
|
|
||||||
|
|
||||||
|
def get_oai_code(token, email):
|
||||||
|
try:
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
deadline = time.time() + 180 # 最多轮询 180 秒
|
||||||
|
|
||||||
|
while time.time() < deadline:
|
||||||
|
r = requests.get(f"{BASE_URL}/messages", headers=headers, timeout=30)
|
||||||
|
r.raise_for_status()
|
||||||
|
messages = r.json().get("hydra:member", [])
|
||||||
|
|
||||||
|
# 按创建时间倒序,优先处理最新邮件
|
||||||
|
messages.sort(key=lambda m: m.get("createdAt", ""), reverse=True)
|
||||||
|
|
||||||
|
# 优先检查看起来像验证码的邮件
|
||||||
|
verification_msgs = [m for m in messages if _is_verification_email(m.get("subject", ""), "")]
|
||||||
|
other_msgs = [m for m in messages if m not in verification_msgs]
|
||||||
|
ordered_msgs = verification_msgs + other_msgs
|
||||||
|
|
||||||
|
for msg in ordered_msgs:
|
||||||
|
msg_id = msg.get("id")
|
||||||
|
if not msg_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 获取邮件详情
|
||||||
|
r = requests.get(f"{BASE_URL}/messages/{msg_id}", headers=headers, timeout=30)
|
||||||
|
r.raise_for_status()
|
||||||
|
detail = r.json()
|
||||||
|
|
||||||
|
subject = detail.get("subject", "")
|
||||||
|
text = detail.get("text", "")
|
||||||
|
html = detail.get("html", [])
|
||||||
|
html_content = " ".join(html) if isinstance(html, list) else str(html)
|
||||||
|
sender = detail.get("from", {}).get("address", "")
|
||||||
|
|
||||||
|
code = _extract_code(subject, text, html_content)
|
||||||
|
if code:
|
||||||
|
# xAI 验证码为 WVB-8OE 格式,OTP 输入框通常只需 6 位字母数字,去掉连字符
|
||||||
|
if "-" in code:
|
||||||
|
code = code.replace("-", "")
|
||||||
|
print(f"[*] 获取到验证码: {code}")
|
||||||
|
# 标记为已读,避免重复提取
|
||||||
|
try:
|
||||||
|
requests.patch(f"{BASE_URL}/messages/{msg_id}", headers={**headers, "Content-Type": "application/merge-patch+json"}, json={"seen": True}, timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return code
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
print("[Error] 轮询超时,未获取到验证码")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Error] 获取验证码失败: {e}")
|
||||||
|
return None
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
async function main() {
|
||||||
|
const { connect } = await import('puppeteer-real-browser');
|
||||||
|
|
||||||
|
// not needed anymore cuz library already does it automatically 😉👌
|
||||||
|
//const EXTENSION_PATH = `${__dirname}/turnstilePatch/`;
|
||||||
|
|
||||||
|
const { page, browser } = await connect({
|
||||||
|
/*
|
||||||
|
args: [
|
||||||
|
`--disable-extensions-except=${EXTENSION_PATH}`,
|
||||||
|
`--load-extension=${EXTENSION_PATH}`
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
turnstile: true,
|
||||||
|
});
|
||||||
|
page.goto('https://nopecha.com/demo/cloudflare');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# CDP bug MouseEvent .screenX .screenY patcher
|
||||||
|
### supports both headless and headful
|
||||||
|
|
||||||
|
## bug that this fixes: https://issues.chromium.org/issues/40280325
|
||||||
|
When the CDP command `Input.dispatchMouseEvent` is ran, the MouseEvent/PointerEvent created will have a "fake" property for .screenX and .screenY (they will be same as the .x and .y properties respectively). Cloudflare Turnstile is able to detect this and will mark you as a bot, and you won't be able to get past (Interstitial) Turnstile.
|
||||||
|
|
||||||
|
This extension fixes this by providing fake values for .screenX and .screenY. One downside of this is that now even real clicks will use fake values. However manually clicking in an automated browser is mostly non-existant so its fine, and .screenX and .screenY are rarely used anyways, so it should be fine. Let me know if it breaks something and I'll see if I can fix it.
|
||||||
|
|
||||||
|
TL;DR: Chrome has a bug allowing fake clicks to be detected. This extension fixes those detections so you can continue scraping.
|
||||||
|
|
||||||
|
## how to use
|
||||||
|
Load the extension in [./turnstilePatch/](/turnstilePatch/)
|
||||||
|
|
||||||
|
tested libraries:
|
||||||
|
- [DrissionPage](https://github.com/g1879/DrissionPage) ([example](/DrissionPage_example.py))
|
||||||
|
- ~~[puppeteer-real-browser](https://github.com/zfcsoftware/puppeteer-real-browser) ([example](/puppeteer-real-browser_example.js))~~ (already directly implemented in library)
|
||||||
|
|
||||||
|
## Docker:
|
||||||
|
- [Dockerfile](/Dockerfile)
|
||||||
|
- [Docker code version](/DrissionPage_example_docker.py)
|
||||||
|
|
||||||
|
## status
|
||||||
|
Gets past Cloudflare Turnstile
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## for the memes
|
||||||
|

|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DrissionPage==4.1.0.9
|
||||||
|
PyVirtualDisplay==3.0
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Turnstile Patcher",
|
||||||
|
"version": "2.1",
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"js": [
|
||||||
|
"./script.js"
|
||||||
|
],
|
||||||
|
"matches": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
"run_at": "document_start",
|
||||||
|
"all_frames": true,
|
||||||
|
"world": "MAIN"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
function getRandomInt(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// old method wouldn't work on 4k screens
|
||||||
|
|
||||||
|
let screenX = getRandomInt(800, 1200);
|
||||||
|
let screenY = getRandomInt(400, 600);
|
||||||
|
|
||||||
|
Object.defineProperty(MouseEvent.prototype, 'screenX', { value: screenX });
|
||||||
|
|
||||||
|
Object.defineProperty(MouseEvent.prototype, 'screenY', { value: screenY });
|
||||||
Reference in New Issue
Block a user