frist
This commit is contained in:
244
test/test_full_flow.js
Normal file
244
test/test_full_flow.js
Normal file
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Test: Full Flow
|
||||
*
|
||||
* checksiteconfig -> hsw(n) -> getcaptcha
|
||||
*
|
||||
* Real sitekey from Stripe integration
|
||||
*/
|
||||
|
||||
import { Logger } from '../src/utils/logger.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
|
||||
const CONFIG = {
|
||||
host: 'b.stripecdn.com',
|
||||
sitekey: 'ec637546-e9b8-447a-ab81-b5fb6d228ab8',
|
||||
};
|
||||
|
||||
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',
|
||||
screenWidth: 1920,
|
||||
screenHeight: 1080,
|
||||
};
|
||||
|
||||
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 match = text.match(/v1\/([a-f0-9]+)\/static/);
|
||||
if (match) {
|
||||
logger.info(`Version: ${match[1]}`);
|
||||
return match[1];
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return '9721ee268e2e8547d41c6d0d4d2f1144bd8b6eb7';
|
||||
}
|
||||
|
||||
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}`;
|
||||
logger.debug(`URL: ${url}`);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Origin': 'https://newassets.hcaptcha.com',
|
||||
'Referer': 'https://newassets.hcaptcha.com/',
|
||||
'User-Agent': FINGERPRINT.userAgent,
|
||||
},
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function computeN(hsw, req) {
|
||||
logger.info('Step 2: Computing n value...');
|
||||
|
||||
const startTime = 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)}...`);
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
function generateMotion() {
|
||||
logger.info('Step 3: Generating motion data...');
|
||||
|
||||
const generator = 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`);
|
||||
|
||||
return motion;
|
||||
}
|
||||
|
||||
async function getCaptcha(version, siteConfig, n, motionData, hswFn) {
|
||||
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)...');
|
||||
|
||||
// Build raw payload as JSON string
|
||||
const rawPayload = {
|
||||
v: version,
|
||||
sitekey: CONFIG.sitekey,
|
||||
host: CONFIG.host,
|
||||
hl: 'en',
|
||||
motionData: JSON.stringify(motionData),
|
||||
n: n,
|
||||
c: JSON.stringify(siteConfig.c),
|
||||
};
|
||||
|
||||
const payloadStr = JSON.stringify(rawPayload);
|
||||
logger.info(`Raw payload size: ${payloadStr.length} chars`);
|
||||
|
||||
// Encrypt with hsw(1, payload)
|
||||
const encrypted = await hswFn(1, payloadStr);
|
||||
logger.info(`Encrypted type: ${encrypted?.constructor?.name}, size: ${encrypted.length}`);
|
||||
|
||||
// Convert Uint8Array to Buffer
|
||||
const encryptedBuffer = Buffer.from(encrypted);
|
||||
|
||||
// Try multiple content types
|
||||
const contentTypes = [
|
||||
'application/x-protobuf',
|
||||
'application/octet-stream',
|
||||
'application/binary',
|
||||
];
|
||||
|
||||
for (const contentType of contentTypes) {
|
||||
logger.info(`Trying Content-Type: ${contentType}...`);
|
||||
|
||||
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 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false, error: 'All content types failed' };
|
||||
}
|
||||
|
||||
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
|
||||
const siteConfig = await checkSiteConfig(version);
|
||||
|
||||
if (!siteConfig.c || !siteConfig.c.req) {
|
||||
logger.error('No challenge request in response');
|
||||
logger.info('Response:', JSON.stringify(siteConfig, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Compute n
|
||||
const n = await computeN(hsw, siteConfig.c.req);
|
||||
|
||||
// Step 3: Generate motion
|
||||
const motionData = generateMotion();
|
||||
|
||||
// Step 4: getcaptcha
|
||||
const result = await getCaptcha(version, siteConfig, n, motionData, hsw.hswFn);
|
||||
|
||||
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();
|
||||
109
test/test_motion.js
Normal file
109
test/test_motion.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Test: Motion Data Generation
|
||||
*
|
||||
* Validates that generated mouse trajectories look human.
|
||||
*/
|
||||
|
||||
import { MotionGenerator } from '../src/generator/motion.js';
|
||||
import { Logger } from '../src/utils/logger.js';
|
||||
|
||||
const logger = new Logger('TestMotion');
|
||||
|
||||
function test() {
|
||||
logger.info('Starting motion generation test...');
|
||||
|
||||
const generator = new MotionGenerator({
|
||||
screenWidth: 1920,
|
||||
screenHeight: 1080,
|
||||
checkboxPos: { x: 200, y: 300 },
|
||||
});
|
||||
|
||||
const motion = generator.generate();
|
||||
|
||||
// Validate structure
|
||||
logger.info('Validating motion data structure...');
|
||||
|
||||
if (!motion.st || typeof motion.st !== 'number') {
|
||||
logger.error('Missing or invalid start timestamp (st)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!Array.isArray(motion.mm) || motion.mm.length === 0) {
|
||||
logger.error('Missing or empty mouse moves (mm)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!Array.isArray(motion.md) || motion.md.length === 0) {
|
||||
logger.error('Missing or empty mouse down (md)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!Array.isArray(motion.mu) || motion.mu.length === 0) {
|
||||
logger.error('Missing or empty mouse up (mu)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.success('Structure validation passed');
|
||||
|
||||
// Analyze trajectory
|
||||
logger.info('Analyzing trajectory characteristics...');
|
||||
|
||||
const mm = motion.mm;
|
||||
let totalDistance = 0;
|
||||
let straightLineDistance = 0;
|
||||
let prevPoint = null;
|
||||
|
||||
for (const point of mm) {
|
||||
if (prevPoint) {
|
||||
const dx = point[0] - prevPoint[0];
|
||||
const dy = point[1] - prevPoint[1];
|
||||
totalDistance += Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
prevPoint = point;
|
||||
}
|
||||
|
||||
// Calculate straight-line distance
|
||||
const start = mm[0];
|
||||
const end = mm[mm.length - 1];
|
||||
const sdx = end[0] - start[0];
|
||||
const sdy = end[1] - start[1];
|
||||
straightLineDistance = Math.sqrt(sdx * sdx + sdy * sdy);
|
||||
|
||||
const curviness = totalDistance / straightLineDistance;
|
||||
|
||||
logger.info(`Total points: ${mm.length}`);
|
||||
logger.info(`Path distance: ${totalDistance.toFixed(1)}px`);
|
||||
logger.info(`Straight distance: ${straightLineDistance.toFixed(1)}px`);
|
||||
logger.info(`Curviness ratio: ${curviness.toFixed(2)}x`);
|
||||
|
||||
if (curviness < 1.05) {
|
||||
logger.warn('Trajectory is too straight! Looks robotic.');
|
||||
logger.warn('Consider increasing bezier control point variance.');
|
||||
} else if (curviness > 3.0) {
|
||||
logger.warn('Trajectory is too curved! Looks erratic.');
|
||||
} else {
|
||||
logger.success('Trajectory curviness looks human');
|
||||
}
|
||||
|
||||
// Check timing
|
||||
const duration = mm[mm.length - 1][2] - mm[0][2];
|
||||
logger.info(`Duration: ${duration}ms`);
|
||||
|
||||
if (duration < 200) {
|
||||
logger.warn('Movement too fast! Humans are slower.');
|
||||
} else if (duration > 5000) {
|
||||
logger.warn('Movement too slow! Humans are faster.');
|
||||
} else {
|
||||
logger.success('Movement timing looks human');
|
||||
}
|
||||
|
||||
// Output sample
|
||||
logger.info('\nSample motion data (first 5 points):');
|
||||
for (let i = 0; i < Math.min(5, mm.length); i++) {
|
||||
console.log(` [${mm[i][0]}, ${mm[i][1]}, ${mm[i][2]}]`);
|
||||
}
|
||||
|
||||
logger.success('\nMotion test completed');
|
||||
}
|
||||
|
||||
test();
|
||||
55
test/test_n_gen.js
Normal file
55
test/test_n_gen.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Test: N Value Generation
|
||||
*
|
||||
* Validates that the sandbox can execute hsw.js and produce valid n values.
|
||||
*/
|
||||
|
||||
import { HswRunner } from '../src/sandbox/hsw_runner.js';
|
||||
import { Logger } from '../src/utils/logger.js';
|
||||
|
||||
Logger.globalLevel = 'debug';
|
||||
const logger = new Logger('TestN');
|
||||
|
||||
async function test() {
|
||||
logger.info('Starting n value generation test...');
|
||||
|
||||
const runner = new HswRunner();
|
||||
|
||||
try {
|
||||
await runner.init();
|
||||
logger.success('Sandbox initialized');
|
||||
} catch (err) {
|
||||
logger.error(`Sandbox init failed: ${err.message}`);
|
||||
logger.error('This is where debugging begins.');
|
||||
logger.error('The error message tells you what hsw.js tried to access.');
|
||||
logger.error('Add that property to browser_mock.js and try again.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Test with a JWT-formatted req string
|
||||
// In production, this comes from checksiteconfig response
|
||||
// Format: base64(header).base64(payload).base64(signature)
|
||||
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64');
|
||||
const payload = Buffer.from(JSON.stringify({
|
||||
t: 'hsw',
|
||||
s: 'test-session-id',
|
||||
l: 'https://example.com',
|
||||
iat: Math.floor(Date.now() / 1000)
|
||||
})).toString('base64');
|
||||
const signature = Buffer.from('fake-signature-for-testing').toString('base64');
|
||||
const testReq = `${header}.${payload}.${signature}`;
|
||||
|
||||
logger.info(`Test JWT: ${testReq.substring(0, 50)}...`);
|
||||
|
||||
try {
|
||||
const n = await runner.getN(testReq);
|
||||
logger.success(`Generated n value: ${n}`);
|
||||
logger.info('If this looks like a valid base64 string, you\'re on the right track.');
|
||||
} catch (err) {
|
||||
logger.error(`N generation failed: ${err.message}`);
|
||||
logger.error('Check the error - it tells you what\'s missing in the mock.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
test().catch(console.error);
|
||||
Reference in New Issue
Block a user