415gotit
This commit is contained in:
498
src/hcaptcha_solver.js
Normal file
498
src/hcaptcha_solver.js
Normal 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-cookie(hmt_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 };
|
||||
Reference in New Issue
Block a user