Files
hcapEnv/src/generator/motion.js
2026-02-20 13:16:51 +08:00

157 lines
5.2 KiB
JavaScript

/**
* Motion Generator - Drawing the Soul
*
* hCaptcha uses mouse trajectory analysis to detect bots.
* Straight lines = robot = death.
*
* We generate human-like mouse movements using:
* - Bezier curves for smooth paths
* - Perlin noise for natural jitter
* - Realistic velocity profiles (slow start, fast middle, slow end)
*/
export class MotionGenerator {
constructor(options = {}) {
this.screenWidth = options.screenWidth || 1920;
this.screenHeight = options.screenHeight || 1080;
this.checkboxPos = options.checkboxPos || { x: 200, y: 300 };
}
/**
* Generate complete motion data matching hCaptcha's expected format
*/
generate() {
const startTime = Date.now();
const duration = this._randomBetween(800, 2000); // Human reaction time
// Starting point (off-screen or edge)
const start = {
x: this._randomBetween(-50, 50),
y: this._randomBetween(this.screenHeight / 2, this.screenHeight),
};
// Target: the checkbox
const end = {
x: this.checkboxPos.x + this._randomBetween(-5, 5),
y: this.checkboxPos.y + this._randomBetween(-5, 5),
};
// Generate movement points
const mm = this._generateMouseMoves(start, end, startTime, duration);
// Mouse down/up at the end
const clickTime = startTime + duration + this._randomBetween(50, 150);
const md = [[end.x, end.y, clickTime]];
const mu = [[end.x, end.y, clickTime + this._randomBetween(80, 150)]];
return {
st: startTime, // Start timestamp
dct: startTime, // Document creation time
mm, // Mouse moves: [[x, y, timestamp], ...]
md, // Mouse down
mu, // Mouse up
topLevel: {
st: startTime - this._randomBetween(1000, 3000),
sc: {
availWidth: this.screenWidth,
availHeight: this.screenHeight - 40,
width: this.screenWidth,
height: this.screenHeight,
colorDepth: 24,
pixelDepth: 24,
},
nv: {
vendorSub: '',
productSub: '20030107',
vendor: 'Google Inc.',
maxTouchPoints: 0,
hardwareConcurrency: 8,
cookieEnabled: true,
appCodeName: 'Mozilla',
appName: 'Netscape',
appVersion: '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
platform: 'Win32',
product: 'Gecko',
language: 'en-US',
onLine: true,
deviceMemory: 8,
},
dr: '',
inv: false,
exec: false,
},
v: 1,
};
}
/**
* Generate mouse movement points using Bezier curves
*/
_generateMouseMoves(start, end, startTime, duration) {
const points = [];
const numPoints = this._randomBetween(30, 60);
// Control points for cubic Bezier
const cp1 = {
x: start.x + (end.x - start.x) * 0.3 + this._randomBetween(-100, 100),
y: start.y + (end.y - start.y) * 0.1 + this._randomBetween(-50, 50),
};
const cp2 = {
x: start.x + (end.x - start.x) * 0.7 + this._randomBetween(-50, 50),
y: start.y + (end.y - start.y) * 0.9 + this._randomBetween(-30, 30),
};
for (let i = 0; i < numPoints; i++) {
// Non-linear time distribution (ease-in-out)
const rawT = i / (numPoints - 1);
const t = this._easeInOutCubic(rawT);
// Bezier interpolation
const pos = this._cubicBezier(start, cp1, cp2, end, t);
// Add micro-jitter (human hands shake)
pos.x += this._randomBetween(-2, 2);
pos.y += this._randomBetween(-2, 2);
// Timestamp with slight randomness
const timestamp = startTime + Math.floor(duration * rawT) + this._randomBetween(-5, 5);
points.push([Math.round(pos.x), Math.round(pos.y), timestamp]);
}
// Sort by timestamp
points.sort((a, b) => a[2] - b[2]);
return points;
}
/**
* Cubic Bezier interpolation
*/
_cubicBezier(p0, p1, p2, p3, t) {
const t2 = t * t;
const t3 = t2 * t;
const mt = 1 - t;
const mt2 = mt * mt;
const mt3 = mt2 * mt;
return {
x: mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x,
y: mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y,
};
}
/**
* Easing function for natural movement
*/
_easeInOutCubic(t) {
return t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2;
}
_randomBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}