Files
hcapEnv/test/test_full_flow.js
2026-02-21 18:27:49 +08:00

290 lines
11 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.
/**
* Test: Full Flow
*
* checksiteconfig → hsw(req) → getcaptcha
*
* Based on exact source code analysis of h.html (hCaptcha client).
*
* Key protocol details (from h.html getTaskData):
* 1. Build payload `s` {v, sitekey, host, hl, motionData, n, c, rqdata, pst, ...}
* 2. Clone payload, delete c: c = deepClone(s); delete c.c
* 3. Encrypt without c: encrypted = hsw(1, msgpack.encode(c))
* 4. Assemble body: body = msgpack.encode([s.c, encrypted])
* 5. POST body as application/octet-stream
* 6. Decrypt response: hsw(0, new Uint8Array(response)) → msgpack.decode()
*/
import { writeFileSync } from 'node:fs';
import { encode, decode } from '@msgpack/msgpack';
import { Logger } from '../src/utils/logger.js';
import { HttpClient } from '../src/core/http_client.js';
import { HswRunner } from '../src/sandbox/hsw_runner.js';
import { MotionGenerator } from '../src/generator/motion.js';
Logger.globalLevel = 'debug';
const logger = new Logger('FullFlow');
// ── Config ───────────────────────────────────────────────────
const CONFIG = {
host: 'b.stripecdn.com',
sitekey: 'ec637546-e9b8-447a-ab81-b5fb6d228ab8',
};
const HCAPTCHA_API = 'https://api.hcaptcha.com';
const FINGERPRINT = {
userAgent:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
screenWidth: 1920,
screenHeight: 1080,
};
// ── Shared HTTP client (TLS + cookies + HTTP/2) ──────────────
const http = new HttpClient(FINGERPRINT);
// ─────────────────────────────────────────────────────────────
// Steps
// ─────────────────────────────────────────────────────────────
async function getVersion() {
logger.info('Fetching hCaptcha version...');
const res = await http.get('https://js.hcaptcha.com/1/api.js');
const text = res.text();
const match = text.match(/v1\/([a-f0-9]+)\/static/);
if (match) {
logger.info(`Version: ${match[1]}`);
return match[1];
}
const fallback = '9721ee268e2e8547d41c6d0d4d2f1144bd8b6eb7';
logger.warn(`Could not parse version, using fallback: ${fallback}`);
return fallback;
}
async function checkSiteConfig(version) {
logger.info('Step 1: checksiteconfig...');
const params = new URLSearchParams({
v: version,
host: CONFIG.host,
sitekey: CONFIG.sitekey,
sc: '1',
swa: '1',
spst: '0',
});
const url = `${HCAPTCHA_API}/checksiteconfig?${params}`;
const res = await http.post(url, '', {
headers: {
'content-type': 'application/x-www-form-urlencoded',
'origin': 'https://newassets.hcaptcha.com',
'referer': 'https://newassets.hcaptcha.com/',
},
});
const data = res.json();
logger.info(`checksiteconfig: pass=${data.pass}, c.type=${data.c?.type}`);
logger.debug(`Full response: ${JSON.stringify(data, null, 2)}`);
return data;
}
async function computeN(hsw, req) {
logger.info('Step 2: Computing n value...');
const t0 = Date.now();
const n = await hsw.getN(req);
logger.info(`n computed in ${Date.now() - t0}ms, length: ${n.length}`);
return n;
}
function generateMotion() {
logger.info('Step 3: Generating motion data...');
const gen = new MotionGenerator({
screenWidth: FINGERPRINT.screenWidth,
screenHeight: FINGERPRINT.screenHeight,
checkboxPos: { x: 200, y: 300 },
});
const motion = gen.generate();
const mm = motion.mm;
logger.info(`Motion: ${mm.length} moves, duration ${mm[mm.length - 1][2] - mm[0][2]}ms`);
return motion;
}
async function getCaptcha(version, siteConfig, n, motionData, hsw) {
logger.info('Step 4: getcaptcha...');
const url = `${HCAPTCHA_API}/getcaptcha/${CONFIG.sitekey}`;
// ── 4a OPTIONS preflight ────────────────────────────────
logger.info('Sending OPTIONS preflight...');
const pfRes = await http.options(url, {
headers: {
'accept': '*/*',
'access-control-request-method': 'POST',
'access-control-request-headers': 'content-type',
'origin': 'https://newassets.hcaptcha.com',
'referer': 'https://newassets.hcaptcha.com/',
},
});
logger.info(`OPTIONS: status=${pfRes.status}`);
// ── 4b Build payload `s` (exactly like h.html getTaskData) ──
//
// h.html builds `s` with these fields:
// v, sitekey, host, hl, motionData(JSON), n, c(JSON),
// rqdata (optional), pst (optional), pd/pdc/pem (optional)
//
const s = {
v: version,
sitekey: CONFIG.sitekey,
host: CONFIG.host,
hl: 'en',
motionData: JSON.stringify(motionData),
n: n,
c: JSON.stringify(siteConfig.c),
};
logger.info(`Payload fields: ${Object.keys(s).join(', ')}`);
// ── 4c Encrypt: clone → delete c → msgpack.encode → hsw(1, ...) ──
//
// h.html:17773-17778:
// var c = JSON.parse(JSON.stringify(s)); // deep clone
// delete c.c; // remove c field
// a = Cr(c) // Cr = hsw(1, msgpack.encode(c))
//
const payloadToEncrypt = JSON.parse(JSON.stringify(s));
delete payloadToEncrypt.c;
logger.info(`Encrypting payload (without c): ${Object.keys(payloadToEncrypt).join(', ')}`);
const msgpackInput = encode(payloadToEncrypt);
logger.info(`msgpack encoded: ${msgpackInput.length} bytes`);
logger.debug(`msgpack hex (first 40): ${Buffer.from(msgpackInput).subarray(0, 40).toString('hex')}`);
// hsw(1, msgpackBytes) → encrypted Uint8Array
const encrypted = await hsw.encrypt(msgpackInput);
const ctor = encrypted?.constructor?.name ?? 'unknown';
const encLen = encrypted?.length ?? encrypted?.byteLength ?? 0;
logger.info(`Encrypted: type=${typeof encrypted}, ctor=${ctor}, len=${encLen}`);
if (!encrypted || encLen < 100) {
logger.error('Encryption returned suspiciously small data aborting');
return { success: false, error: 'encryption failed' };
}
// ── 4d Assemble body: msgpack.encode([s.c, encrypted]) ──
//
// h.html:17779:
// l = Sr([s.c, t]) // Sr = msgpack.encode, s.c = JSON string, t = encrypted bytes
//
const body = encode([s.c, encrypted]);
logger.info(`Body: total=${body.length} bytes`);
logger.info(`Body first 20 bytes hex: ${Buffer.from(body).subarray(0, 20).toString('hex')}`);
// Save for external testing
writeFileSync('/tmp/hcaptcha_body.bin', body);
logger.debug('Saved body → /tmp/hcaptcha_body.bin');
// ── 4e POST ─────────────────────────────────────────────
const res = await http.post(url, Buffer.from(body), {
headers: {
'content-type': 'application/octet-stream',
'accept': 'application/json, application/octet-stream',
'origin': 'https://newassets.hcaptcha.com',
'referer': 'https://newassets.hcaptcha.com/',
'priority': 'u=1, i',
},
});
logger.info(`Response: status=${res.status}, content-type=${res.headers['content-type']}`);
if (res.status !== 200) {
const text = res.text();
logger.error(`Error body: ${text.substring(0, 300)}`);
return { success: false, status: res.status, body: text };
}
// ── 4f Decrypt response ─────────────────────────────────
//
// h.html:17809-17817:
// if (e instanceof ArrayBuffer)
// return Ar(new Uint8Array(e)) // Ar = hsw(0, bytes) → msgpack.decode()
//
const rawBody = res.body; // Buffer
// Check if response is JSON (fallback/error) or binary (encrypted)
const contentType = res.headers['content-type'] || '';
if (contentType.includes('application/json')) {
logger.info('Response is JSON (no decryption needed)');
return res.json();
}
logger.info(`Decrypting response (${rawBody.length} bytes)...`);
const decrypted = await hsw.decrypt(new Uint8Array(rawBody));
if (!decrypted) {
logger.error('Decryption returned null/undefined');
return { success: false, error: 'decryption failed' };
}
const result = decode(decrypted);
logger.info(`Decrypted result: ${JSON.stringify(result).substring(0, 200)}`);
return result;
}
// ─────────────────────────────────────────────────────────────
// main
// ─────────────────────────────────────────────────────────────
async function main() {
console.log('\n' + '='.repeat(60));
logger.info('Starting full flow test');
console.log('='.repeat(60) + '\n');
try {
const hsw = new HswRunner({ fingerprint: FINGERPRINT });
await hsw.init();
const version = await getVersion();
// Step 1
const siteConfig = await checkSiteConfig(version);
if (!siteConfig.c?.req) {
logger.error('No challenge request in response');
return;
}
// Step 2
const n = await computeN(hsw, siteConfig.c.req);
// Step 3
const motionData = generateMotion();
// Step 4
const result = await getCaptcha(
version, siteConfig, n, motionData, hsw,
);
console.log('\n' + '='.repeat(60));
if (result.generated_pass_UUID) {
logger.success('🎉 SUCCESS! Got pass token:');
console.log(result.generated_pass_UUID.substring(0, 100) + '...');
} else if (result.pass === true) {
logger.success('🎉 SUCCESS! Captcha passed');
console.log(JSON.stringify(result, null, 2));
} else {
logger.warn('Captcha not passed. Response:');
console.log(JSON.stringify(result, null, 2));
}
console.log('='.repeat(60) + '\n');
} catch (err) {
logger.error(`Flow failed: ${err?.message || err}`);
if (err?.stack) logger.error(err.stack);
}
}
main();