This commit is contained in:
dela
2026-02-21 18:27:49 +08:00
parent 0ac4b23f07
commit 5dc86ccfbf
270 changed files with 49508 additions and 4636 deletions

498
src/hcaptcha_solver.js Normal file
View File

@@ -0,0 +1,498 @@
'use strict';
/**
* hCaptcha Solver (Node.js)
*
* 完整流程:
* checksiteconfig → hsw(req) 计算 n → 构建加密体 → getcaptcha → 解密响应 → 拿 token
*
* 使用 hsw.js 在 Node 沙盒中运行(全局污染方式)
*/
const { readFileSync } = require('fs');
const { join } = require('path');
const msgpack = require('msgpack-lite');
const { createBrowserEnvironment } = require('./sandbox/mocks/index');
const { Logger } = require('./utils/logger');
const logger = new Logger('hcaptcha_solver');
// 保存原始 fetch在全局被 mock 污染之前)
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._savedGlobals = {};
}
/**
* 将 mock 注入全局,加载并执行 hsw.js
* @param {string} hswPath - hsw.js 文件路径
* @param {object} fingerprint - 指纹覆盖
*/
async init(hswPath, fingerprint = {}) {
if (this.initialized) return;
const env = createBrowserEnvironment(fingerprint);
// 保存原始全局
const keys = ['window', 'document', 'navigator', 'screen', 'location',
'localStorage', 'sessionStorage', 'crypto', 'performance',
'self', 'top', 'parent', 'fetch', 'XMLHttpRequest'];
for (const k of keys) {
if (k in globalThis) this._savedGlobals[k] = globalThis[k];
}
// 注入全局
const force = (obj, prop, val) => {
Object.defineProperty(obj, prop, {
value: val, writable: true, configurable: true, enumerable: true,
});
};
force(globalThis, 'window', env.window);
force(globalThis, 'document', env.document);
force(globalThis, 'navigator', env.navigator);
force(globalThis, 'screen', env.screen);
force(globalThis, 'location', env.location);
force(globalThis, 'localStorage', env.localStorage);
force(globalThis, 'sessionStorage', env.sessionStorage);
force(globalThis, 'crypto', env.crypto);
force(globalThis, 'performance', env.performance);
force(globalThis, 'self', env.window);
force(globalThis, 'top', env.window);
force(globalThis, 'parent', env.window);
// 浏览器 API
globalThis.fetch = env.window.fetch;
globalThis.btoa = env.window.btoa;
globalThis.atob = env.window.atob;
globalThis.setTimeout = env.window.setTimeout;
globalThis.setInterval = env.window.setInterval;
globalThis.clearTimeout = env.window.clearTimeout;
globalThis.clearInterval = env.window.clearInterval;
globalThis.TextEncoder = env.window.TextEncoder;
globalThis.TextDecoder = env.window.TextDecoder;
globalThis.requestAnimationFrame = env.window.requestAnimationFrame;
globalThis.cancelAnimationFrame = env.window.cancelAnimationFrame;
// 加载 hsw.js
const code = readFileSync(hswPath, 'utf-8');
logger.info(`hsw.js 已加载 (${(code.length / 1024).toFixed(1)} KB)`);
try {
const fn = new Function(`(function() { ${code} })();`);
fn();
} catch (err) {
logger.error(`hsw.js 执行失败: ${err.message}`);
throw err;
}
// 查找 hsw 函数
if (typeof globalThis.window?.hsw === 'function') {
this.hswFn = globalThis.window.hsw;
} else if (typeof globalThis.hsw === 'function') {
this.hswFn = globalThis.hsw;
}
if (!this.hswFn) {
throw new Error('hsw function not found after execution');
}
this.initialized = true;
logger.success('Bridge 已就绪');
}
/** 计算 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 };