/** * 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();