'use strict'; /** * Stripe hCaptcha 全流程 Runner * * 流程: * 1. 解析 Stripe Checkout URL → pk_live + session_id * 2. 调用 Stripe init API → sitekey + rqdata * 3. hCaptcha 求解 → token * * 使用: * node run_solver.js [stripe_checkout_url] * node run_solver.js # 使用默认 URL 测试 */ const { HCaptchaSolver } = require('./src/hcaptcha_solver'); const { Logger } = require('./src/utils/logger'); const logger = new Logger('run_solver'); // ══════════════════════════════════════════════════════════════ // 步骤 1: 从 Stripe Checkout URL 提取参数 // ══════════════════════════════════════════════════════════════ /** * 解析 Stripe Checkout URL * 格式: https://pay.xxx.com/c/pay/cs_live_XXXX#pk_live_XXXX... * https://checkout.stripe.com/c/pay/cs_live_XXXX#encoded_blob * * @param {string} url - Stripe Checkout URL * @returns {{ sessionId: string, pkLive: string }} */ function parseStripeUrl(url) { logger.info('=== 开始获取 Stripe Checkout 参数 ==='); logger.info(`URL: ${url.substring(0, 80)}...`); // [步骤 1] 从 hash 解码 pk_live logger.info('[步骤 1] 从 URL hash 解码 pk_live...'); const hashPart = url.split('#')[1] || ''; let pkLive = ''; if (hashPart) { // hash 直接是 pk_live_XXXX / pk_test_XXXX if (hashPart.startsWith('pk_live_') || hashPart.startsWith('pk_test_')) { pkLive = hashPart; } else { // 尝试 base64 JSON 解码 try { const decoded = Buffer.from(hashPart, 'base64').toString(); const data = JSON.parse(decoded); pkLive = data.pk || data.key || ''; } catch { // hash 是 Stripe 自定义编码 blob,不是 pk_live logger.info(' hash 不是 pk_live 格式,需要从页面提取'); } } } if (pkLive) { logger.info(` 提取到 pk_live: ${pkLive.substring(0, 50)}...`); } // [步骤 2] 从路径提取 session_id const urlObj = new URL(url.split('#')[0]); const pathParts = urlObj.pathname.split('/'); const sessionId = pathParts.find(p => p.startsWith('cs_live_') || p.startsWith('cs_test_')) || ''; logger.info(`[步骤 2] 提取到 Session ID: ${sessionId}`); return { sessionId, pkLive }; } /** * 从 Stripe Checkout 页面 HTML 中提取 pk_live * Stripe 会把 publishable key 嵌入页面 JS/JSON 中 */ async function fetchPkFromPage(url) { logger.info('[步骤 1b] 从 Checkout 页面提取 pk_live...'); const resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', }, }); if (!resp.ok) { throw new Error(`获取 Checkout 页面失败 (${resp.status})`); } const html = await resp.text(); // 多种模式匹配 pk_live / pk_test const patterns = [ /["'](pk_live_[A-Za-z0-9]+)["']/, /["'](pk_test_[A-Za-z0-9]+)["']/, /publishableKey["']\s*[:=]\s*["'](pk_(?:live|test)_[A-Za-z0-9]+)["']/, /apiKey["']\s*[:=]\s*["'](pk_(?:live|test)_[A-Za-z0-9]+)["']/, /key["']\s*[:=]\s*["'](pk_(?:live|test)_[A-Za-z0-9]+)["']/, ]; for (const pat of patterns) { const m = html.match(pat); if (m) { logger.info(` 从页面提取到 pk_live: ${m[1].substring(0, 50)}...`); return m[1]; } } throw new Error('无法从 Checkout 页面提取 pk_live,请用 --pk 手动指定'); } // ══════════════════════════════════════════════════════════════ // 步骤 2: 调用 Stripe Init API 获取 hCaptcha 参数 // ══════════════════════════════════════════════════════════════ async function fetchStripeParams(sessionId, pkLive) { const initUrl = `https://api.stripe.com/v1/payment_pages/${sessionId}/init`; logger.info(`[步骤 3] 正在调用 Init API: ${initUrl.substring(0, 80)}...`); const resp = await fetch(initUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${pkLive}`, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'Origin': 'https://checkout.stripe.com', 'Referer': 'https://checkout.stripe.com/', }, body: new URLSearchParams({ 'key': pkLive, }), }); logger.info(`HTTP POST ${initUrl.substring(0, 80)}... → ${resp.status}`); if (!resp.ok) { const text = await resp.text(); throw new Error(`Stripe Init API 失败 (${resp.status}): ${text.substring(0, 200)}`); } const data = await resp.json(); // 从返回数据中提取 hCaptcha 相关字段 // Stripe 返回的 JSON 结构中通常包含 hcaptcha_site_key 和 hcaptcha_rqdata let siteKey = ''; let rqdata = ''; let host = 'b.stripecdn.com'; // 递归搜索 JSON 中的 hcaptcha 字段 const findHcaptchaParams = (obj, path = '') => { if (!obj || typeof obj !== 'object') return; for (const [k, v] of Object.entries(obj)) { const key = k.toLowerCase(); if (key.includes('hcaptcha_site_key') || key === 'site_key' || key === 'sitekey') { if (typeof v === 'string' && v.length > 10) siteKey = v; } if (key.includes('hcaptcha_rqdata') || key === 'rqdata') { if (typeof v === 'string' && v.length > 10) rqdata = v; } if (key === 'host' && typeof v === 'string' && v.includes('.')) { host = v; } if (typeof v === 'object' && v !== null) { findHcaptchaParams(v, `${path}.${k}`); } } }; findHcaptchaParams(data); // Stripe 常见的返回路径 if (!siteKey && data.hcaptcha_site_key) siteKey = data.hcaptcha_site_key; if (!siteKey && data.captcha?.hcaptcha?.site_key) siteKey = data.captcha.hcaptcha.site_key; if (!rqdata && data.hcaptcha_rqdata) rqdata = data.hcaptcha_rqdata; if (!rqdata && data.captcha?.hcaptcha?.rqdata) rqdata = data.captcha.hcaptcha.rqdata; logger.info(` 成功获取 rqdata: ${rqdata ? rqdata.substring(0, 50) + '...' : '(空)'}`); logger.info(` 成功获取 site_key: ${siteKey}`); logger.info('=== Stripe 参数获取完成 ==='); return { siteKey, rqdata, host }; } // ══════════════════════════════════════════════════════════════ // 步骤 3: hCaptcha 求解 // ══════════════════════════════════════════════════════════════ async function solveHCaptcha(siteKey, host, rqdata) { console.log(''); console.log('=== 开始求解 hCaptcha ==='); console.log(`Host: ${host}`); console.log(`Sitekey: ${siteKey}`); console.log(''); const solver = new HCaptchaSolver({ sitekey: siteKey, host, rqdata, }); return solver.solve(); } // ══════════════════════════════════════════════════════════════ // 直接用 sitekey 测试(跳过 Stripe 部分) // ══════════════════════════════════════════════════════════════ async function directSolve(sitekey, host, rqdata) { console.log(''); console.log('=== 直接模式 (跳过 Stripe) ==='); console.log(`Sitekey: ${sitekey}`); console.log(`Host: ${host}`); if (rqdata) console.log(`Rqdata: ${rqdata.substring(0, 50)}...`); console.log(''); return solveHCaptcha(sitekey, host, rqdata); } // ══════════════════════════════════════════════════════════════ // 主入口 // ══════════════════════════════════════════════════════════════ async function main() { const args = process.argv.slice(2); // 配置项(可按需修改) const CONFIG = { // Stripe Checkout URL(测试用) stripeUrl: 'https://pay.verdent.ai/c/pay/cs_live_a1H5uyD1bkpXKyqaw0BXzwzGrdzTngoNXBO6ejdyvCmswD9D6Cqzy7URwB#pk_live_51S5juuHIX9Hc8tITIZnW34rV6PJhIzl66WgEZ8kLv', // 手动指定 pk_live(当 URL hash 不含 pk_live 时使用) pkOverride: '', // 直接模式参数(跳过 Stripe API 调用,直接测试 hCaptcha) direct: { sitekey: 'ec637546-e9b8-447a-ab81-b5fb6d228ab8', host: 'b.stripecdn.com', rqdata: '', // 留空则不附加 rqdata }, }; let mode = 'direct'; // 默认直接模式 let stripeUrl = CONFIG.stripeUrl; // 解析命令行参数 for (let i = 0; i < args.length; i++) { if (args[i] === '--stripe' || args[i] === '-s') { mode = 'stripe'; if (args[i + 1] && !args[i + 1].startsWith('-')) { stripeUrl = args[++i]; } } else if (args[i] === '--direct' || args[i] === '-d') { mode = 'direct'; } else if (args[i] === '--sitekey') { CONFIG.direct.sitekey = args[++i]; } else if (args[i] === '--host') { CONFIG.direct.host = args[++i]; } else if (args[i] === '--rqdata') { CONFIG.direct.rqdata = args[++i]; } else if (args[i] === '--pk') { CONFIG.pkOverride = args[++i]; } else if (args[i] === '--help' || args[i] === '-h') { console.log(` Stripe hCaptcha Solver Runner 用法: node run_solver.js 直接模式 (默认 sitekey) node run_solver.js --direct --sitekey KEY --host H 指定参数直接模式 node run_solver.js --stripe [URL] Stripe 全流程模式 node run_solver.js --stripe https://pay.xxx.com/... 指定 Stripe URL 选项: --direct, -d 直接模式 (跳过 Stripe, 默认) --stripe, -s Stripe 全流程模式 --sitekey KEY hCaptcha sitekey --host HOST hCaptcha host --rqdata DATA 附加 rqdata --pk KEY 手动指定 pk_live (当 URL hash 里没有时) --help, -h 显示帮助 `); process.exit(0); } else if (args[i].startsWith('http')) { mode = 'stripe'; stripeUrl = args[i]; } } console.log(`\n${'═'.repeat(60)}`); console.log(' Stripe hCaptcha Solver Runner'); console.log(`${'═'.repeat(60)}\n`); let result; if (mode === 'stripe') { // ── 完整 Stripe 流程 ── const { sessionId, pkLive } = parseStripeUrl(stripeUrl); if (!sessionId) { logger.error('无法从 URL 提取 sessionId'); process.exit(1); } // pk_live 优先级: --pk 手动指定 > URL hash 提取 > 从页面自动抓取 let pk = CONFIG.pkOverride || pkLive; if (!pk) { logger.info('URL hash 中未找到 pk_live,尝试从 Checkout 页面提取...'); pk = await fetchPkFromPage(stripeUrl); } const { siteKey, rqdata, host } = await fetchStripeParams(sessionId, pk); if (!siteKey) { logger.error('无法从 Stripe 获取 hCaptcha sitekey'); process.exit(1); } result = await solveHCaptcha(siteKey, host, rqdata); } else { // ── 直接测试模式 ── result = await directSolve( CONFIG.direct.sitekey, CONFIG.direct.host, CONFIG.direct.rqdata, ); } // ── 输出结果 ── console.log(''); console.log('═'.repeat(60)); if (result.success) { console.log(' ✅ 求解成功!'); if (typeof result.token === 'string') { console.log(` Token: ${result.token.substring(0, 60)}...`); } } else { console.log(' ❌ 求解失败'); if (result.error) console.log(` Error: ${result.error}`); } console.log('═'.repeat(60)); console.log(''); process.exit(result.success ? 0 : 1); } main().catch(err => { logger.error(`未捕获异常: ${err.message}`); console.error(err.stack); process.exit(1); });