415gotit
This commit is contained in:
@@ -1,19 +1,30 @@
|
||||
/**
|
||||
* Test: Full Flow
|
||||
*
|
||||
* checksiteconfig -> hsw(n) -> getcaptcha
|
||||
* checksiteconfig → hsw(req) → getcaptcha
|
||||
*
|
||||
* Real sitekey from Stripe integration
|
||||
* 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');
|
||||
|
||||
// Real config from log.txt
|
||||
// ── Config ───────────────────────────────────────────────────
|
||||
const CONFIG = {
|
||||
host: 'b.stripecdn.com',
|
||||
sitekey: 'ec637546-e9b8-447a-ab81-b5fb6d228ab8',
|
||||
@@ -21,28 +32,33 @@ const CONFIG = {
|
||||
|
||||
const HCAPTCHA_API = 'https://api.hcaptcha.com';
|
||||
|
||||
// Browser fingerprint for consistency
|
||||
const FINGERPRINT = {
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
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 fetch('https://js.hcaptcha.com/1/api.js');
|
||||
const text = await res.text();
|
||||
|
||||
// Extract version from api.js
|
||||
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];
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return '9721ee268e2e8547d41c6d0d4d2f1144bd8b6eb7';
|
||||
const fallback = '9721ee268e2e8547d41c6d0d4d2f1144bd8b6eb7';
|
||||
logger.warn(`Could not parse version, using fallback: ${fallback}`);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function checkSiteConfig(version) {
|
||||
@@ -58,67 +74,67 @@ async function checkSiteConfig(version) {
|
||||
});
|
||||
|
||||
const url = `${HCAPTCHA_API}/checksiteconfig?${params}`;
|
||||
logger.debug(`URL: ${url}`);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
const res = await http.post(url, '', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Origin': 'https://newassets.hcaptcha.com',
|
||||
'Referer': 'https://newassets.hcaptcha.com/',
|
||||
'User-Agent': FINGERPRINT.userAgent,
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
'origin': 'https://newassets.hcaptcha.com',
|
||||
'referer': 'https://newassets.hcaptcha.com/',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
logger.info(`checksiteconfig response: pass=${data.pass}, c.type=${data.c?.type}`);
|
||||
logger.info(`Full response: ${JSON.stringify(data, null, 2)}`);
|
||||
|
||||
if (!data.pass) {
|
||||
logger.error('checksiteconfig failed - captcha required');
|
||||
}
|
||||
|
||||
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 startTime = Date.now();
|
||||
const t0 = Date.now();
|
||||
const n = await hsw.getN(req);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
logger.info(`n computed in ${duration}ms, length: ${n.length}`);
|
||||
logger.debug(`n preview: ${n.substring(0, 64)}...`);
|
||||
|
||||
logger.info(`n computed in ${Date.now() - t0}ms, length: ${n.length}`);
|
||||
return n;
|
||||
}
|
||||
|
||||
function generateMotion() {
|
||||
logger.info('Step 3: Generating motion data...');
|
||||
|
||||
const generator = new MotionGenerator({
|
||||
const gen = new MotionGenerator({
|
||||
screenWidth: FINGERPRINT.screenWidth,
|
||||
screenHeight: FINGERPRINT.screenHeight,
|
||||
checkboxPos: { x: 200, y: 300 },
|
||||
});
|
||||
|
||||
const motion = generator.generate();
|
||||
logger.info(`Motion: ${motion.mm.length} mouse moves, duration ${motion.mm[motion.mm.length-1][2] - motion.mm[0][2]}ms`);
|
||||
|
||||
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, hswFn) {
|
||||
async function getCaptcha(version, siteConfig, n, motionData, hsw) {
|
||||
logger.info('Step 4: getcaptcha...');
|
||||
|
||||
const url = `${HCAPTCHA_API}/getcaptcha/${CONFIG.sitekey}`;
|
||||
|
||||
// enc_get_req: true means we MUST encrypt
|
||||
logger.info('Building encrypted request (enc_get_req=true)...');
|
||||
// ── 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}`);
|
||||
|
||||
// Build raw payload as JSON string
|
||||
const rawPayload = {
|
||||
// ── 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,
|
||||
@@ -128,99 +144,129 @@ async function getCaptcha(version, siteConfig, n, motionData, hswFn) {
|
||||
c: JSON.stringify(siteConfig.c),
|
||||
};
|
||||
|
||||
const payloadStr = JSON.stringify(rawPayload);
|
||||
logger.info(`Raw payload size: ${payloadStr.length} chars`);
|
||||
logger.info(`Payload fields: ${Object.keys(s).join(', ')}`);
|
||||
|
||||
// Encrypt with hsw(1, payload)
|
||||
const encrypted = await hswFn(1, payloadStr);
|
||||
logger.info(`Encrypted type: ${encrypted?.constructor?.name}, size: ${encrypted.length}`);
|
||||
// ── 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;
|
||||
|
||||
// Convert Uint8Array to Buffer
|
||||
const encryptedBuffer = Buffer.from(encrypted);
|
||||
logger.info(`Encrypting payload (without c): ${Object.keys(payloadToEncrypt).join(', ')}`);
|
||||
|
||||
// Try multiple content types
|
||||
const contentTypes = [
|
||||
'application/x-protobuf',
|
||||
'application/octet-stream',
|
||||
'application/binary',
|
||||
];
|
||||
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')}`);
|
||||
|
||||
for (const contentType of contentTypes) {
|
||||
logger.info(`Trying Content-Type: ${contentType}...`);
|
||||
// hsw(1, msgpackBytes) → encrypted Uint8Array
|
||||
const encrypted = await hsw.encrypt(msgpackInput);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Origin': 'https://newassets.hcaptcha.com',
|
||||
'Referer': 'https://newassets.hcaptcha.com/',
|
||||
'User-Agent': FINGERPRINT.userAgent,
|
||||
},
|
||||
body: encryptedBuffer,
|
||||
});
|
||||
const ctor = encrypted?.constructor?.name ?? 'unknown';
|
||||
const encLen = encrypted?.length ?? encrypted?.byteLength ?? 0;
|
||||
logger.info(`Encrypted: type=${typeof encrypted}, ctor=${ctor}, len=${encLen}`);
|
||||
|
||||
const text = await res.text();
|
||||
logger.info(`Response status: ${res.status}, body: ${text.substring(0, 200)}`);
|
||||
|
||||
if (res.status === 200 && !text.includes('Unsupported')) {
|
||||
try {
|
||||
// Try to parse as JSON first
|
||||
const data = JSON.parse(text);
|
||||
return data;
|
||||
} catch (e) {
|
||||
// If not JSON, might be encrypted - try to decrypt
|
||||
logger.info('Response not JSON, trying to decrypt...');
|
||||
try {
|
||||
const decrypted = await hswFn(0, new Uint8Array(Buffer.from(text)));
|
||||
logger.info(`Decrypted: ${decrypted?.substring?.(0, 200) || decrypted}`);
|
||||
return JSON.parse(decrypted);
|
||||
} catch (e2) {
|
||||
// Try treating text as binary
|
||||
try {
|
||||
const binaryResp = new Uint8Array(text.split('').map(c => c.charCodeAt(0)));
|
||||
const decrypted2 = await hswFn(0, binaryResp);
|
||||
return JSON.parse(decrypted2);
|
||||
} catch (e3) {
|
||||
logger.error(`Decrypt failed: ${e3.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!encrypted || encLen < 100) {
|
||||
logger.error('Encryption returned suspiciously small data – aborting');
|
||||
return { success: false, error: 'encryption failed' };
|
||||
}
|
||||
|
||||
return { success: false, error: 'All content types 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 {
|
||||
// Initialize HSW runner
|
||||
const hsw = new HswRunner({ fingerprint: FINGERPRINT });
|
||||
await hsw.init();
|
||||
|
||||
// Get current version
|
||||
const version = await getVersion();
|
||||
|
||||
// Step 1: checksiteconfig
|
||||
// Step 1
|
||||
const siteConfig = await checkSiteConfig(version);
|
||||
|
||||
if (!siteConfig.c || !siteConfig.c.req) {
|
||||
if (!siteConfig.c?.req) {
|
||||
logger.error('No challenge request in response');
|
||||
logger.info('Response:', JSON.stringify(siteConfig, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Compute n
|
||||
// Step 2
|
||||
const n = await computeN(hsw, siteConfig.c.req);
|
||||
|
||||
// Step 3: Generate motion
|
||||
// Step 3
|
||||
const motionData = generateMotion();
|
||||
|
||||
// Step 4: getcaptcha
|
||||
const result = await getCaptcha(version, siteConfig, n, motionData, hsw.hswFn);
|
||||
// Step 4
|
||||
const result = await getCaptcha(
|
||||
version, siteConfig, n, motionData, hsw,
|
||||
);
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
if (result.generated_pass_UUID) {
|
||||
@@ -234,7 +280,6 @@ async function main() {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user