Files
hcapEnv/run_solver.js
2026-03-11 14:28:17 +08:00

339 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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);
});