Files
hcapEnv/src/hcaptcha_solver.js
2026-02-22 15:05:49 +08:00

511 lines
18 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';
/**
* hCaptcha Solver (Node.js)
*
* 完整流程:
* checksiteconfig → hsw(req) 计算 n → 构建加密体 → getcaptcha → 解密响应 → 拿 token
*
* 使用 hsw.js 在 Node 沙盒中运行(全局污染方式)
*/
const vm = require('vm');
const { readFileSync } = require('fs');
const { join } = require('path');
const msgpack = require('msgpack-lite');
const windowMock = require('./sandbox/mocks/window');
const { applySandboxPatches } = require('./sandbox/mocks/index');
const { Logger } = require('./utils/logger');
const logger = new Logger('hcaptcha_solver');
// 保存原始 fetch供真实网络请求使用
const realFetch = globalThis.fetch;
// ── 常量 ──────────────────────────────────────────────────────
const HCAPTCHA_API = 'https://api.hcaptcha.com';
const HCAPTCHA_JS = 'https://js.hcaptcha.com/1/api.js';
const HCAPTCHA_CDN = 'https://newassets.hcaptcha.com';
// ── 伪造 motion data ──────────────────────────────────────────
function generateMotionData() {
const now = Date.now();
const st = now - 3000 - Math.floor(Math.random() * 1000);
// 随机鼠标轨迹
const mm = [];
let x = 200 + Math.floor(Math.random() * 100);
let y = 300 + Math.floor(Math.random() * 100);
const steps = 15 + Math.floor(Math.random() * 10);
for (let i = 0; i < steps; i++) {
x += Math.floor(Math.random() * 6) - 3;
y += Math.floor(Math.random() * 6) - 3;
mm.push([x, y, st + i * (80 + Math.floor(Math.random() * 40))]);
}
return {
st,
dct: st + 200 + Math.floor(Math.random() * 100),
mm,
'mm-mp': 15.42857142857143,
md: [[mm[0][0], mm[0][1], mm[0][2] + 500]],
'md-mp': 0,
mu: [[mm[0][0], mm[0][1], mm[0][2] + 600]],
'mu-mp': 0,
kd: [],
'kd-mp': 0,
ku: [],
'ku-mp': 0,
topLevel: {
st,
sc: {
availWidth: 1920,
availHeight: 1040,
width: 1920,
height: 1080,
colorDepth: 24,
pixelDepth: 24,
availLeft: 0,
availTop: 0,
},
nv: {
hardwareConcurrency: 8,
deviceMemory: 8,
},
dr: '',
inv: false,
exec: false,
},
v: 1,
};
}
// ── HSW 沙盒管理 ──────────────────────────────────────────────
class HswBridge {
constructor() {
this.hswFn = null;
this.initialized = false;
this._ctx = null;
}
/**
* 构建 vm 沙盒上下文,将 mock window 注入隔离环境
* @param {object} fingerprint - 指纹覆盖
*/
_buildContext(fingerprint) {
const ctx = Object.create(null);
// 把 windowMock 上所有 key 复制进 ctx浅拷贝
for (const key of Reflect.ownKeys(windowMock)) {
try { ctx[key] = windowMock[key]; } catch (_) {}
}
// vm 必需的自引用
ctx.global = ctx;
ctx.globalThis = ctx;
ctx.window = ctx;
ctx.self = ctx;
// 透传 console调试用
ctx.console = console;
// 保证 Promise / 定时器在 vm 里可用
ctx.Promise = Promise;
ctx.setTimeout = setTimeout;
ctx.clearTimeout = clearTimeout;
ctx.setInterval = setInterval;
ctx.clearInterval = clearInterval;
ctx.queueMicrotask = queueMicrotask;
// 应用指纹覆盖
if (fingerprint.userAgent && ctx.navigator) {
ctx.navigator.userAgent = fingerprint.userAgent;
ctx.navigator.appVersion = fingerprint.userAgent.replace('Mozilla/', '');
}
if (fingerprint.platform && ctx.navigator) {
ctx.navigator.platform = fingerprint.platform;
}
if (fingerprint.host && ctx.location?.ancestorOrigins) {
ctx.location.ancestorOrigins[0] = `https://${fingerprint.host}`;
}
const vmCtx = vm.createContext(ctx);
// Apply escape defense + error stack rewriting AFTER context creation
applySandboxPatches(vmCtx);
return vmCtx;
}
/**
* 在 vm 沙盒中加载并执行 hsw.js
* @param {string} hswPath - hsw.js 文件路径
* @param {object} fingerprint - 指纹覆盖
*/
async init(hswPath, fingerprint = {}) {
if (this.initialized) return;
const code = readFileSync(hswPath, 'utf-8');
logger.info(`hsw.js 已加载 (${(code.length / 1024).toFixed(1)} KB)`);
const ctx = this._buildContext(fingerprint);
this._ctx = ctx;
const script = new vm.Script(code, { filename: 'hsw.js' });
try {
script.runInContext(ctx, { timeout: 10000 });
} catch (err) {
logger.error(`hsw.js 执行失败: ${err.message}`);
throw err;
}
// 查找 hsw 函数
if (typeof ctx.hsw === 'function') {
this.hswFn = ctx.hsw;
} else if (typeof ctx.window?.hsw === 'function') {
this.hswFn = ctx.window.hsw;
}
if (!this.hswFn) {
throw new Error('hsw function not found after execution');
}
this.initialized = true;
logger.success('Bridge 已就绪 (vm 沙盒)');
}
/** 计算 PoW n 值: hsw(req_jwt_string) */
async getN(req) {
return this.hswFn(req);
}
/** 加密请求体: hsw(1, data) */
async encrypt(data) {
return this.hswFn(1, data);
}
/** 解密响应体: hsw(0, data) */
async decrypt(data) {
return this.hswFn(0, data);
}
}
// ── HTTP 工具 ─────────────────────────────────────────────────
const DEFAULT_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': '*/*',
'Accept-Language': 'en-US,en;q=0.9',
'sec-ch-ua': '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
};
async function httpGet(url, headers = {}) {
const resp = await realFetch(url, {
method: 'GET',
headers: { ...DEFAULT_HEADERS, ...headers },
});
const text = await resp.text();
logger.info(`HTTP GET ${url.substring(0, 80)}... → ${resp.status}`);
return { status: resp.status, text, headers: resp.headers };
}
async function httpPost(url, body, headers = {}) {
const isBuffer = body instanceof Uint8Array || Buffer.isBuffer(body);
const opts = {
method: 'POST',
headers: {
...DEFAULT_HEADERS,
...(isBuffer
? { 'Content-Type': 'application/octet-stream' }
: { 'Content-Type': 'application/x-www-form-urlencoded' }),
...headers,
},
body: isBuffer ? body : body,
};
const resp = await realFetch(url, opts);
logger.info(`HTTP POST ${url.substring(0, 80)}... → ${resp.status}`);
return resp;
}
// ── 主求解器 ──────────────────────────────────────────────────
class HCaptchaSolver {
/**
* @param {object} opts
* @param {string} opts.sitekey
* @param {string} opts.host - 嵌入 hCaptcha 的站点域名
* @param {string} [opts.rqdata] - 可选附加数据
* @param {string} [opts.hswPath] - hsw.js 本地路径
*/
constructor(opts) {
this.sitekey = opts.sitekey;
this.host = opts.host;
this.rqdata = opts.rqdata || null;
this.hswPath = opts.hswPath || join(__dirname, '../asset/hsw.js');
this.bridge = new HswBridge();
this.version = null;
}
/** 1. 从 api.js 获取最新版本 hash */
async fetchVersion() {
const { text } = await httpGet(HCAPTCHA_JS);
// 尝试多种模式匹配版本 hash
const patterns = [
/captcha\/v1\/([a-f0-9]{40})\//i, // captcha/v1/HASH/
/\/c\/([a-f0-9]{40})\//i, // /c/HASH/
/v=([a-f0-9]{40})/i, // v=HASH
/["']([a-f0-9]{40})["']/, // 直接引用的40位hash
];
for (const pat of patterns) {
const m = text.match(pat);
if (m) {
this.version = m[1];
logger.info(`获取到最新 hCaptcha 版本: ${this.version}`);
return this.version;
}
}
throw new Error('无法从 api.js 提取版本 hash');
}
/** 2. 初始化 Bridge加载 hsw.js 到沙盒) */
async initBridge() {
let hswCode;
// 优先用本地 hsw.js
try {
readFileSync(this.hswPath);
hswCode = this.hswPath;
logger.info(`使用本地 hsw.js: ${this.hswPath}`);
} catch {
// 从 CDN 下载
if (!this.version) await this.fetchVersion();
const url = `${HCAPTCHA_CDN}/c/${this.version}/hsw.js`;
logger.info(`从 CDN 下载 hsw.js: ${url}`);
const { text } = await httpGet(url);
// 写入临时文件
const tmp = join(__dirname, '../asset/hsw_latest.js');
require('fs').writeFileSync(tmp, text);
hswCode = tmp;
logger.info(`hsw.js 已保存: ${tmp}`);
}
await this.bridge.init(hswCode, {
host: this.host,
});
}
/** 3. checksiteconfig */
async checkSiteConfig() {
if (!this.version) await this.fetchVersion();
const params = new URLSearchParams({
v: this.version,
host: this.host,
sitekey: this.sitekey,
sc: '1',
swa: '1',
spst: '0',
});
const t0 = Date.now();
const url = `${HCAPTCHA_API}/checksiteconfig?${params}`;
const resp = await realFetch(url, {
method: 'POST',
headers: {
...DEFAULT_HEADERS,
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'https://newassets.hcaptcha.com',
'Referer': 'https://newassets.hcaptcha.com/',
},
body: params.toString(),
});
const data = await resp.json();
const dur = Date.now() - t0;
logger.info(`HTTP POST ${url.substring(0, 80)}... → ${resp.status}`);
// 提取 set-cookiehmt_id 等),后续请求需要带上
const rawCookie = resp.headers.get('set-cookie') || '';
const hmtMatch = rawCookie.match(/hmt_id=[^;]+/);
this.cookie = hmtMatch ? hmtMatch[0] : '';
if (this.cookie) logger.info(`获取到 cookie: ${this.cookie}`);
const pass = data.pass !== false;
const cType = data.c?.type || 'unknown';
logger.info(`checksiteconfig: pass=${pass}, type=${cType}, duration=${dur}ms`);
if (!data.c) {
throw new Error(`checksiteconfig 缺少 challenge 字段: ${JSON.stringify(data)}`);
}
return data;
}
/** 4. 构建并加密请求体 */
async buildEncryptedBody(challenge) {
// 计算 PoW n 值
logger.info('计算 PoW n 值...');
const n = await this.bridge.getN(challenge.c.req);
logger.info(`n = ${typeof n === 'string' ? n.substring(0, 40) + '...' : n}`);
// 构建 payload
const motionData = generateMotionData();
const payload = {
v: this.version,
sitekey: this.sitekey,
host: this.host,
hl: 'en',
motionData: JSON.stringify(motionData),
n,
c: JSON.stringify(challenge.c),
pst: false,
};
// 如果有 rqdata加入
if (this.rqdata) {
payload.rqdata = this.rqdata;
}
logger.info('构建加密请求体...');
// ── 正确的加密流程(与 flow_manager.js / h.html 一致)──
// Step 1: 克隆 payload去掉 c 字段c 会单独放在外层)
const payloadClone = { ...payload };
const cValue = payloadClone.c;
delete payloadClone.c;
// Step 2: msgpack 编码后再加密: hsw(1, msgpack.encode(payload_without_c))
const msgpackPayload = msgpack.encode(payloadClone);
logger.info(`msgpack 编码大小: ${msgpackPayload.length} bytes`);
const encrypted = await this.bridge.encrypt(msgpackPayload);
// 诊断encrypted 的类型和内容
const etype = typeof encrypted;
const ector = encrypted?.constructor?.name || 'unknown';
logger.info(`encrypt 返回类型: typeof=${etype}, constructor=${ector}`);
if (encrypted instanceof Uint8Array || Buffer.isBuffer(encrypted)) {
logger.info(`encrypt 返回 Uint8Array/Buffer, length=${encrypted.length}`);
} else if (typeof encrypted === 'string') {
logger.info(`encrypt 返回 string, length=${encrypted.length}`);
} else {
logger.info(`encrypt 返回: ${JSON.stringify(encrypted)?.substring(0, 200)}`);
}
if (!encrypted) {
throw new Error('hsw encrypt 返回 null/undefined');
}
// Step 3: 二次打包: body = msgpack.encode([JSON.stringify(c), encrypted_bytes])
const cString = typeof cValue === 'string' ? cValue : JSON.stringify(cValue);
const body = msgpack.encode([cString, encrypted]);
logger.info(`最终 body 大小: ${body.length} bytes, first10=[${Array.from(body.slice(0, 10)).map(b => '0x' + b.toString(16).padStart(2, '0')).join(',')}]`);
// 导出加密体供 curl_cffi 测试
require('fs').writeFileSync('body.bin', body);
logger.info('已导出加密体到 body.bin');
return body;
}
/** 5. getcaptcha → 拿 token */
async getCaptcha(encryptedBody) {
const url = `${HCAPTCHA_API}/getcaptcha/${this.sitekey}`;
const resp = await realFetch(url, {
method: 'POST',
headers: {
...DEFAULT_HEADERS,
'Accept': 'application/json, application/octet-stream',
'Content-Type': 'application/octet-stream',
'Origin': 'https://newassets.hcaptcha.com',
'Referer': 'https://newassets.hcaptcha.com/',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
'sec-fetch-storage-access': 'none',
'priority': 'u=1, i',
...(this.cookie ? { 'Cookie': this.cookie } : {}),
},
body: encryptedBody,
});
logger.info(`HTTP POST ${url}${resp.status}`);
logger.info(`响应 content-type: ${resp.headers.get('content-type')}`);
if (!resp.ok) {
const errText = await resp.text();
logger.error(`getcaptcha 失败 (${resp.status}): ${errText.substring(0, 200)}`);
throw new Error(`getcaptcha HTTP ${resp.status}: ${errText.substring(0, 200)}`);
}
// 尝试解密响应
const contentType = resp.headers.get('content-type') || '';
let result;
if (contentType.includes('json')) {
// 明文 JSON 响应
result = await resp.json();
} else {
// 二进制加密响应
const rawBuf = await resp.arrayBuffer();
const raw = new Uint8Array(rawBuf);
logger.info('解密响应...');
const decrypted = await this.bridge.decrypt(raw);
try {
result = msgpack.decode(decrypted);
} catch {
// 可能是 JSON 字符串
result = JSON.parse(new TextDecoder().decode(decrypted));
}
}
const pass = result.pass || result.generated_pass_UUID;
logger.info(`getcaptcha 结果: pass=${!!pass}`);
return result;
}
/** 完整求解流程 */
async solve() {
logger.info(`开始求解 sitekey=${this.sitekey.substring(0, 12)}... host=${this.host}`);
const t0 = Date.now();
try {
// 步骤 1: 获取版本
await this.fetchVersion();
// 步骤 2: 初始化 Bridge
await this.initBridge();
// 步骤 3: checksiteconfig
const config = await this.checkSiteConfig();
// 步骤 4: 构建加密体
const body = await this.buildEncryptedBody(config);
// 步骤 5: getcaptcha
const result = await this.getCaptcha(body);
const token = result.generated_pass_UUID || result.pass;
const dur = ((Date.now() - t0) / 1000).toFixed(2);
if (token && typeof token === 'string' && token.startsWith('P1_')) {
logger.success(`✅ 求解成功! (${dur}s)`);
logger.info(`Token: ${token.substring(0, 50)}...`);
return { success: true, token, result };
} else if (result.pass === true) {
logger.success(`✅ pass=true (${dur}s)`);
return { success: true, token: token || true, result };
} else {
logger.error(`❌ 求解失败: ${JSON.stringify(result).substring(0, 200)}`);
return { success: false, result };
}
} catch (err) {
logger.error(`求解异常: ${err.message}`);
logger.error(err.stack);
return { success: false, error: err.message };
}
}
}
module.exports = { HCaptchaSolver, HswBridge, generateMotionData };