290 lines
11 KiB
JavaScript
290 lines
11 KiB
JavaScript
/**
|
||
* 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();
|