This commit is contained in:
dela
2026-02-21 18:27:49 +08:00
parent 0ac4b23f07
commit 5dc86ccfbf
270 changed files with 49508 additions and 4636 deletions

View File

@@ -4,10 +4,17 @@
* Step 1: checksiteconfig -> get 'c' (config) and 'req' (challenge)
* Step 2: sandbox -> compute 'n' from 'req'
* Step 3: motion -> generate mouse trajectory
* Step 4: getcaptcha -> submit payload
* Step 5: extract generated_pass_UUID
* Step 4: build payload, encrypt via hsw(1, msgpack), pack body
* Step 5: getcaptcha -> submit encrypted body
* Step 6: decrypt response via hsw(0, bytes) -> msgpack.decode
*
* Encryption flow (from h.html source):
* Cr(payload_without_c) = hsw(1, msgpack.encode(payload_without_c))
* body = msgpack.encode([JSON.stringify(c), encrypted_bytes])
* response = msgpack.decode(hsw(0, response_bytes))
*/
import { encode, decode } from '@msgpack/msgpack';
import { HttpClient } from './http_client.js';
import { HswRunner } from '../sandbox/hsw_runner.js';
import { MotionGenerator } from '../generator/motion.js';
@@ -19,7 +26,9 @@ const HCAPTCHA_API = 'https://hcaptcha.com';
export class FlowManager {
constructor(config) {
this.config = config;
this.http = new HttpClient();
this.http = new HttpClient({
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
});
this.hsw = new HswRunner();
this.motion = new MotionGenerator();
this.logger = new Logger('FlowManager');
@@ -34,7 +43,7 @@ export class FlowManager {
throw new Error('Invalid site config response');
}
// Step 2: Compute n value
// Step 2: Compute n value via hsw(req_string)
this.logger.info('Computing n value in sandbox...');
const n = await this.hsw.getN(siteConfig.req);
@@ -42,17 +51,20 @@ export class FlowManager {
this.logger.info('Generating motion data...');
const motionData = this.motion.generate();
// Step 4: Build and submit payload
this.logger.info('Submitting captcha...');
// Step 4: Build payload with all required fields
this.logger.info('Building payload...');
const payload = PayloadBuilder.build({
siteKey: this.config.siteKey,
host: this.config.host,
n,
c: siteConfig.c,
motionData,
rqdata: siteConfig.rqdata || '',
});
const result = await this._getCaptcha(payload);
// Step 5: Encrypt and submit
this.logger.info('Encrypting and submitting...');
const result = await this._getCaptchaEncrypted(payload, siteConfig.c);
return {
pass: result.generated_pass_UUID || null,
@@ -77,24 +89,62 @@ export class FlowManager {
},
});
return JSON.parse(response.body);
return response.json();
}
async _getCaptcha(payload) {
/**
* Encrypted getcaptcha flow (matches h.html getTaskData)
*
* 1. Clone payload, delete c field
* 2. Encrypt: hsw(1, msgpack.encode(payload_without_c))
* 3. Pack body: msgpack.encode([JSON.stringify(c), encrypted_bytes])
* 4. POST with content-type: application/octet-stream
* 5. Decrypt response: msgpack.decode(hsw(0, response_bytes))
*/
async _getCaptchaEncrypted(payload, cConfig) {
const url = `${HCAPTCHA_API}/getcaptcha/${this.config.siteKey}`;
const response = await this.http.post(url, payload, {
// Step 1: Clone and remove c field before encryption
const payloadClone = JSON.parse(JSON.stringify(payload));
delete payloadClone.c;
// Step 2: Encrypt via hsw(1, msgpack_bytes)
const msgpackPayload = encode(payloadClone);
const encrypted = await this.hsw.encrypt(msgpackPayload);
if (!encrypted) {
throw new Error('Encryption returned null/undefined');
}
// Step 3: Pack body = msgpack([c_string, encrypted_bytes])
const cString = typeof cConfig === 'string' ? cConfig : JSON.stringify(cConfig);
const body = encode([cString, encrypted]);
this.logger.info(`Body assembled: ${body.length} bytes`);
// Step 4: POST as octet-stream
const response = await this.http.post(url, body, {
headers: {
'content-type': 'application/octet-stream',
'origin': 'https://newassets.hcaptcha.com',
'referer': 'https://newassets.hcaptcha.com/',
},
});
return JSON.parse(response.body);
this.logger.info(`Response status: ${response.status}`);
// Step 5: Decrypt response
if (response.status === 200) {
const responseBytes = new Uint8Array(response.body);
const decrypted = await this.hsw.decrypt(responseBytes);
return decode(decrypted);
}
// Non-200: try plain JSON fallback
throw new Error(`getcaptcha failed with status ${response.status}: ${response.text()}`);
}
_getVersion() {
// hsw.js version - extract from assets or hardcode latest
return 'a9589f9';
}
}

View File

@@ -1,68 +0,0 @@
/**
* HTTP Client - TLS Fingerprint Spoofing Layer
*
* WARNING: Standard axios/node-fetch = instant death.
* Their JA3 fingerprint screams "I AM NODE.JS" to Cloudflare.
*
* We use got-scraping to mimic Chrome's TLS handshake.
*/
import { gotScraping } from 'got-scraping';
export class HttpClient {
constructor(fingerprint = {}) {
this.fingerprint = fingerprint;
this.baseHeaders = this._buildHeaders();
}
_buildHeaders() {
// Chrome 120+ header order matters
// :method, :authority, :scheme, :path come first (HTTP2 pseudo-headers)
return {
'accept': '*/*',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.9',
'cache-control': 'no-cache',
'pragma': 'no-cache',
'sec-ch-ua': '"Chromium";v="120", "Google Chrome";v="120", "Not(A:Brand";v="99"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
'user-agent': this.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',
};
}
async get(url, options = {}) {
return gotScraping({
url,
method: 'GET',
headers: { ...this.baseHeaders, ...options.headers },
headerGeneratorOptions: {
browsers: ['chrome'],
operatingSystems: ['windows'],
},
...options,
});
}
async post(url, body, options = {}) {
return gotScraping({
url,
method: 'POST',
headers: {
...this.baseHeaders,
'content-type': 'application/json',
...options.headers
},
body: typeof body === 'string' ? body : JSON.stringify(body),
headerGeneratorOptions: {
browsers: ['chrome'],
operatingSystems: ['windows'],
},
...options,
});
}
}

View File

@@ -1,156 +0,0 @@
/**
* 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;
}
}

View File

@@ -2,14 +2,24 @@
* Payload Builder - Assembling the Final Form
*
* Takes all our crafted components and stitches them into
* the exact JSON structure hCaptcha expects.
* the exact structure hCaptcha expects.
*
* From h.html source, the payload object (s) contains:
* {v, sitekey, host, hl, motionData, n, c, rqdata, pst, pd, pdc, pem...}
*
* IMPORTANT: The 'c' field is included in the payload object but gets
* REMOVED before encryption. The encrypted body is then packed as:
* msgpack.encode([JSON.stringify(c), encrypted_payload_without_c])
*/
export class PayloadBuilder {
/**
* Build the getcaptcha request payload
*
* The returned object includes 'c' - the caller is responsible for
* cloning and deleting 'c' before encryption (matching h.html behavior).
*/
static build({ siteKey, host, n, c, motionData }) {
static build({ siteKey, host, n, c, motionData, rqdata = '' }) {
const now = Date.now();
return {
@@ -20,50 +30,43 @@ export class PayloadBuilder {
// Challenge response
n, // Proof of work from hsw.js
c: JSON.stringify(c), // Config from checksiteconfig
// c field — will be stripped before encryption,
// then used separately in body packing as msgpack([c_string, encrypted])
c: typeof c === 'string' ? c : JSON.stringify(c),
// Motion telemetry
motionData: JSON.stringify(motionData),
motionData: typeof motionData === 'string'
? motionData
: JSON.stringify(motionData),
// Timestamps
prev: {
// Additional fields from h.html source
rqdata, // Request data from checksiteconfig
pst: false, // Previous success token
// Performance / detection data
pd: JSON.stringify({
si: now - 5000, // Script init
ce: now - 4500, // Challenge end
cs: now - 4000, // Challenge start
re: now - 500, // Response end
rs: now - 1000, // Response start
}),
pdc: JSON.stringify({}), // Performance data cached
pem: JSON.stringify({}), // Performance event map
// Previous state
prev: JSON.stringify({
escaped: false,
passed: false,
expiredChallenge: false,
expiredResponse: false,
},
// Widget metadata
d: PayloadBuilder._generateWidgetData(host, now),
// Response type
pst: false, // Previous success token
}),
};
}
/**
* Generate widget embedding data
*/
static _generateWidgetData(host, timestamp) {
return {
gt: 0, // Widget type
ct: timestamp - 1000, // Creation time
fc: 1, // Frame count
ff: false, // First frame
// Fake performance metrics
pd: {
si: timestamp - 5000, // Script init
ce: timestamp - 4500, // Challenge end
cs: timestamp - 4000, // Challenge start
re: timestamp - 500, // Response end
rs: timestamp - 1000, // Response start
},
};
}
/**
* Build form-encoded payload (alternative format)
* Build form-encoded payload (alternative format for non-encrypted requests)
*/
static buildFormData(data) {
const params = new URLSearchParams();

498
src/hcaptcha_solver.js Normal file
View File

@@ -0,0 +1,498 @@
'use strict';
/**
* hCaptcha Solver (Node.js)
*
* 完整流程:
* checksiteconfig → hsw(req) 计算 n → 构建加密体 → getcaptcha → 解密响应 → 拿 token
*
* 使用 hsw.js 在 Node 沙盒中运行(全局污染方式)
*/
const { readFileSync } = require('fs');
const { join } = require('path');
const msgpack = require('msgpack-lite');
const { createBrowserEnvironment } = require('./sandbox/mocks/index');
const { Logger } = require('./utils/logger');
const logger = new Logger('hcaptcha_solver');
// 保存原始 fetch在全局被 mock 污染之前)
const realFetch = globalThis.fetch;
// ── 常量 ──────────────────────────────────────────────────────
const HCAPTCHA_API = 'https://api.hcaptcha.com';
const HCAPTCHA_JS = 'https://js.hcaptcha.com/1/api.js';
const HCAPTCHA_CDN = 'https://newassets.hcaptcha.com';
// ── 伪造 motion data ──────────────────────────────────────────
function generateMotionData() {
const now = Date.now();
const st = now - 3000 - Math.floor(Math.random() * 1000);
// 随机鼠标轨迹
const mm = [];
let x = 200 + Math.floor(Math.random() * 100);
let y = 300 + Math.floor(Math.random() * 100);
const steps = 15 + Math.floor(Math.random() * 10);
for (let i = 0; i < steps; i++) {
x += Math.floor(Math.random() * 6) - 3;
y += Math.floor(Math.random() * 6) - 3;
mm.push([x, y, st + i * (80 + Math.floor(Math.random() * 40))]);
}
return {
st,
dct: st + 200 + Math.floor(Math.random() * 100),
mm,
'mm-mp': 15.42857142857143,
md: [[mm[0][0], mm[0][1], mm[0][2] + 500]],
'md-mp': 0,
mu: [[mm[0][0], mm[0][1], mm[0][2] + 600]],
'mu-mp': 0,
kd: [],
'kd-mp': 0,
ku: [],
'ku-mp': 0,
topLevel: {
st,
sc: {
availWidth: 1920,
availHeight: 1040,
width: 1920,
height: 1080,
colorDepth: 24,
pixelDepth: 24,
availLeft: 0,
availTop: 0,
},
nv: {
hardwareConcurrency: 8,
deviceMemory: 8,
},
dr: '',
inv: false,
exec: false,
},
v: 1,
};
}
// ── HSW 沙盒管理 ──────────────────────────────────────────────
class HswBridge {
constructor() {
this.hswFn = null;
this.initialized = false;
this._savedGlobals = {};
}
/**
* 将 mock 注入全局,加载并执行 hsw.js
* @param {string} hswPath - hsw.js 文件路径
* @param {object} fingerprint - 指纹覆盖
*/
async init(hswPath, fingerprint = {}) {
if (this.initialized) return;
const env = createBrowserEnvironment(fingerprint);
// 保存原始全局
const keys = ['window', 'document', 'navigator', 'screen', 'location',
'localStorage', 'sessionStorage', 'crypto', 'performance',
'self', 'top', 'parent', 'fetch', 'XMLHttpRequest'];
for (const k of keys) {
if (k in globalThis) this._savedGlobals[k] = globalThis[k];
}
// 注入全局
const force = (obj, prop, val) => {
Object.defineProperty(obj, prop, {
value: val, writable: true, configurable: true, enumerable: true,
});
};
force(globalThis, 'window', env.window);
force(globalThis, 'document', env.document);
force(globalThis, 'navigator', env.navigator);
force(globalThis, 'screen', env.screen);
force(globalThis, 'location', env.location);
force(globalThis, 'localStorage', env.localStorage);
force(globalThis, 'sessionStorage', env.sessionStorage);
force(globalThis, 'crypto', env.crypto);
force(globalThis, 'performance', env.performance);
force(globalThis, 'self', env.window);
force(globalThis, 'top', env.window);
force(globalThis, 'parent', env.window);
// 浏览器 API
globalThis.fetch = env.window.fetch;
globalThis.btoa = env.window.btoa;
globalThis.atob = env.window.atob;
globalThis.setTimeout = env.window.setTimeout;
globalThis.setInterval = env.window.setInterval;
globalThis.clearTimeout = env.window.clearTimeout;
globalThis.clearInterval = env.window.clearInterval;
globalThis.TextEncoder = env.window.TextEncoder;
globalThis.TextDecoder = env.window.TextDecoder;
globalThis.requestAnimationFrame = env.window.requestAnimationFrame;
globalThis.cancelAnimationFrame = env.window.cancelAnimationFrame;
// 加载 hsw.js
const code = readFileSync(hswPath, 'utf-8');
logger.info(`hsw.js 已加载 (${(code.length / 1024).toFixed(1)} KB)`);
try {
const fn = new Function(`(function() { ${code} })();`);
fn();
} catch (err) {
logger.error(`hsw.js 执行失败: ${err.message}`);
throw err;
}
// 查找 hsw 函数
if (typeof globalThis.window?.hsw === 'function') {
this.hswFn = globalThis.window.hsw;
} else if (typeof globalThis.hsw === 'function') {
this.hswFn = globalThis.hsw;
}
if (!this.hswFn) {
throw new Error('hsw function not found after execution');
}
this.initialized = true;
logger.success('Bridge 已就绪');
}
/** 计算 PoW n 值: hsw(req_jwt_string) */
async getN(req) {
return this.hswFn(req);
}
/** 加密请求体: hsw(1, data) */
async encrypt(data) {
return this.hswFn(1, data);
}
/** 解密响应体: hsw(0, data) */
async decrypt(data) {
return this.hswFn(0, data);
}
}
// ── HTTP 工具 ─────────────────────────────────────────────────
const DEFAULT_HEADERS = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.9',
'sec-ch-ua': '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
};
async function httpGet(url, headers = {}) {
const resp = await realFetch(url, {
method: 'GET',
headers: { ...DEFAULT_HEADERS, ...headers },
});
const text = await resp.text();
logger.info(`HTTP GET ${url.substring(0, 80)}... → ${resp.status}`);
return { status: resp.status, text, headers: resp.headers };
}
async function httpPost(url, body, headers = {}) {
const isBuffer = body instanceof Uint8Array || Buffer.isBuffer(body);
const opts = {
method: 'POST',
headers: {
...DEFAULT_HEADERS,
...(isBuffer
? { 'Content-Type': 'application/octet-stream' }
: { 'Content-Type': 'application/x-www-form-urlencoded' }),
...headers,
},
body: isBuffer ? body : body,
};
const resp = await realFetch(url, opts);
logger.info(`HTTP POST ${url.substring(0, 80)}... → ${resp.status}`);
return resp;
}
// ── 主求解器 ──────────────────────────────────────────────────
class HCaptchaSolver {
/**
* @param {object} opts
* @param {string} opts.sitekey
* @param {string} opts.host - 嵌入 hCaptcha 的站点域名
* @param {string} [opts.rqdata] - 可选附加数据
* @param {string} [opts.hswPath] - hsw.js 本地路径
*/
constructor(opts) {
this.sitekey = opts.sitekey;
this.host = opts.host;
this.rqdata = opts.rqdata || null;
this.hswPath = opts.hswPath || join(__dirname, '../asset/hsw.js');
this.bridge = new HswBridge();
this.version = null;
}
/** 1. 从 api.js 获取最新版本 hash */
async fetchVersion() {
const { text } = await httpGet(HCAPTCHA_JS);
// 尝试多种模式匹配版本 hash
const patterns = [
/captcha\/v1\/([a-f0-9]{40})\//i, // captcha/v1/HASH/
/\/c\/([a-f0-9]{40})\//i, // /c/HASH/
/v=([a-f0-9]{40})/i, // v=HASH
/["']([a-f0-9]{40})["']/, // 直接引用的40位hash
];
for (const pat of patterns) {
const m = text.match(pat);
if (m) {
this.version = m[1];
logger.info(`获取到最新 hCaptcha 版本: ${this.version}`);
return this.version;
}
}
throw new Error('无法从 api.js 提取版本 hash');
}
/** 2. 初始化 Bridge加载 hsw.js 到沙盒) */
async initBridge() {
let hswCode;
// 优先用本地 hsw.js
try {
readFileSync(this.hswPath);
hswCode = this.hswPath;
logger.info(`使用本地 hsw.js: ${this.hswPath}`);
} catch {
// 从 CDN 下载
if (!this.version) await this.fetchVersion();
const url = `${HCAPTCHA_CDN}/c/${this.version}/hsw.js`;
logger.info(`从 CDN 下载 hsw.js: ${url}`);
const { text } = await httpGet(url);
// 写入临时文件
const tmp = join(__dirname, '../asset/hsw_latest.js');
require('fs').writeFileSync(tmp, text);
hswCode = tmp;
logger.info(`hsw.js 已保存: ${tmp}`);
}
await this.bridge.init(hswCode, {
host: this.host,
});
}
/** 3. checksiteconfig */
async checkSiteConfig() {
if (!this.version) await this.fetchVersion();
const params = new URLSearchParams({
v: this.version,
host: this.host,
sitekey: this.sitekey,
sc: '1',
swa: '1',
spst: '0',
});
const t0 = Date.now();
const url = `${HCAPTCHA_API}/checksiteconfig?${params}`;
const resp = await realFetch(url, {
method: 'POST',
headers: {
...DEFAULT_HEADERS,
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'https://newassets.hcaptcha.com',
'Referer': 'https://newassets.hcaptcha.com/',
},
body: params.toString(),
});
const data = await resp.json();
const dur = Date.now() - t0;
logger.info(`HTTP POST ${url.substring(0, 80)}... → ${resp.status}`);
// 提取 set-cookiehmt_id 等),后续请求需要带上
const rawCookie = resp.headers.get('set-cookie') || '';
const hmtMatch = rawCookie.match(/hmt_id=[^;]+/);
this.cookie = hmtMatch ? hmtMatch[0] : '';
if (this.cookie) logger.info(`获取到 cookie: ${this.cookie}`);
const pass = data.pass !== false;
const cType = data.c?.type || 'unknown';
logger.info(`checksiteconfig: pass=${pass}, type=${cType}, duration=${dur}ms`);
if (!data.c) {
throw new Error(`checksiteconfig 缺少 challenge 字段: ${JSON.stringify(data)}`);
}
return data;
}
/** 4. 构建并加密请求体 */
async buildEncryptedBody(challenge) {
// 计算 PoW n 值
logger.info('计算 PoW n 值...');
const n = await this.bridge.getN(challenge.c.req);
logger.info(`n = ${typeof n === 'string' ? n.substring(0, 40) + '...' : n}`);
// 构建 payload
const motionData = generateMotionData();
const payload = {
v: this.version,
sitekey: this.sitekey,
host: this.host,
hl: 'en',
motionData: JSON.stringify(motionData),
n,
c: JSON.stringify(challenge.c),
pst: false,
};
// 如果有 rqdata加入
if (this.rqdata) {
payload.rqdata = this.rqdata;
}
logger.info('构建加密请求体...');
// ── 正确的加密流程(与 flow_manager.js / h.html 一致)──
// Step 1: 克隆 payload去掉 c 字段c 会单独放在外层)
const payloadClone = { ...payload };
const cValue = payloadClone.c;
delete payloadClone.c;
// Step 2: msgpack 编码后再加密: hsw(1, msgpack.encode(payload_without_c))
const msgpackPayload = msgpack.encode(payloadClone);
logger.info(`msgpack 编码大小: ${msgpackPayload.length} bytes`);
const encrypted = await this.bridge.encrypt(msgpackPayload);
// 诊断encrypted 的类型和内容
const etype = typeof encrypted;
const ector = encrypted?.constructor?.name || 'unknown';
logger.info(`encrypt 返回类型: typeof=${etype}, constructor=${ector}`);
if (encrypted instanceof Uint8Array || Buffer.isBuffer(encrypted)) {
logger.info(`encrypt 返回 Uint8Array/Buffer, length=${encrypted.length}`);
} else if (typeof encrypted === 'string') {
logger.info(`encrypt 返回 string, length=${encrypted.length}`);
} else {
logger.info(`encrypt 返回: ${JSON.stringify(encrypted)?.substring(0, 200)}`);
}
if (!encrypted) {
throw new Error('hsw encrypt 返回 null/undefined');
}
// Step 3: 二次打包: body = msgpack.encode([JSON.stringify(c), encrypted_bytes])
const cString = typeof cValue === 'string' ? cValue : JSON.stringify(cValue);
const body = msgpack.encode([cString, encrypted]);
logger.info(`最终 body 大小: ${body.length} bytes, first10=[${Array.from(body.slice(0, 10)).map(b => '0x' + b.toString(16).padStart(2, '0')).join(',')}]`);
// 导出加密体供 curl_cffi 测试
require('fs').writeFileSync('body.bin', body);
logger.info('已导出加密体到 body.bin');
return body;
}
/** 5. getcaptcha → 拿 token */
async getCaptcha(encryptedBody) {
const url = `${HCAPTCHA_API}/getcaptcha/${this.sitekey}`;
const resp = await realFetch(url, {
method: 'POST',
headers: {
...DEFAULT_HEADERS,
'Accept': 'application/json, application/octet-stream',
'Content-Type': 'application/octet-stream',
'Origin': 'https://newassets.hcaptcha.com',
'Referer': 'https://newassets.hcaptcha.com/',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
'sec-fetch-storage-access': 'none',
'priority': 'u=1, i',
...(this.cookie ? { 'Cookie': this.cookie } : {}),
},
body: encryptedBody,
});
logger.info(`HTTP POST ${url}${resp.status}`);
logger.info(`响应 content-type: ${resp.headers.get('content-type')}`);
if (!resp.ok) {
const errText = await resp.text();
logger.error(`getcaptcha 失败 (${resp.status}): ${errText.substring(0, 200)}`);
throw new Error(`getcaptcha HTTP ${resp.status}: ${errText.substring(0, 200)}`);
}
// 尝试解密响应
const contentType = resp.headers.get('content-type') || '';
let result;
if (contentType.includes('json')) {
// 明文 JSON 响应
result = await resp.json();
} else {
// 二进制加密响应
const rawBuf = await resp.arrayBuffer();
const raw = new Uint8Array(rawBuf);
logger.info('解密响应...');
const decrypted = await this.bridge.decrypt(raw);
try {
result = msgpack.decode(decrypted);
} catch {
// 可能是 JSON 字符串
result = JSON.parse(new TextDecoder().decode(decrypted));
}
}
const pass = result.pass || result.generated_pass_UUID;
logger.info(`getcaptcha 结果: pass=${!!pass}`);
return result;
}
/** 完整求解流程 */
async solve() {
logger.info(`开始求解 sitekey=${this.sitekey.substring(0, 12)}... host=${this.host}`);
const t0 = Date.now();
try {
// 步骤 1: 获取版本
await this.fetchVersion();
// 步骤 2: 初始化 Bridge
await this.initBridge();
// 步骤 3: checksiteconfig
const config = await this.checkSiteConfig();
// 步骤 4: 构建加密体
const body = await this.buildEncryptedBody(config);
// 步骤 5: getcaptcha
const result = await this.getCaptcha(body);
const token = result.generated_pass_UUID || result.pass;
const dur = ((Date.now() - t0) / 1000).toFixed(2);
if (token && typeof token === 'string' && token.startsWith('P1_')) {
logger.success(`✅ 求解成功! (${dur}s)`);
logger.info(`Token: ${token.substring(0, 50)}...`);
return { success: true, token, result };
} else if (result.pass === true) {
logger.success(`✅ pass=true (${dur}s)`);
return { success: true, token: token || true, result };
} else {
logger.error(`❌ 求解失败: ${JSON.stringify(result).substring(0, 200)}`);
return { success: false, result };
}
} catch (err) {
logger.error(`求解异常: ${err.message}`);
logger.error(err.stack);
return { success: false, error: err.message };
}
}
}
module.exports = { HCaptchaSolver, HswBridge, generateMotionData };

View File

@@ -1,205 +1,113 @@
'use strict';
/**
* HSW Runner - The Execution Chamber (Global Pollution Method)
* HSW Runner
* 用 vm 沙盒加载 hsw.js注入 mock window调用 window.hsw(req, callback)
*
* No vm sandbox. We directly inject our mocked browser objects
* into the global scope, then execute the hsw.js code.
* 用法:
* const { solveHsw } = require('./hsw_runner');
* const token = await solveHsw({ req: 'xxx' });
*/
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { createBrowserEnvironment } from './mocks/index.js';
import { Logger } from '../utils/logger.js';
const vm = require('vm');
const fs = require('fs');
const path = require('path');
const __dirname = dirname(fileURLToPath(import.meta.url));
const logger = new Logger('HswRunner');
const HSW_PATH = path.resolve(__dirname, '../../asset/hsw.js');
export class HswRunner {
constructor(options = {}) {
this.hswPath = options.hswPath || join(__dirname, '../../assets/hsw.js');
this.fingerprint = options.fingerprint || {};
this.initialized = false;
this.originalGlobals = {};
this.hswFn = null;
// ── 加载 window mock ─────────────────────────────────────────
const windowMock = require('./mocks/window');
// ── 读取 hsw.js 源码(只读一次) ─────────────────────────────
const hswCode = fs.readFileSync(HSW_PATH, 'utf-8');
// ── 构建 vm 上下文 ───────────────────────────────────────────
function buildContext() {
// 以 windowMock 为基础展开,避免 vm 访问 global 时找不到基础全局量
const ctx = Object.create(null);
// 把 windowMock 上的所有 key 复制进 ctx
for (const key of Reflect.ownKeys(windowMock)) {
try { ctx[key] = windowMock[key]; } catch (_) {}
}
async init() {
if (this.initialized) return;
// vm 必需的几个全局
ctx.global = ctx;
ctx.globalThis = ctx;
ctx.window = ctx;
ctx.self = ctx;
logger.info('Initializing sandbox via global pollution...');
// 把 console 透传(调试用)
ctx.console = console;
// Create the fake browser environment
const env = createBrowserEnvironment(this.fingerprint);
// 保证 Promise / setTimeout 等是 vm 里可用的
ctx.Promise = Promise;
ctx.setTimeout = setTimeout;
ctx.clearTimeout = clearTimeout;
ctx.setInterval = setInterval;
ctx.clearInterval = clearInterval;
ctx.queueMicrotask = queueMicrotask;
// Save original globals (in case we need to restore)
this._saveOriginalGlobals();
// Pollute global scope
this._injectGlobals(env);
// Load and execute hsw.js
let hswCode;
try {
hswCode = readFileSync(this.hswPath, 'utf-8');
logger.info(`Loaded hsw.js (${(hswCode.length / 1024).toFixed(1)} KB)`);
} catch (err) {
throw new Error(`Failed to load hsw.js from ${this.hswPath}: ${err.message}`);
}
// Execute in global scope
try {
// Wrap in IIFE to avoid strict mode issues
const wrappedCode = `(function() { ${hswCode} })();`;
const execFn = new Function(wrappedCode);
execFn();
logger.info('hsw.js executed successfully');
} catch (err) {
logger.error(`hsw.js execution failed: ${err.message}`);
logger.error(`This error tells you what property hsw.js tried to access.`);
logger.error(`Add it to the appropriate mock file and try again.`);
throw err;
}
// Check if hsw function is now available
// hsw.js attaches to window.hsw, not globalThis.hsw
if (typeof globalThis.window?.hsw === 'function') {
logger.info('Found hsw function on window.hsw');
this.hswFn = globalThis.window.hsw;
} else if (typeof globalThis.hsw === 'function') {
logger.info('Found hsw function on globalThis.hsw');
this.hswFn = globalThis.hsw;
} else {
// Search other possible locations
const locations = [
['window', 'hsw'],
['self', 'hsw'],
['globalThis', 'hcaptcha'],
];
for (const [obj, prop] of locations) {
const target = globalThis[obj];
if (target && typeof target[prop] === 'function') {
logger.info(`Found function at ${obj}.${prop}`);
this.hswFn = target[prop];
break;
}
}
}
if (!this.hswFn) {
logger.warn('hsw function not found after execution');
logger.warn('Check hsw.js structure for export pattern');
} else {
logger.success('HSW runner initialized');
}
this.initialized = true;
}
_saveOriginalGlobals() {
const keys = ['window', 'document', 'navigator', 'screen', 'location',
'localStorage', 'sessionStorage', 'crypto', 'performance',
'self', 'top', 'parent', 'fetch', 'XMLHttpRequest'];
for (const key of keys) {
if (key in globalThis) {
this.originalGlobals[key] = globalThis[key];
}
}
}
_injectGlobals(env) {
// Force override read-only properties
const forceSet = (obj, prop, value) => {
Object.defineProperty(obj, prop, {
value,
writable: true,
configurable: true,
enumerable: true,
});
};
// Core browser objects (some are read-only in Node, must force)
forceSet(globalThis, 'window', env.window);
forceSet(globalThis, 'document', env.document);
forceSet(globalThis, 'navigator', env.navigator);
forceSet(globalThis, 'screen', env.screen);
// Window properties that scripts access directly
forceSet(globalThis, 'location', env.location);
forceSet(globalThis, 'localStorage', env.localStorage);
forceSet(globalThis, 'sessionStorage', env.sessionStorage);
forceSet(globalThis, 'crypto', env.crypto);
forceSet(globalThis, 'performance', env.performance);
// Self-references
forceSet(globalThis, 'self', env.window);
forceSet(globalThis, 'top', env.window);
forceSet(globalThis, 'parent', env.window);
// Browser APIs from window
globalThis.fetch = env.window.fetch;
globalThis.XMLHttpRequest = env.window.XMLHttpRequest;
globalThis.btoa = env.window.btoa;
globalThis.atob = env.window.atob;
globalThis.setTimeout = env.window.setTimeout;
globalThis.setInterval = env.window.setInterval;
globalThis.clearTimeout = env.window.clearTimeout;
globalThis.clearInterval = env.window.clearInterval;
globalThis.requestAnimationFrame = env.window.requestAnimationFrame;
globalThis.cancelAnimationFrame = env.window.cancelAnimationFrame;
// Additional globals from window
globalThis.Event = env.window.Event;
globalThis.CustomEvent = env.window.CustomEvent;
globalThis.MessageEvent = env.window.MessageEvent;
globalThis.Blob = env.window.Blob;
globalThis.File = env.window.File;
globalThis.FileReader = env.window.FileReader;
globalThis.URL = env.window.URL;
globalThis.URLSearchParams = env.window.URLSearchParams;
globalThis.TextEncoder = env.window.TextEncoder;
globalThis.TextDecoder = env.window.TextDecoder;
globalThis.Worker = env.window.Worker;
logger.debug('Global scope polluted with browser mocks');
}
restoreGlobals() {
for (const [key, value] of Object.entries(this.originalGlobals)) {
if (value !== undefined) {
try {
Object.defineProperty(globalThis, key, {
value,
writable: true,
configurable: true,
});
} catch (e) {
// Some properties can't be restored
}
}
}
logger.debug('Original globals restored');
}
async getN(req) {
if (!this.initialized) {
await this.init();
}
if (typeof this.hswFn !== 'function') {
throw new Error('hsw function not available. Check hsw.js structure.');
}
logger.debug(`Computing n for req: ${req.substring(0, 32)}...`);
try {
// hsw(req) returns a promise that resolves to the 'n' value
const n = await this.hswFn(req);
logger.debug(`Computed n: ${typeof n === 'string' ? n.substring(0, 32) + '...' : n}`);
return n;
} catch (err) {
logger.error(`Failed to compute n: ${err.message}`);
logger.error(`Stack: ${err.stack}`);
throw err;
}
}
return vm.createContext(ctx);
}
// ── 编译脚本(只编译一次,复用) ─────────────────────────────
const hswScript = new vm.Script(hswCode, {
filename: 'hsw.js',
lineOffset: 0,
});
/**
* 在沙盒里执行 hsw.js并调用 window.hsw(req)
*
* @param {string} req - hsw 第一个参数(来自 checksiteconfig 响应的 req 字段)
* @param {object} opts
* @param {number} opts.timeout - 超时毫秒,默认 10000
* @returns {Promise<string>} token
*/
async function solveHsw(req, opts = {}) {
const timeout = opts.timeout ?? 10000;
const ctx = buildContext();
// 运行 hsw.js注册 window.hsw
hswScript.runInContext(ctx, { timeout });
if (typeof ctx.hsw !== 'function') {
throw new Error('[hsw_runner] hsw.js 未正确导出 window.hsw 函数');
}
// window.hsw(Ig, tH) 返回 Promise<string>
// 第二个参数 tH 在部分版本是 callback部分版本未使用先传 undefined
const result = await Promise.race([
ctx.hsw(req, undefined),
new Promise((_, rej) =>
setTimeout(() => rej(new Error('[hsw_runner] 超时 ' + timeout + 'ms')), timeout)
),
]);
return result;
}
module.exports = { solveHsw };
// ── 直接执行时的调试入口 ─────────────────────────────────────
if (require.main === module) {
const testReq = process.argv[2] || '';
if (!testReq) {
console.error('用法: node hsw_runner.js <req_string>');
console.error('例如: node hsw_runner.js "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."');
process.exit(1);
}
solveHsw(testReq)
.then(token => {
console.log('[✓] token:', token);
})
.catch(err => {
console.error('[✗] 错误:', err.message);
if (err.stack) console.error(err.stack);
process.exit(1);
});
}

View File

@@ -0,0 +1,47 @@
'use strict';
/**
* P0-B: Bot 痕迹字段黑名单
* 这些字段在 tH=154/155 被逐一枚举检测,存在即判 bot。
* Proxy 的 get/has 拦截直接返回 undefined/false。
*/
const BOT_KEYS = new Set([
'webdriver',
'_phantom', '__nightmare', '_selenium', '__phantomas',
'callPhantom', 'callSelenium', 'callSelenium',
'domAutomation', 'domAutomationController',
'spawn', 'awesomium', '$wdc_',
'hcaptchaCallbackZenno',
'_Selenium_IDE_Recorder',
'_WEBDRIVER_ELEM_CACHE',
'__webdriver_script_fn',
'__webdriver_script_func',
'__driver_evaluate',
'__webdriver_evaluate',
'__selenium_evaluate',
'__fxdriver_evaluate',
'__driver_unwrapped',
'__webdriver_unwrapped',
'__selenium_unwrapped',
'__fxdriver_unwrapped',
'cdc_adoQpoasnfa76pfcZLmcfl_Array',
'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
'cdc_adoQpoasnfa76pfcZLmcfl_Symbol',
'CDCJStestRunStatus',
'$cdc_asdjflasutopfhvcZLmcfl_',
'$chrome_asyncScriptInfo',
]);
function isBotKey(key) {
if (BOT_KEYS.has(key)) return true;
if (typeof key === 'string' && (
key.startsWith('cdc_') ||
key.startsWith('$cdc_') ||
key.includes('webdriver') ||
key.includes('selenium') ||
key.includes('phantom')
)) return true;
return false;
}
module.exports = { BOT_KEYS, isBotKey };

View File

@@ -1,560 +1,132 @@
'use strict';
/**
* Canvas & WebGL Context Mocks
*
* Canvas fingerprinting is a major detection vector.
* hsw.js uses canvas to generate unique device signatures.
* P1: Canvas mock
* hsw 检测HTMLCanvasElement / CanvasRenderingContext2D / fillStyle 默认值 / measureText
*/
import webglProps from '../stubs/webgl_props.json' with { type: 'json' };
const { createNative, nativeClass } = require('./native');
export function createCanvasRenderingContext2D(canvas, fingerprint = {}) {
let fillStyle = '#000000';
let strokeStyle = '#000000';
let font = '10px sans-serif';
let textAlign = 'start';
let textBaseline = 'alphabetic';
let globalAlpha = 1;
let globalCompositeOperation = 'source-over';
let lineCap = 'butt';
let lineJoin = 'miter';
let lineWidth = 1;
let miterLimit = 10;
let shadowBlur = 0;
let shadowColor = 'rgba(0, 0, 0, 0)';
let shadowOffsetX = 0;
let shadowOffsetY = 0;
let imageSmoothingEnabled = true;
let imageSmoothingQuality = 'low';
// 2D Context
const CanvasRenderingContext2D = createNative('CanvasRenderingContext2D', function () {});
CanvasRenderingContext2D.prototype = {
constructor: CanvasRenderingContext2D,
fillStyle: '#000000', // P1: 默认值必须是黑色
strokeStyle: '#000000',
font: '10px sans-serif',
textAlign: 'start',
textBaseline: 'alphabetic',
globalAlpha: 1,
lineWidth: 1,
fillRect: createNative('fillRect', function () {}),
strokeRect: createNative('strokeRect', function () {}),
clearRect: createNative('clearRect', function () {}),
fillText: createNative('fillText', function () {}),
strokeText: createNative('strokeText', function () {}),
beginPath: createNative('beginPath', function () {}),
closePath: createNative('closePath', function () {}),
moveTo: createNative('moveTo', function () {}),
lineTo: createNative('lineTo', function () {}),
arc: createNative('arc', function () {}),
fill: createNative('fill', function () {}),
stroke: createNative('stroke', function () {}),
save: createNative('save', function () {}),
restore: createNative('restore', function () {}),
scale: createNative('scale', function () {}),
rotate: createNative('rotate', function () {}),
translate: createNative('translate', function () {}),
drawImage: createNative('drawImage', function () {}),
getImageData: createNative('getImageData', function (x, y, w, h) {
return { data: new Uint8ClampedArray(w * h * 4), width: w, height: h };
}),
putImageData: createNative('putImageData', function () {}),
createImageData: createNative('createImageData', function (w, h) {
return { data: new Uint8ClampedArray(w * h * 4), width: w, height: h };
}),
measureText: createNative('measureText', function (text) {
// 近似真实 Chrome 的字体测量Helvetica 10px
return {
width: text.length * 5.5,
actualBoundingBoxAscent: 7,
actualBoundingBoxDescent: 2,
fontBoundingBoxAscent: 8,
fontBoundingBoxDescent: 2,
};
}),
setTransform: createNative('setTransform', function () {}),
resetTransform: createNative('resetTransform', function () {}),
clip: createNative('clip', function () {}),
isPointInPath: createNative('isPointInPath', function () { return false; }),
createLinearGradient: createNative('createLinearGradient', function () {
return { addColorStop: createNative('addColorStop', function () {}) };
}),
createRadialGradient: createNative('createRadialGradient', function () {
return { addColorStop: createNative('addColorStop', function () {}) };
}),
createPattern: createNative('createPattern', function () { return null; }),
canvas: null, // 会在 createElement 里回填
};
const stateStack = [];
const ctx = {
canvas,
// State
get fillStyle() { return fillStyle; },
set fillStyle(v) { fillStyle = v; },
get strokeStyle() { return strokeStyle; },
set strokeStyle(v) { strokeStyle = v; },
get font() { return font; },
set font(v) { font = v; },
get textAlign() { return textAlign; },
set textAlign(v) { textAlign = v; },
get textBaseline() { return textBaseline; },
set textBaseline(v) { textBaseline = v; },
get globalAlpha() { return globalAlpha; },
set globalAlpha(v) { globalAlpha = v; },
get globalCompositeOperation() { return globalCompositeOperation; },
set globalCompositeOperation(v) { globalCompositeOperation = v; },
get lineCap() { return lineCap; },
set lineCap(v) { lineCap = v; },
get lineJoin() { return lineJoin; },
set lineJoin(v) { lineJoin = v; },
get lineWidth() { return lineWidth; },
set lineWidth(v) { lineWidth = v; },
get miterLimit() { return miterLimit; },
set miterLimit(v) { miterLimit = v; },
get shadowBlur() { return shadowBlur; },
set shadowBlur(v) { shadowBlur = v; },
get shadowColor() { return shadowColor; },
set shadowColor(v) { shadowColor = v; },
get shadowOffsetX() { return shadowOffsetX; },
set shadowOffsetX(v) { shadowOffsetX = v; },
get shadowOffsetY() { return shadowOffsetY; },
set shadowOffsetY(v) { shadowOffsetY = v; },
get imageSmoothingEnabled() { return imageSmoothingEnabled; },
set imageSmoothingEnabled(v) { imageSmoothingEnabled = v; },
get imageSmoothingQuality() { return imageSmoothingQuality; },
set imageSmoothingQuality(v) { imageSmoothingQuality = v; },
// Line styles
lineDashOffset: 0,
getLineDash() { return []; },
setLineDash() {},
// State stack
save() {
stateStack.push({
fillStyle, strokeStyle, font, textAlign, textBaseline,
globalAlpha, globalCompositeOperation, lineCap, lineJoin,
lineWidth, miterLimit, shadowBlur, shadowColor,
shadowOffsetX, shadowOffsetY
});
},
restore() {
const state = stateStack.pop();
if (state) {
fillStyle = state.fillStyle;
strokeStyle = state.strokeStyle;
font = state.font;
textAlign = state.textAlign;
textBaseline = state.textBaseline;
globalAlpha = state.globalAlpha;
globalCompositeOperation = state.globalCompositeOperation;
lineCap = state.lineCap;
lineJoin = state.lineJoin;
lineWidth = state.lineWidth;
miterLimit = state.miterLimit;
shadowBlur = state.shadowBlur;
shadowColor = state.shadowColor;
shadowOffsetX = state.shadowOffsetX;
shadowOffsetY = state.shadowOffsetY;
}
},
reset() {
fillStyle = '#000000';
strokeStyle = '#000000';
font = '10px sans-serif';
stateStack.length = 0;
},
// Transformations
getTransform() {
return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
},
setTransform() {},
resetTransform() {},
transform() {},
translate() {},
rotate() {},
scale() {},
// Drawing rectangles
clearRect() {},
fillRect() {},
strokeRect() {},
// Drawing text
fillText() {},
strokeText() {},
measureText(text) {
// Approximate text measurement
const fontSize = parseInt(font) || 10;
return {
width: text.length * fontSize * 0.6,
actualBoundingBoxAscent: fontSize * 0.8,
actualBoundingBoxDescent: fontSize * 0.2,
actualBoundingBoxLeft: 0,
actualBoundingBoxRight: text.length * fontSize * 0.6,
fontBoundingBoxAscent: fontSize,
fontBoundingBoxDescent: fontSize * 0.25,
emHeightAscent: fontSize * 0.8,
emHeightDescent: fontSize * 0.2,
hangingBaseline: fontSize * 0.8,
alphabeticBaseline: 0,
ideographicBaseline: fontSize * -0.2,
};
},
// Paths
beginPath() {},
closePath() {},
moveTo() {},
lineTo() {},
bezierCurveTo() {},
quadraticCurveTo() {},
arc() {},
arcTo() {},
ellipse() {},
rect() {},
roundRect() {},
fill() {},
stroke() {},
clip() {},
isPointInPath() { return false; },
isPointInStroke() { return false; },
// Drawing images
drawImage() {},
createImageData(width, height) {
const size = width * height * 4;
return {
width,
height,
data: new Uint8ClampedArray(size),
colorSpace: 'srgb',
};
},
getImageData(sx, sy, sw, sh) {
const size = sw * sh * 4;
const data = new Uint8ClampedArray(size);
// Fill with slight noise for fingerprinting
for (let i = 0; i < size; i += 4) {
const noise = fingerprint.canvasNoise || 0;
data[i] = noise; // R
data[i + 1] = noise; // G
data[i + 2] = noise; // B
data[i + 3] = 255; // A
}
return { width: sw, height: sh, data, colorSpace: 'srgb' };
},
putImageData() {},
// Gradients and patterns
createLinearGradient() {
return { addColorStop() {} };
},
createRadialGradient() {
return { addColorStop() {} };
},
createConicGradient() {
return { addColorStop() {} };
},
createPattern() {
return {};
},
// Filters
filter: 'none',
// Misc
drawFocusIfNeeded() {},
scrollPathIntoView() {},
};
return ctx;
}
export function createWebGLContext(type, fingerprint = {}) {
const props = { ...webglProps, ...fingerprint.webgl };
// Build parameter map
const parameters = {
// Vendor info
7936: props.vendor, // GL_VENDOR
7937: props.renderer, // GL_RENDERER
7938: props.version, // GL_VERSION
35724: props.shadingLanguageVersion, // GL_SHADING_LANGUAGE_VERSION
// Unmasked (via extension)
37445: props.unmaskedVendor, // UNMASKED_VENDOR_WEBGL
37446: props.unmaskedRenderer, // UNMASKED_RENDERER_WEBGL
// Limits
3379: props.maxTextureSize, // MAX_TEXTURE_SIZE
34076: props.maxCubeMapTextureSize, // MAX_CUBE_MAP_TEXTURE_SIZE
34024: props.maxRenderbufferSize, // MAX_RENDERBUFFER_SIZE
3386: props.maxViewportDims, // MAX_VIEWPORT_DIMS
34921: props.maxVertexAttribs, // MAX_VERTEX_ATTRIBS
36347: props.maxVertexUniformVectors, // MAX_VERTEX_UNIFORM_VECTORS
36348: props.maxVaryingVectors, // MAX_VARYING_VECTORS
36349: props.maxFragmentUniformVectors, // MAX_FRAGMENT_UNIFORM_VECTORS
35660: props.maxVertexTextureImageUnits, // MAX_VERTEX_TEXTURE_IMAGE_UNITS
34930: props.maxTextureImageUnits, // MAX_TEXTURE_IMAGE_UNITS
35661: props.maxCombinedTextureImageUnits, // MAX_COMBINED_TEXTURE_IMAGE_UNITS
// Precision
3408: props.aliasedLineWidthRange, // ALIASED_LINE_WIDTH_RANGE
3407: props.aliasedPointSizeRange, // ALIASED_POINT_SIZE_RANGE
};
const extensions = props.extensions || [];
const gl = {
canvas: null,
drawingBufferWidth: 300,
drawingBufferHeight: 150,
drawingBufferColorSpace: 'srgb',
// Parameter query
getParameter(pname) {
return parameters[pname] ?? null;
},
// Extension handling
getExtension(name) {
if (!extensions.includes(name)) return null;
if (name === 'WEBGL_debug_renderer_info') {
return {
UNMASKED_VENDOR_WEBGL: 37445,
UNMASKED_RENDERER_WEBGL: 37446,
};
}
if (name === 'EXT_texture_filter_anisotropic') {
return {
MAX_TEXTURE_MAX_ANISOTROPY_EXT: 34047,
TEXTURE_MAX_ANISOTROPY_EXT: 34046,
};
}
return {};
},
getSupportedExtensions() {
return [...extensions];
},
// Shader precision
getShaderPrecisionFormat(shaderType, precisionType) {
return {
rangeMin: 127,
rangeMax: 127,
precision: 23,
};
},
// Context state
isContextLost() { return false; },
getContextAttributes() {
return {
alpha: true,
antialias: true,
depth: true,
desynchronized: false,
failIfMajorPerformanceCaveat: false,
powerPreference: 'default',
premultipliedAlpha: true,
preserveDrawingBuffer: false,
stencil: false,
xrCompatible: false,
};
},
// Buffer operations
createBuffer() { return {}; },
deleteBuffer() {},
bindBuffer() {},
bufferData() {},
bufferSubData() {},
isBuffer() { return true; },
getBufferParameter() { return 0; },
// Shader operations
createShader() { return {}; },
deleteShader() {},
shaderSource() {},
compileShader() {},
getShaderParameter() { return true; },
getShaderInfoLog() { return ''; },
getShaderSource() { return ''; },
isShader() { return true; },
// Program operations
createProgram() { return {}; },
deleteProgram() {},
attachShader() {},
detachShader() {},
linkProgram() {},
useProgram() {},
validateProgram() {},
getProgramParameter() { return true; },
getProgramInfoLog() { return ''; },
isProgram() { return true; },
getAttachedShaders() { return []; },
// Attribute operations
getAttribLocation() { return 0; },
bindAttribLocation() {},
enableVertexAttribArray() {},
disableVertexAttribArray() {},
vertexAttribPointer() {},
vertexAttrib1f() {},
vertexAttrib2f() {},
vertexAttrib3f() {},
vertexAttrib4f() {},
vertexAttrib1fv() {},
vertexAttrib2fv() {},
vertexAttrib3fv() {},
vertexAttrib4fv() {},
getVertexAttrib() { return null; },
getVertexAttribOffset() { return 0; },
// Uniform operations
getUniformLocation() { return {}; },
getUniform() { return null; },
uniform1f() {},
uniform2f() {},
uniform3f() {},
uniform4f() {},
uniform1i() {},
uniform2i() {},
uniform3i() {},
uniform4i() {},
uniform1fv() {},
uniform2fv() {},
uniform3fv() {},
uniform4fv() {},
uniform1iv() {},
uniform2iv() {},
uniform3iv() {},
uniform4iv() {},
uniformMatrix2fv() {},
uniformMatrix3fv() {},
uniformMatrix4fv() {},
getActiveUniform() { return { name: '', size: 1, type: 5126 }; },
getActiveAttrib() { return { name: '', size: 1, type: 5126 }; },
// Texture operations
createTexture() { return {}; },
deleteTexture() {},
bindTexture() {},
activeTexture() {},
texImage2D() {},
texSubImage2D() {},
texParameterf() {},
texParameteri() {},
getTexParameter() { return 0; },
generateMipmap() {},
isTexture() { return true; },
copyTexImage2D() {},
copyTexSubImage2D() {},
compressedTexImage2D() {},
compressedTexSubImage2D() {},
// Framebuffer operations
createFramebuffer() { return {}; },
deleteFramebuffer() {},
bindFramebuffer() {},
framebufferTexture2D() {},
framebufferRenderbuffer() {},
checkFramebufferStatus() { return 36053; }, // FRAMEBUFFER_COMPLETE
getFramebufferAttachmentParameter() { return 0; },
isFramebuffer() { return true; },
// Renderbuffer operations
createRenderbuffer() { return {}; },
deleteRenderbuffer() {},
bindRenderbuffer() {},
renderbufferStorage() {},
getRenderbufferParameter() { return 0; },
isRenderbuffer() { return true; },
// Drawing operations
clear() {},
clearColor() {},
clearDepth() {},
clearStencil() {},
drawArrays() {},
drawElements() {},
finish() {},
flush() {},
readPixels() {},
// State operations
enable() {},
disable() {},
isEnabled() { return false; },
blendColor() {},
blendEquation() {},
blendEquationSeparate() {},
blendFunc() {},
blendFuncSeparate() {},
colorMask() {},
cullFace() {},
depthFunc() {},
depthMask() {},
depthRange() {},
frontFace() {},
lineWidth() {},
pixelStorei() {},
polygonOffset() {},
sampleCoverage() {},
scissor() {},
stencilFunc() {},
stencilFuncSeparate() {},
stencilMask() {},
stencilMaskSeparate() {},
stencilOp() {},
stencilOpSeparate() {},
viewport() {},
hint() {},
// Error handling
getError() { return 0; }, // NO_ERROR
// WebGL2 specific (if type is webgl2)
...(type === 'webgl2' ? getWebGL2Methods() : {}),
};
return gl;
}
function getWebGL2Methods() {
// WebGL context (浅实现,过类型检测)
function makeWebGLContext() {
return {
// WebGL2 additions
createVertexArray() { return {}; },
deleteVertexArray() {},
bindVertexArray() {},
isVertexArray() { return true; },
createSampler() { return {}; },
deleteSampler() {},
bindSampler() {},
isSampler() { return true; },
samplerParameteri() {},
samplerParameterf() {},
getSamplerParameter() { return 0; },
createTransformFeedback() { return {}; },
deleteTransformFeedback() {},
bindTransformFeedback() {},
isTransformFeedback() { return true; },
beginTransformFeedback() {},
endTransformFeedback() {},
transformFeedbackVaryings() {},
getTransformFeedbackVarying() { return null; },
pauseTransformFeedback() {},
resumeTransformFeedback() {},
createQuery() { return {}; },
deleteQuery() {},
isQuery() { return true; },
beginQuery() {},
endQuery() {},
getQuery() { return null; },
getQueryParameter() { return 0; },
fenceSync() { return {}; },
deleteSync() {},
isSync() { return true; },
clientWaitSync() { return 0; },
waitSync() {},
getSyncParameter() { return 0; },
drawArraysInstanced() {},
drawElementsInstanced() {},
drawRangeElements() {},
vertexAttribDivisor() {},
readBuffer() {},
drawBuffers() {},
clearBufferfv() {},
clearBufferiv() {},
clearBufferuiv() {},
clearBufferfi() {},
blitFramebuffer() {},
renderbufferStorageMultisample() {},
framebufferTextureLayer() {},
invalidateFramebuffer() {},
invalidateSubFramebuffer() {},
getInternalformatParameter() { return null; },
texStorage2D() {},
texStorage3D() {},
texImage3D() {},
texSubImage3D() {},
copyTexSubImage3D() {},
compressedTexImage3D() {},
compressedTexSubImage3D() {},
getFragDataLocation() { return -1; },
uniform1ui() {},
uniform2ui() {},
uniform3ui() {},
uniform4ui() {},
uniform1uiv() {},
uniform2uiv() {},
uniform3uiv() {},
uniform4uiv() {},
uniformMatrix2x3fv() {},
uniformMatrix3x2fv() {},
uniformMatrix2x4fv() {},
uniformMatrix4x2fv() {},
uniformMatrix3x4fv() {},
uniformMatrix4x3fv() {},
vertexAttribI4i() {},
vertexAttribI4ui() {},
vertexAttribI4iv() {},
vertexAttribI4uiv() {},
vertexAttribIPointer() {},
getUniformIndices() { return []; },
getActiveUniforms() { return []; },
getUniformBlockIndex() { return 0; },
getActiveUniformBlockParameter() { return null; },
getActiveUniformBlockName() { return ''; },
uniformBlockBinding() {},
copyBufferSubData() {},
getBufferSubData() {},
getParameter: createNative('getParameter', function (param) {
// RENDERER / VENDOR 参数
if (param === 0x1F01) return 'Google Inc. (Intel)'; // RENDERER
if (param === 0x1F00) return 'WebKit WebGL'; // VENDOR
if (param === 0x8B8C) return 'WebGL GLSL ES 3.00'; // SHADING_LANGUAGE_VERSION
if (param === 0x1F02) return 'WebGL 2.0 (OpenGL ES 3.0)';// VERSION
return null;
}),
getExtension: createNative('getExtension', function () { return null; }),
getSupportedExtensions: createNative('getSupportedExtensions', function () { return []; }),
createBuffer: createNative('createBuffer', function () { return {}; }),
bindBuffer: createNative('bindBuffer', function () {}),
bufferData: createNative('bufferData', function () {}),
createShader: createNative('createShader', function () { return {}; }),
shaderSource: createNative('shaderSource', function () {}),
compileShader: createNative('compileShader', function () {}),
createProgram: createNative('createProgram', function () { return {}; }),
attachShader: createNative('attachShader', function () {}),
linkProgram: createNative('linkProgram', function () {}),
useProgram: createNative('useProgram', function () {}),
getUniformLocation: createNative('getUniformLocation', function () { return {}; }),
uniform1f: createNative('uniform1f', function () {}),
drawArrays: createNative('drawArrays', function () {}),
readPixels: createNative('readPixels', function () {}),
enable: createNative('enable', function () {}),
clear: createNative('clear', function () {}),
clearColor: createNative('clearColor', function () {}),
viewport: createNative('viewport', function () {}),
};
}
// HTMLCanvasElement
class HTMLCanvasElement {
constructor() {
this.width = 300;
this.height = 150;
this._ctx2d = null;
}
getContext(type) {
if (type === '2d') {
if (!this._ctx2d) {
this._ctx2d = Object.create(CanvasRenderingContext2D.prototype);
this._ctx2d.canvas = this;
}
return this._ctx2d;
}
if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') {
return makeWebGLContext();
}
return null;
}
toDataURL(type) {
// 返回一个最小的合法 1x1 透明 PNG base64
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
}
toBlob(cb) { cb(null); }
captureStream() { return {}; }
}
nativeClass(HTMLCanvasElement);
module.exports = { HTMLCanvasElement, CanvasRenderingContext2D };

View File

@@ -1,289 +1,66 @@
'use strict';
/**
* Crypto Mock
*
* Web Crypto API implementation using Node.js crypto module.
* P1: Crypto / Storage / IDBFactory / atob / btoa mock
*/
import nodeCrypto from 'crypto';
const { createNative, nativeClass } = require('./native');
const nodeCrypto = require('crypto');
export function createCrypto() {
return {
getRandomValues(array) {
const bytes = nodeCrypto.randomBytes(array.byteLength);
const view = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
view.set(new Uint8Array(bytes));
return array;
},
// ── Crypto ───────────────────────────────────────────────────
const cryptoMock = {
getRandomValues: createNative('getRandomValues', function (array) {
return nodeCrypto.randomFillSync(array);
}),
randomUUID: createNative('randomUUID', function () {
return nodeCrypto.randomUUID();
}),
subtle: {
digest: createNative('digest', function () { return Promise.resolve(new ArrayBuffer(32)); }),
encrypt: createNative('encrypt', function () { return Promise.resolve(new ArrayBuffer(0)); }),
decrypt: createNative('decrypt', function () { return Promise.resolve(new ArrayBuffer(0)); }),
sign: createNative('sign', function () { return Promise.resolve(new ArrayBuffer(32)); }),
verify: createNative('verify', function () { return Promise.resolve(true); }),
generateKey: createNative('generateKey', function () { return Promise.resolve({}); }),
importKey: createNative('importKey', function () { return Promise.resolve({}); }),
exportKey: createNative('exportKey', function () { return Promise.resolve({}); }),
},
};
randomUUID() {
return nodeCrypto.randomUUID();
},
subtle: {
async digest(algorithm, data) {
const algoName = typeof algorithm === 'string'
? algorithm
: algorithm.name;
const hashMap = {
'SHA-1': 'sha1',
'SHA-256': 'sha256',
'SHA-384': 'sha384',
'SHA-512': 'sha512',
};
const nodeAlgo = hashMap[algoName.toUpperCase()] || 'sha256';
const hash = nodeCrypto.createHash(nodeAlgo);
// Handle different data types
if (data instanceof ArrayBuffer) {
hash.update(Buffer.from(data));
} else if (ArrayBuffer.isView(data)) {
hash.update(Buffer.from(data.buffer, data.byteOffset, data.byteLength));
} else {
hash.update(Buffer.from(data));
}
const result = hash.digest();
return result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength);
},
async encrypt(algorithm, key, data) {
const algoName = algorithm.name || algorithm;
if (algoName === 'AES-GCM') {
const cipher = nodeCrypto.createCipheriv(
'aes-256-gcm',
Buffer.from(key.key || key),
Buffer.from(algorithm.iv)
);
const encrypted = Buffer.concat([
cipher.update(Buffer.from(data)),
cipher.final(),
cipher.getAuthTag()
]);
return encrypted.buffer;
}
if (algoName === 'AES-CBC') {
const cipher = nodeCrypto.createCipheriv(
'aes-256-cbc',
Buffer.from(key.key || key),
Buffer.from(algorithm.iv)
);
const encrypted = Buffer.concat([
cipher.update(Buffer.from(data)),
cipher.final()
]);
return encrypted.buffer;
}
throw new Error(`Unsupported encryption algorithm: ${algoName}`);
},
async decrypt(algorithm, key, data) {
const algoName = algorithm.name || algorithm;
if (algoName === 'AES-GCM') {
const buffer = Buffer.from(data);
const authTag = buffer.slice(-16);
const encrypted = buffer.slice(0, -16);
const decipher = nodeCrypto.createDecipheriv(
'aes-256-gcm',
Buffer.from(key.key || key),
Buffer.from(algorithm.iv)
);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
]);
return decrypted.buffer;
}
if (algoName === 'AES-CBC') {
const decipher = nodeCrypto.createDecipheriv(
'aes-256-cbc',
Buffer.from(key.key || key),
Buffer.from(algorithm.iv)
);
const decrypted = Buffer.concat([
decipher.update(Buffer.from(data)),
decipher.final()
]);
return decrypted.buffer;
}
throw new Error(`Unsupported decryption algorithm: ${algoName}`);
},
async sign(algorithm, key, data) {
const algoName = algorithm.name || algorithm;
if (algoName === 'HMAC') {
const hashAlgo = algorithm.hash?.name || 'SHA-256';
const nodeHash = hashAlgo.replace('-', '').toLowerCase();
const hmac = nodeCrypto.createHmac(nodeHash, Buffer.from(key.key || key));
hmac.update(Buffer.from(data));
return hmac.digest().buffer;
}
throw new Error(`Unsupported signing algorithm: ${algoName}`);
},
async verify(algorithm, key, signature, data) {
const expected = await this.sign(algorithm, key, data);
const sig = Buffer.from(signature);
const exp = Buffer.from(expected);
return sig.length === exp.length && nodeCrypto.timingSafeEqual(sig, exp);
},
async generateKey(algorithm, extractable, keyUsages) {
const algoName = algorithm.name || algorithm;
if (algoName === 'AES-GCM' || algoName === 'AES-CBC') {
const length = algorithm.length || 256;
const key = nodeCrypto.randomBytes(length / 8);
return {
type: 'secret',
extractable,
algorithm: { name: algoName, length },
usages: keyUsages,
key,
};
}
if (algoName === 'HMAC') {
const hashAlgo = algorithm.hash?.name || 'SHA-256';
const length = algorithm.length || 256;
const key = nodeCrypto.randomBytes(length / 8);
return {
type: 'secret',
extractable,
algorithm: { name: algoName, hash: { name: hashAlgo }, length },
usages: keyUsages,
key,
};
}
throw new Error(`Unsupported key generation algorithm: ${algoName}`);
},
async importKey(format, keyData, algorithm, extractable, keyUsages) {
const algoName = algorithm.name || algorithm;
let key;
if (format === 'raw') {
key = Buffer.from(keyData);
} else if (format === 'jwk') {
// Basic JWK support
key = Buffer.from(keyData.k, 'base64url');
} else {
throw new Error(`Unsupported key format: ${format}`);
}
return {
type: 'secret',
extractable,
algorithm: typeof algorithm === 'string' ? { name: algorithm } : algorithm,
usages: keyUsages,
key,
};
},
async exportKey(format, key) {
if (format === 'raw') {
return key.key.buffer;
}
if (format === 'jwk') {
return {
kty: 'oct',
k: key.key.toString('base64url'),
alg: key.algorithm.name,
ext: key.extractable,
key_ops: key.usages,
};
}
throw new Error(`Unsupported export format: ${format}`);
},
async deriveBits(algorithm, baseKey, length) {
const algoName = algorithm.name || algorithm;
if (algoName === 'PBKDF2') {
const salt = Buffer.from(algorithm.salt);
const iterations = algorithm.iterations;
const hashAlgo = algorithm.hash?.name?.replace('-', '').toLowerCase() || 'sha256';
const derived = nodeCrypto.pbkdf2Sync(
Buffer.from(baseKey.key || baseKey),
salt,
iterations,
length / 8,
hashAlgo
);
return derived.buffer;
}
if (algoName === 'HKDF') {
const salt = Buffer.from(algorithm.salt || []);
const info = Buffer.from(algorithm.info || []);
const hashAlgo = algorithm.hash?.name?.replace('-', '').toLowerCase() || 'sha256';
const derived = nodeCrypto.hkdfSync(
hashAlgo,
Buffer.from(baseKey.key || baseKey),
salt,
info,
length / 8
);
return Buffer.from(derived).buffer;
}
throw new Error(`Unsupported deriveBits algorithm: ${algoName}`);
},
async deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages) {
const bits = await this.deriveBits(algorithm, baseKey, derivedKeyAlgorithm.length || 256);
return {
type: 'secret',
extractable,
algorithm: derivedKeyAlgorithm,
usages: keyUsages,
key: Buffer.from(bits),
};
},
async wrapKey(format, key, wrappingKey, wrapAlgorithm) {
const exported = await this.exportKey(format, key);
const data = format === 'raw' ? exported : Buffer.from(JSON.stringify(exported));
return this.encrypt(wrapAlgorithm, wrappingKey, data);
},
async unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, keyUsages) {
const decrypted = await this.decrypt(unwrapAlgorithm, unwrappingKey, wrappedKey);
const keyData = format === 'raw' ? decrypted : JSON.parse(Buffer.from(decrypted).toString());
return this.importKey(format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages);
},
},
};
// ── Storage (localStorage / sessionStorage) ──────────────────
class Storage {
constructor() { this._store = {}; }
get length() { return Object.keys(this._store).length; }
key(i) { return Object.keys(this._store)[i] || null; }
getItem(k) { return Object.prototype.hasOwnProperty.call(this._store, k) ? this._store[k] : null; }
setItem(k, v) { this._store[String(k)] = String(v); }
removeItem(k) { delete this._store[k]; }
clear() { this._store = {}; }
}
nativeClass(Storage);
// ── IDBFactory (indexedDB) ────────────────────────────────────
class IDBFactory {
open() { return { result: null, onerror: null, onsuccess: null }; }
deleteDatabase() { return {}; }
databases() { return Promise.resolve([]); }
cmp() { return 0; }
}
nativeClass(IDBFactory);
// ── Notification ──────────────────────────────────────────────
class Notification {
constructor(title, opts) {
this.title = title;
this.options = opts || {};
}
close() {}
static get permission() { return 'denied'; } // P2: denied 或 default
static requestPermission() { return Promise.resolve('denied'); }
}
nativeClass(Notification);
// ── atob / btoa ───────────────────────────────────────────────
const atob = createNative('atob', (str) => Buffer.from(str, 'base64').toString('binary'));
const btoa = createNative('btoa', (str) => Buffer.from(str, 'binary').toString('base64'));
module.exports = { cryptoMock, Storage, IDBFactory, Notification, atob, btoa };

View File

@@ -1,451 +1,53 @@
'use strict';
/**
* Document Mock
*
* Provides the document object for hsw.js
* P1: Document / HTMLDocument mock
* hsw 检测document 类型、createElement、cookie 等
*/
import { createElement } from './element.js';
export function createDocument(fingerprint = {}) {
const elements = new Map();
const eventListeners = new Map();
// Create default elements
const html = createElement('html', fingerprint);
const head = createElement('head', fingerprint);
const body = createElement('body', fingerprint);
html.appendChild(head);
html.appendChild(body);
body.clientWidth = fingerprint.screenWidth || 1920;
body.clientHeight = fingerprint.screenHeight || 1080;
const doc = {
// Node properties
nodeType: 9,
nodeName: '#document',
nodeValue: null,
// Document type
doctype: {
name: 'html',
publicId: '',
systemId: '',
},
// Document info
URL: fingerprint.url || 'https://example.com/',
documentURI: fingerprint.url || 'https://example.com/',
domain: fingerprint.domain || 'example.com',
baseURI: fingerprint.url || 'https://example.com/',
referrer: fingerprint.referrer || '',
cookie: '',
lastModified: new Date().toLocaleString(),
// Charset
characterSet: 'UTF-8',
charset: 'UTF-8',
inputEncoding: 'UTF-8',
// Ready state
readyState: 'complete',
// Content type
contentType: 'text/html',
// Visibility
hidden: false,
visibilityState: 'visible',
// Design mode
designMode: 'off',
// Document element
documentElement: html,
head,
body,
// Children
childNodes: [html],
children: [html],
firstChild: html,
lastChild: html,
firstElementChild: html,
lastElementChild: html,
childElementCount: 1,
// Active element
activeElement: body,
// Fullscreen
fullscreenEnabled: true,
fullscreenElement: null,
pictureInPictureEnabled: true,
pictureInPictureElement: null,
// Pointerlock
pointerLockElement: null,
// Scripts
currentScript: null,
scripts: [],
// Stylesheets
styleSheets: [],
// Forms
forms: [],
// Images
images: [],
// Links
links: [],
// Anchors
anchors: [],
// Embeds
embeds: [],
plugins: [],
// Default view
defaultView: null, // Will be set by window
// Implementation
implementation: {
createDocument: () => createDocument(fingerprint),
createDocumentType: () => ({}),
createHTMLDocument: () => createDocument(fingerprint),
hasFeature: () => true,
},
// Timeline
timeline: {
currentTime: performance?.now?.() || Date.now(),
},
// Feature policy
featurePolicy: {
allowedFeatures: () => [],
allowsFeature: () => true,
features: () => [],
getAllowlistForFeature: () => [],
},
// Permissions policy
permissionsPolicy: {
allowedFeatures: () => [],
allowsFeature: () => true,
features: () => [],
getAllowlistForFeature: () => [],
},
// Fonts
fonts: {
ready: Promise.resolve(),
check: () => true,
load: () => Promise.resolve([]),
forEach: () => {},
entries: () => [][Symbol.iterator](),
keys: () => [][Symbol.iterator](),
values: () => [][Symbol.iterator](),
[Symbol.iterator]: () => [][Symbol.iterator](),
},
// Methods - Element creation
createElement(tagName) {
return createElement(tagName, fingerprint);
},
createElementNS(namespace, tagName) {
return createElement(tagName, fingerprint);
},
createTextNode(text) {
return {
nodeType: 3,
nodeName: '#text',
nodeValue: text,
textContent: text,
data: text,
length: text.length,
};
},
createComment(text) {
return {
nodeType: 8,
nodeName: '#comment',
nodeValue: text,
textContent: text,
data: text,
length: text.length,
};
},
createDocumentFragment() {
return {
nodeType: 11,
nodeName: '#document-fragment',
childNodes: [],
children: [],
appendChild(child) {
this.childNodes.push(child);
return child;
},
removeChild(child) {
const idx = this.childNodes.indexOf(child);
if (idx > -1) this.childNodes.splice(idx, 1);
return child;
},
querySelector() { return null; },
querySelectorAll() { return []; },
};
},
createEvent(type) {
return {
type,
target: null,
currentTarget: null,
bubbles: false,
cancelable: false,
defaultPrevented: false,
timeStamp: Date.now(),
initEvent(type, bubbles, cancelable) {
this.type = type;
this.bubbles = bubbles;
this.cancelable = cancelable;
},
preventDefault() { this.defaultPrevented = true; },
stopPropagation() {},
stopImmediatePropagation() {},
};
},
createRange() {
return {
startContainer: doc,
endContainer: doc,
startOffset: 0,
endOffset: 0,
collapsed: true,
commonAncestorContainer: doc,
setStart() {},
setEnd() {},
setStartBefore() {},
setStartAfter() {},
setEndBefore() {},
setEndAfter() {},
collapse() {},
selectNode() {},
selectNodeContents() {},
cloneContents() { return doc.createDocumentFragment(); },
deleteContents() {},
extractContents() { return doc.createDocumentFragment(); },
insertNode() {},
surroundContents() {},
compareBoundaryPoints() { return 0; },
cloneRange() { return this; },
detach() {},
toString() { return ''; },
getBoundingClientRect() {
return { top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 };
},
getClientRects() { return []; },
};
},
createTreeWalker() {
return {
currentNode: null,
root: doc,
whatToShow: 0xFFFFFFFF,
filter: null,
nextNode() { return null; },
previousNode() { return null; },
firstChild() { return null; },
lastChild() { return null; },
nextSibling() { return null; },
previousSibling() { return null; },
parentNode() { return null; },
};
},
createNodeIterator() {
return {
root: doc,
whatToShow: 0xFFFFFFFF,
filter: null,
referenceNode: doc,
pointerBeforeReferenceNode: true,
nextNode() { return null; },
previousNode() { return null; },
detach() {},
};
},
// Methods - Element queries
getElementById(id) {
return elements.get(id) || null;
},
getElementsByTagName(tagName) {
return [];
},
getElementsByTagNameNS(namespace, tagName) {
return [];
},
getElementsByClassName(className) {
return [];
},
getElementsByName(name) {
return [];
},
querySelector(selector) {
return null;
},
querySelectorAll(selector) {
return [];
},
// Methods - Element from point
elementFromPoint(x, y) {
return body;
},
elementsFromPoint(x, y) {
return [body, html];
},
caretPositionFromPoint(x, y) {
return null;
},
// Methods - Document commands
execCommand(command, showUI, value) {
return false;
},
queryCommandEnabled(command) {
return false;
},
queryCommandSupported(command) {
return false;
},
queryCommandState(command) {
return false;
},
queryCommandValue(command) {
return '';
},
// Methods - Selection
getSelection() {
return {
anchorNode: null,
anchorOffset: 0,
focusNode: null,
focusOffset: 0,
isCollapsed: true,
rangeCount: 0,
type: 'None',
addRange() {},
collapse() {},
collapseToEnd() {},
collapseToStart() {},
containsNode() { return false; },
deleteFromDocument() {},
empty() {},
extend() {},
getRangeAt() { return doc.createRange(); },
removeAllRanges() {},
removeRange() {},
selectAllChildren() {},
setBaseAndExtent() {},
setPosition() {},
toString() { return ''; },
};
},
// Methods - Document state
hasFocus() {
return true;
},
// Methods - Fullscreen
exitFullscreen() {
return Promise.resolve();
},
exitPictureInPicture() {
return Promise.resolve();
},
exitPointerLock() {},
// Methods - Adoption
adoptNode(node) {
return node;
},
importNode(node, deep) {
return node;
},
// Methods - Writing
open() { return doc; },
close() {},
write() {},
writeln() {},
// Events
addEventListener(type, listener, options) {
if (!eventListeners.has(type)) {
eventListeners.set(type, []);
}
eventListeners.get(type).push(listener);
},
removeEventListener(type, listener, options) {
const listeners = eventListeners.get(type);
if (listeners) {
const idx = listeners.indexOf(listener);
if (idx > -1) listeners.splice(idx, 1);
}
},
dispatchEvent(event) {
const listeners = eventListeners.get(event.type);
if (listeners) {
listeners.forEach(fn => fn(event));
}
return true;
},
// Callbacks (deprecated but used by some scripts)
onreadystatechange: null,
onvisibilitychange: null,
onpointerlockchange: null,
onpointerlockerror: null,
onfullscreenchange: null,
onfullscreenerror: null,
};
// Set ownerDocument references
html.ownerDocument = doc;
head.ownerDocument = doc;
body.ownerDocument = doc;
return doc;
const { createNative, nativeClass } = require('./native');
const { HTMLCanvasElement } = require('./canvas');
class HTMLDocument {
constructor() {
this.cookie = '';
this.referrer = '';
this.title = '';
this.readyState = 'complete';
this.visibilityState = 'visible';
this.hidden = false;
this.charset = 'UTF-8';
this.characterSet = 'UTF-8';
this.contentType = 'text/html';
this.URL = '';
this.domain = '';
this.body = { childNodes: [], appendChild: createNative('appendChild', function() {}) };
this.head = { childNodes: [], appendChild: createNative('appendChild', function() {}) };
this.documentElement = { clientWidth: 1920, clientHeight: 1080 };
}
}
HTMLDocument.prototype.createElement = createNative('createElement', function (tag) {
const t = tag.toLowerCase();
if (t === 'canvas') return new HTMLCanvasElement();
if (t === 'div' || t === 'span' || t === 'p') {
return {
style: {},
appendChild: createNative('appendChild', function() {}),
getAttribute: createNative('getAttribute', function() { return null; }),
setAttribute: createNative('setAttribute', function() {}),
};
}
return { style: {} };
});
HTMLDocument.prototype.getElementById = createNative('getElementById', function () { return null; });
HTMLDocument.prototype.querySelector = createNative('querySelector', function () { return null; });
HTMLDocument.prototype.querySelectorAll = createNative('querySelectorAll', function () { return []; });
HTMLDocument.prototype.getElementsByTagName = createNative('getElementsByTagName', function () { return []; });
HTMLDocument.prototype.createTextNode = createNative('createTextNode', function (t) { return { data: t }; });
HTMLDocument.prototype.addEventListener = createNative('addEventListener', function () {});
HTMLDocument.prototype.removeEventListener = createNative('removeEventListener', function () {});
HTMLDocument.prototype.dispatchEvent = createNative('dispatchEvent', function () { return true; });
nativeClass(HTMLDocument);
module.exports = HTMLDocument;

View File

@@ -1,415 +0,0 @@
/**
* DOM Element Mock
*
* Provides createElement and element behavior for hsw.js
*/
import { createCanvasRenderingContext2D, createWebGLContext } from './canvas.js';
export function createElement(tagName, fingerprint = {}) {
const tag = tagName.toLowerCase();
const base = createBaseElement(tag);
switch (tag) {
case 'canvas':
return createCanvasElement(base, fingerprint);
case 'div':
case 'span':
case 'iframe':
return createContainerElement(base);
case 'script':
return createScriptElement(base);
case 'style':
return createStyleElement(base);
case 'img':
return createImageElement(base);
case 'input':
return createInputElement(base);
case 'a':
return createAnchorElement(base);
default:
return base;
}
}
function createBaseElement(tagName) {
const style = createCSSStyleDeclaration();
const classList = createClassList();
const dataset = {};
const attributes = new Map();
const children = [];
let parent = null;
const elem = {
tagName: tagName.toUpperCase(),
nodeName: tagName.toUpperCase(),
nodeType: 1,
nodeValue: null,
style,
classList,
dataset,
className: '',
id: '',
innerHTML: '',
innerText: '',
textContent: '',
outerHTML: '',
children,
childNodes: children,
firstChild: null,
lastChild: null,
parentNode: null,
parentElement: null,
nextSibling: null,
previousSibling: null,
ownerDocument: null, // Set by document
// Attribute methods
setAttribute(name, value) {
attributes.set(name, String(value));
if (name === 'id') this.id = value;
if (name === 'class') this.className = value;
},
getAttribute(name) {
return attributes.get(name) ?? null;
},
removeAttribute(name) {
attributes.delete(name);
},
hasAttribute(name) {
return attributes.has(name);
},
getAttributeNames() {
return [...attributes.keys()];
},
// DOM manipulation
appendChild(child) {
children.push(child);
child.parentNode = this;
child.parentElement = this;
this.firstChild = children[0];
this.lastChild = children[children.length - 1];
return child;
},
removeChild(child) {
const idx = children.indexOf(child);
if (idx > -1) {
children.splice(idx, 1);
child.parentNode = null;
child.parentElement = null;
}
return child;
},
insertBefore(newChild, refChild) {
const idx = children.indexOf(refChild);
if (idx > -1) {
children.splice(idx, 0, newChild);
} else {
children.push(newChild);
}
newChild.parentNode = this;
return newChild;
},
replaceChild(newChild, oldChild) {
const idx = children.indexOf(oldChild);
if (idx > -1) {
children[idx] = newChild;
newChild.parentNode = this;
oldChild.parentNode = null;
}
return oldChild;
},
cloneNode(deep) {
const clone = createBaseElement(tagName);
attributes.forEach((v, k) => clone.setAttribute(k, v));
if (deep) {
children.forEach(c => clone.appendChild(c.cloneNode?.(true) || c));
}
return clone;
},
contains(node) {
return children.includes(node);
},
// Query
querySelector() { return null; },
querySelectorAll() { return []; },
getElementsByTagName() { return []; },
getElementsByClassName() { return []; },
// Geometry
getBoundingClientRect() {
return {
top: 0, right: 100, bottom: 100, left: 0,
width: 100, height: 100, x: 0, y: 0,
toJSON() { return this; }
};
},
getClientRects() {
return [this.getBoundingClientRect()];
},
// Dimensions
offsetWidth: 100,
offsetHeight: 100,
offsetTop: 0,
offsetLeft: 0,
offsetParent: null,
clientWidth: 100,
clientHeight: 100,
clientTop: 0,
clientLeft: 0,
scrollWidth: 100,
scrollHeight: 100,
scrollTop: 0,
scrollLeft: 0,
// Events
addEventListener() {},
removeEventListener() {},
dispatchEvent() { return true; },
// Focus
focus() {},
blur() {},
click() {},
// Scroll
scrollTo() {},
scrollBy() {},
scrollIntoView() {},
// Animation
animate() { return { finished: Promise.resolve() }; },
getAnimations() { return []; },
// Misc
matches() { return false; },
closest() { return null; },
remove() {
if (parent) parent.removeChild(this);
},
before() {},
after() {},
replaceWith() {},
append() {},
prepend() {},
};
return elem;
}
function createCanvasElement(base, fingerprint) {
let width = 300;
let height = 150;
let context2d = null;
let contextWebGL = null;
return Object.assign(base, {
get width() { return width; },
set width(v) { width = v; },
get height() { return height; },
set height(v) { height = v; },
getContext(type, attrs) {
if (type === '2d') {
if (!context2d) {
context2d = createCanvasRenderingContext2D(this, fingerprint);
}
return context2d;
}
if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') {
if (!contextWebGL) {
contextWebGL = createWebGLContext(type, fingerprint);
}
return contextWebGL;
}
return null;
},
toDataURL(type = 'image/png', quality) {
// Return a deterministic but realistic-looking data URL
// In production, this should return fingerprint-specific data
return fingerprint.canvasDataUrl ||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
},
toBlob(callback, type = 'image/png', quality) {
const dataUrl = this.toDataURL(type, quality);
const base64 = dataUrl.split(',')[1];
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
callback(new Blob([bytes], { type }));
},
captureStream() {
return { getTracks: () => [] };
},
transferControlToOffscreen() {
return this; // Simplified
},
});
}
function createContainerElement(base) {
return base;
}
function createScriptElement(base) {
return Object.assign(base, {
src: '',
async: false,
defer: false,
type: '',
text: '',
charset: '',
crossOrigin: null,
noModule: false,
});
}
function createStyleElement(base) {
return Object.assign(base, {
media: '',
type: 'text/css',
disabled: false,
sheet: null,
});
}
function createImageElement(base) {
return Object.assign(base, {
src: '',
alt: '',
width: 0,
height: 0,
naturalWidth: 0,
naturalHeight: 0,
complete: true,
currentSrc: '',
loading: 'auto',
decoding: 'auto',
crossOrigin: null,
decode: () => Promise.resolve(),
});
}
function createInputElement(base) {
return Object.assign(base, {
type: 'text',
value: '',
name: '',
disabled: false,
checked: false,
placeholder: '',
readOnly: false,
required: false,
maxLength: -1,
minLength: -1,
pattern: '',
form: null,
select() {},
setSelectionRange() {},
});
}
function createAnchorElement(base) {
return Object.assign(base, {
href: '',
target: '',
rel: '',
protocol: '',
host: '',
hostname: '',
port: '',
pathname: '',
search: '',
hash: '',
origin: '',
});
}
function createCSSStyleDeclaration() {
const styles = {};
const handler = {
get(target, prop) {
if (prop === 'cssText') {
return Object.entries(styles)
.map(([k, v]) => `${k}: ${v}`)
.join('; ');
}
if (prop === 'length') {
return Object.keys(styles).length;
}
if (prop === 'setProperty') {
return (name, value) => { styles[name] = value; };
}
if (prop === 'getPropertyValue') {
return (name) => styles[name] || '';
}
if (prop === 'removeProperty') {
return (name) => { delete styles[name]; };
}
if (prop === 'item') {
return (i) => Object.keys(styles)[i] || '';
}
return styles[prop] ?? '';
},
set(target, prop, value) {
styles[prop] = value;
return true;
}
};
return new Proxy({}, handler);
}
function createClassList() {
const classes = new Set();
return {
add(...tokens) { tokens.forEach(t => classes.add(t)); },
remove(...tokens) { tokens.forEach(t => classes.delete(t)); },
toggle(token, force) {
if (force !== undefined) {
force ? classes.add(token) : classes.delete(token);
return force;
}
if (classes.has(token)) {
classes.delete(token);
return false;
}
classes.add(token);
return true;
},
contains(token) { return classes.has(token); },
replace(oldToken, newToken) {
if (classes.has(oldToken)) {
classes.delete(oldToken);
classes.add(newToken);
return true;
}
return false;
},
item(i) { return [...classes][i] ?? null; },
get length() { return classes.size; },
get value() { return [...classes].join(' '); },
set value(v) {
classes.clear();
v.split(/\s+/).filter(Boolean).forEach(t => classes.add(t));
},
toString() { return this.value; },
[Symbol.iterator]() { return classes.values(); },
};
}

View File

@@ -1,39 +1,52 @@
'use strict';
/**
* Mock Index - Entry point for browser environment
*
* Usage:
* import { createBrowserEnvironment } from './mocks/index.js';
* const env = createBrowserEnvironment(fingerprint);
* Mock 总装工厂
* 导出 createBrowserEnvironment(),返回 { window, document, navigator, ... }
* 供 HswRunner 注入全局作用域
*/
export { createScreen } from './screen.js';
export { createNavigator } from './navigator.js';
export { createDocument } from './document.js';
export { createWindow } from './window.js';
export { createPerformance } from './performance.js';
export { createCrypto } from './crypto.js';
export { createStorage } from './storage.js';
export { createElement } from './element.js';
export { createCanvasRenderingContext2D, createWebGLContext } from './canvas.js';
const windowProxy = require('./window');
import { createWindow } from './window.js';
function createBrowserEnvironment(fingerprint = {}) {
const win = windowProxy;
/**
* Create a complete browser environment
*/
export function createBrowserEnvironment(fingerprint = {}) {
const window = createWindow(fingerprint);
// 如果传入了指纹覆盖,应用到对应属性上
if (fingerprint.userAgent) {
win.navigator.userAgent = fingerprint.userAgent;
win.navigator.appVersion = fingerprint.userAgent.replace('Mozilla/', '');
}
if (fingerprint.platform) {
win.navigator.platform = fingerprint.platform;
}
if (fingerprint.languages) {
win.navigator.languages = fingerprint.languages;
win.navigator.language = fingerprint.languages[0];
}
if (fingerprint.screenWidth && fingerprint.screenHeight) {
win.screen.width = fingerprint.screenWidth;
win.screen.height = fingerprint.screenHeight;
win.screen.availWidth = fingerprint.screenWidth;
win.screen.availHeight = fingerprint.screenHeight - 40;
}
if (fingerprint.host) {
// 更新 location 中与 host 相关的字段
const loc = win.location;
if (loc.ancestorOrigins) {
loc.ancestorOrigins[0] = `https://${fingerprint.host}`;
}
}
return {
window,
document: window.document,
navigator: window.navigator,
screen: window.screen,
location: window.location,
history: window.history,
performance: window.performance,
crypto: window.crypto,
localStorage: window.localStorage,
sessionStorage: window.sessionStorage,
window: win,
document: win.document,
navigator: win.navigator,
screen: win.screen,
location: win.location,
localStorage: win.localStorage,
sessionStorage: win.sessionStorage,
crypto: win.crypto,
performance: win.performance,
};
}
module.exports = { createBrowserEnvironment };

View File

@@ -0,0 +1,50 @@
/**
* 基建:原生函数伪装器
* 所有 mock 函数必须通过 createNative() 包装,
* 否则 toString() 会暴露 JS 源码被 hsw 检测到。
*/
'use strict';
// 用 WeakSet 存需要伪装的函数,避免污染函数本身
const nativeSet = new WeakSet();
// 劫持 Function.prototype.toString
const _origToString = Function.prototype.toString;
Function.prototype.toString = function () {
if (nativeSet.has(this)) {
return `function ${this.name || ''}() { [native code] }`;
}
return _origToString.call(this);
};
/**
* 将一个 JS 函数包装成"看起来像原生"的函数
* @param {string} name - 函数名(影响 toString 输出)
* @param {Function} fn - 实际实现
* @returns {Function}
*/
function createNative(name, fn) {
Object.defineProperty(fn, 'name', { value: name, configurable: true });
nativeSet.add(fn);
return fn;
}
/**
* 将一个 class 的构造函数 + 所有原型方法 全部标记为 native
* @param {Function} cls
* @returns {Function}
*/
function nativeClass(cls) {
nativeSet.add(cls);
Object.getOwnPropertyNames(cls.prototype).forEach(key => {
const desc = Object.getOwnPropertyDescriptor(cls.prototype, key);
if (desc && typeof desc.value === 'function') nativeSet.add(desc.value);
// 也伪装 getter/setter
if (desc && typeof desc.get === 'function') nativeSet.add(desc.get);
if (desc && typeof desc.set === 'function') nativeSet.add(desc.set);
});
return cls;
}
module.exports = { createNative, nativeClass, nativeSet };

View File

@@ -1,262 +1,91 @@
'use strict';
/**
* Navigator Mock
*
* Critical fingerprinting surface. Every property must match
* the User-Agent exactly or hsw.js will produce invalid n values.
* P0/P1: Navigator mock
* hsw 检测webdriver / languages / maxTouchPoints / plugins / userAgentData
*/
import navProps from '../stubs/navigator_props.json' with { type: 'json' };
const { createNative } = require('./native');
export function createNavigator(overrides = {}) {
const props = { ...navProps, ...overrides };
// PluginArray 结构
const plugins = Object.assign(Object.create({
item: createNative('item', function (i) { return this[i] || null; }),
namedItem: createNative('namedItem', function () { return null; }),
refresh: createNative('refresh', function () {}),
}), {
0: { name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 },
1: { name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 },
2: { name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 },
length: 3,
});
// Plugin array mock
const plugins = createPluginArray(props.plugins);
const mimeTypes = createMimeTypeArray(props.mimeTypes);
const navigatorMock = {
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
appVersion: '5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
appName: 'Netscape',
appCodeName: 'Mozilla',
platform: 'Linux x86_64',
product: 'Gecko',
vendor: 'Google Inc.',
language: 'en-US',
languages: ['en-US', 'en'], // P1: 必须是非空数组
webdriver: false, // navigator.webdriver = falsewindow.webdriver = undefined
maxTouchPoints: 0, // P1: 桌面为 0
hardwareConcurrency: 8,
deviceMemory: 8,
cookieEnabled: true,
onLine: true,
doNotTrack: null,
plugins,
mimeTypes: { length: 0 },
// UserAgentData mock (modern Chrome)
const userAgentData = props.userAgentData ? {
brands: props.userAgentData.brands,
mobile: props.userAgentData.mobile,
platform: props.userAgentData.platform,
getHighEntropyValues: (hints) => Promise.resolve({
brands: props.userAgentData.brands,
mobile: props.userAgentData.mobile,
platform: props.userAgentData.platform,
platformVersion: '15.0.0',
architecture: 'x86',
bitness: '64',
model: '',
uaFullVersion: '120.0.0.0',
fullVersionList: props.userAgentData.brands,
// P2: userAgentData (NavigatorUAData)
userAgentData: {
brands: [
{ brand: 'Not:A-Brand', version: '99' },
{ brand: 'Google Chrome', version: '145' },
{ brand: 'Chromium', version: '145' },
],
mobile: false,
platform: 'Linux',
getHighEntropyValues: createNative('getHighEntropyValues', function (hints) {
return Promise.resolve({
architecture: 'x86',
bitness: '64',
model: '',
platform: 'Linux',
platformVersion: '6.1.0',
uaFullVersion: '145.0.0.0',
fullVersionList: [
{ brand: 'Not:A-Brand', version: '99.0.0.0' },
{ brand: 'Google Chrome', version: '145.0.0.0' },
{ brand: 'Chromium', version: '145.0.0.0' },
],
});
}),
toJSON: () => ({
brands: props.userAgentData.brands,
mobile: props.userAgentData.mobile,
platform: props.userAgentData.platform,
},
// P2: connection (NetworkInformation)
connection: {
effectiveType: '4g',
downlink: 10,
rtt: 50,
saveData: false,
},
geolocation: {
getCurrentPosition: createNative('getCurrentPosition', function (s, e) { e && e({ code: 1, message: 'denied' }); }),
watchPosition: createNative('watchPosition', function () { return 0; }),
clearWatch: createNative('clearWatch', function () {}),
},
permissions: {
query: createNative('query', function (desc) {
return Promise.resolve({ state: desc.name === 'notifications' ? 'denied' : 'prompt' });
}),
} : undefined;
},
// NetworkInformation mock
const connection = props.connection ? {
effectiveType: props.connection.effectiveType,
rtt: props.connection.rtt,
downlink: props.connection.downlink,
saveData: props.connection.saveData,
addEventListener: () => {},
removeEventListener: () => {},
} : undefined;
sendBeacon: createNative('sendBeacon', function () { return true; }),
vibrate: createNative('vibrate', function () { return false; }),
};
const navigator = {
// Identity
userAgent: props.userAgent,
appVersion: props.appVersion,
platform: props.platform,
vendor: props.vendor,
vendorSub: props.vendorSub,
product: props.product,
productSub: props.productSub,
appName: props.appName,
appCodeName: props.appCodeName,
// Locale
language: props.language,
languages: Object.freeze([...props.languages]),
// State
onLine: props.onLine,
cookieEnabled: props.cookieEnabled,
doNotTrack: props.doNotTrack,
// Hardware
maxTouchPoints: props.maxTouchPoints,
hardwareConcurrency: props.hardwareConcurrency,
deviceMemory: props.deviceMemory,
// Features
pdfViewerEnabled: props.pdfViewerEnabled,
webdriver: props.webdriver, // CRITICAL: must be false
// Modern APIs
userAgentData,
connection,
// Plugin system
plugins,
mimeTypes,
// Methods
javaEnabled: () => false,
getGamepads: () => [null, null, null, null],
vibrate: () => true,
share: () => Promise.reject(new Error('Share canceled')),
canShare: () => false,
sendBeacon: (url, data) => true,
registerProtocolHandler: () => {},
unregisterProtocolHandler: () => {},
getBattery: () => Promise.resolve({
charging: true,
chargingTime: 0,
dischargingTime: Infinity,
level: 1,
addEventListener: () => {},
removeEventListener: () => {},
}),
getInstalledRelatedApps: () => Promise.resolve([]),
requestMediaKeySystemAccess: () => Promise.reject(new Error('Not supported')),
// Permissions
permissions: {
query: (desc) => Promise.resolve({
state: 'prompt',
name: desc.name,
addEventListener: () => {},
removeEventListener: () => {},
}),
},
// MediaDevices
mediaDevices: {
enumerateDevices: () => Promise.resolve([]),
getUserMedia: () => Promise.reject(new Error('Not allowed')),
getDisplayMedia: () => Promise.reject(new Error('Not allowed')),
getSupportedConstraints: () => ({}),
addEventListener: () => {},
removeEventListener: () => {},
},
// Clipboard
clipboard: {
read: () => Promise.reject(new Error('Not allowed')),
readText: () => Promise.reject(new Error('Not allowed')),
write: () => Promise.reject(new Error('Not allowed')),
writeText: () => Promise.resolve(),
},
// Credentials
credentials: {
get: () => Promise.resolve(null),
store: () => Promise.resolve(),
create: () => Promise.resolve(null),
preventSilentAccess: () => Promise.resolve(),
},
// Service Worker
serviceWorker: {
controller: null,
ready: Promise.resolve({
active: null,
installing: null,
waiting: null,
}),
register: () => Promise.reject(new Error('Not supported')),
getRegistration: () => Promise.resolve(undefined),
getRegistrations: () => Promise.resolve([]),
addEventListener: () => {},
removeEventListener: () => {},
},
// Geolocation
geolocation: {
getCurrentPosition: (s, e) => e && e({ code: 1, message: 'Denied' }),
watchPosition: () => 0,
clearWatch: () => {},
},
// Storage
storage: {
estimate: () => Promise.resolve({ quota: 1073741824, usage: 0 }),
persist: () => Promise.resolve(false),
persisted: () => Promise.resolve(false),
getDirectory: () => Promise.reject(new Error('Not supported')),
},
// Locks
locks: {
request: () => Promise.reject(new Error('Not supported')),
query: () => Promise.resolve({ held: [], pending: [] }),
},
// GPU (WebGPU)
gpu: undefined,
// USB
usb: undefined,
// Bluetooth
bluetooth: undefined,
// Serial
serial: undefined,
// HID
hid: undefined,
};
return navigator;
}
function createPluginArray(config) {
const items = (config?.items || []).map((p, i) => createPlugin(p, i));
const arr = [...items];
arr.item = (i) => arr[i] || null;
arr.namedItem = (name) => arr.find(p => p.name === name) || null;
arr.refresh = () => {};
// Make length non-enumerable like real PluginArray
Object.defineProperty(arr, 'length', {
value: items.length,
writable: false,
enumerable: false,
});
return arr;
}
function createPlugin(props, index) {
const plugin = {
name: props.name,
filename: props.filename,
description: props.description,
length: 1,
item: (i) => i === 0 ? plugin[0] : null,
namedItem: (name) => name === props.name ? plugin[0] : null,
};
// Add MimeType reference
plugin[0] = {
type: 'application/pdf',
suffixes: 'pdf',
description: props.description,
enabledPlugin: plugin,
};
return plugin;
}
function createMimeTypeArray(config) {
const items = (config?.items || []).map(m => ({
type: m.type,
suffixes: m.suffixes,
description: m.description,
enabledPlugin: null,
}));
const arr = [...items];
arr.item = (i) => arr[i] || null;
arr.namedItem = (type) => arr.find(m => m.type === type) || null;
Object.defineProperty(arr, 'length', {
value: items.length,
writable: false,
enumerable: false,
});
return arr;
}
module.exports = navigatorMock;

View File

@@ -1,150 +1,170 @@
'use strict';
/**
* Performance Mock
*
* Timing and performance metrics for fingerprinting.
* P0: Performance mock
* hsw 检测timing / timeOrigin / getEntriesByType('resource') / getEntriesByType('navigation')
*/
export function createPerformance(fingerprint = {}) {
const timeOrigin = Date.now() - (fingerprint.uptime || 10000);
const entries = [];
const { createNative } = require('./native');
return {
timeOrigin,
const NAV_START = Date.now() - 1200;
now() {
return Date.now() - timeOrigin;
},
const timingData = {
navigationStart: NAV_START,
fetchStart: NAV_START + 11,
domainLookupStart: NAV_START + 11,
domainLookupEnd: NAV_START + 11,
connectStart: NAV_START + 11,
secureConnectionStart: NAV_START + 11,
connectEnd: NAV_START + 11,
requestStart: NAV_START + 37,
responseStart: NAV_START + 47,
responseEnd: NAV_START + 114,
domLoading: NAV_START + 203,
domInteractive: NAV_START + 399,
domContentLoadedEventStart: NAV_START + 399,
domContentLoadedEventEnd: NAV_START + 399,
domComplete: NAV_START + 399,
loadEventStart: NAV_START + 399,
loadEventEnd: NAV_START + 399,
redirectStart: 0,
redirectEnd: 0,
unloadEventStart: 0,
unloadEventEnd: 0,
};
// Timing (deprecated but still used)
timing: {
navigationStart: timeOrigin,
unloadEventStart: 0,
unloadEventEnd: 0,
redirectStart: 0,
redirectEnd: 0,
fetchStart: timeOrigin + 1,
domainLookupStart: timeOrigin + 2,
domainLookupEnd: timeOrigin + 10,
connectStart: timeOrigin + 10,
connectEnd: timeOrigin + 50,
secureConnectionStart: timeOrigin + 20,
requestStart: timeOrigin + 50,
responseStart: timeOrigin + 100,
responseEnd: timeOrigin + 200,
domLoading: timeOrigin + 200,
domInteractive: timeOrigin + 500,
domContentLoadedEventStart: timeOrigin + 500,
domContentLoadedEventEnd: timeOrigin + 510,
domComplete: timeOrigin + 1000,
loadEventStart: timeOrigin + 1000,
loadEventEnd: timeOrigin + 1010,
},
// 模拟 resource 条目hsw 会查 checksiteconfig 请求痕迹)
const resourceEntries = [
{
name: 'https://api.hcaptcha.com/checksiteconfig?v=xxx&host=b.stripecdn.com&sitekey=xxx&sc=1&swa=1&spst=1',
entryType: 'resource',
initiatorType: 'xmlhttprequest',
startTime: 399.2,
duration: 643.1,
fetchStart: 399.2,
responseEnd: 1042.3,
transferSize: 0,
encodedBodySize: 0,
decodedBodySize: 0,
responseStatus: 200,
deliveryType: '',
nextHopProtocol: '',
contentEncoding: 'br',
workerStart: 0,
redirectStart: 0,
redirectEnd: 0,
domainLookupStart: 0,
domainLookupEnd: 0,
connectStart: 0,
secureConnectionStart: 0,
connectEnd: 0,
requestStart: 0,
responseStart: 0,
firstInterimResponseStart: 0,
finalResponseHeadersStart: 0, // P2 要求的字段
serverTiming: [],
renderBlockingStatus: 'non-blocking',
},
{
name: 'https://newassets.hcaptcha.com/c/xxx/hsw.js',
entryType: 'resource',
initiatorType: 'script',
deliveryType: 'cache',
nextHopProtocol: 'h2',
startTime: 1043.8,
duration: 5.7,
fetchStart: 1043.8,
domainLookupStart: 1043.8,
domainLookupEnd: 1043.8,
connectStart: 1043.8,
secureConnectionStart: 1043.8,
connectEnd: 1043.8,
requestStart: 1044.6,
responseStart: 1044.6,
firstInterimResponseStart: 1044.6,
finalResponseHeadersStart: 0,
responseEnd: 1049.5,
transferSize: 0,
encodedBodySize: 359059,
decodedBodySize: 829689,
responseStatus: 200,
contentEncoding: 'gzip',
workerStart: 0,
redirectStart: 0,
redirectEnd: 0,
serverTiming: [],
renderBlockingStatus: 'non-blocking',
},
];
// Navigation (deprecated)
navigation: {
type: 0, // TYPE_NAVIGATE
redirectCount: 0,
},
// 模拟 navigation 条目
const navigationEntry = {
name: 'https://newassets.hcaptcha.com/captcha/v1/xxx/static/hcaptcha.html',
entryType: 'navigation',
initiatorType: 'navigation',
deliveryType: 'cache',
nextHopProtocol: 'h2',
startTime: 0,
duration: 399.9,
fetchStart: 11.6,
domainLookupStart: 11.6,
domainLookupEnd: 11.6,
connectStart: 11.6,
secureConnectionStart: 11.6,
connectEnd: 11.6,
requestStart: 37.6,
responseStart: 47.4,
firstInterimResponseStart: 47.4,
finalResponseHeadersStart: 0,
responseEnd: 114.2,
transferSize: 0,
encodedBodySize: 167487,
decodedBodySize: 567885,
responseStatus: 200,
redirectStart: 0,
redirectEnd: 0,
unloadEventStart: 0,
unloadEventEnd: 0,
domInteractive: 399.4,
domContentLoadedEventStart: 399.5,
domContentLoadedEventEnd: 399.5,
domComplete: 399.8,
loadEventStart: 399.9,
loadEventEnd: 399.9,
type: 'navigate',
redirectCount: 0,
activationStart: 0,
criticalCHRestart: 0,
notRestoredReasons: null,
confidence: null,
serverTiming: [],
workerStart: 0,
contentEncoding: 'br',
renderBlockingStatus: 'non-blocking',
};
// Memory (Chrome-specific)
memory: {
jsHeapSizeLimit: 4294705152,
totalJSHeapSize: 35000000,
usedJSHeapSize: 25000000,
},
const performanceMock = {
timeOrigin: NAV_START,
timing: timingData,
navigation: { type: 0, redirectCount: 0 },
// Event counts (Chrome)
eventCounts: {
size: 0,
get: () => 0,
has: () => false,
keys: () => [][Symbol.iterator](),
values: () => [][Symbol.iterator](),
entries: () => [][Symbol.iterator](),
forEach: () => {},
[Symbol.iterator]: () => [][Symbol.iterator](),
},
getEntriesByType: createNative('getEntriesByType', function (type) {
if (type === 'resource') return resourceEntries;
if (type === 'navigation') return [navigationEntry];
return [];
}),
// Entry methods
getEntries() {
return [...entries];
},
getEntriesByName: createNative('getEntriesByName', function (name) {
return resourceEntries.filter(e => e.name === name);
}),
getEntriesByType(type) {
return entries.filter(e => e.entryType === type);
},
now: createNative('now', function () {
return Date.now() - NAV_START;
}),
getEntriesByName(name, type) {
return entries.filter(e =>
e.name === name && (!type || e.entryType === type)
);
},
mark: createNative('mark', function () {}),
measure: createNative('measure', function () {}),
clearMarks: createNative('clearMarks', function () {}),
clearMeasures: createNative('clearMeasures', function () {}),
};
// Marks and measures
mark(name, options) {
const entry = {
name,
entryType: 'mark',
startTime: this.now(),
duration: 0,
detail: options?.detail || null,
};
entries.push(entry);
return entry;
},
measure(name, startMark, endMark) {
const startTime = typeof startMark === 'string'
? (entries.find(e => e.name === startMark)?.startTime || 0)
: (startMark?.start || 0);
const endTime = typeof endMark === 'string'
? (entries.find(e => e.name === endMark)?.startTime || this.now())
: (endMark?.end || this.now());
const entry = {
name,
entryType: 'measure',
startTime,
duration: endTime - startTime,
};
entries.push(entry);
return entry;
},
clearMarks(name) {
if (name) {
const idx = entries.findIndex(e => e.name === name && e.entryType === 'mark');
if (idx > -1) entries.splice(idx, 1);
} else {
entries.splice(0, entries.length, ...entries.filter(e => e.entryType !== 'mark'));
}
},
clearMeasures(name) {
if (name) {
const idx = entries.findIndex(e => e.name === name && e.entryType === 'measure');
if (idx > -1) entries.splice(idx, 1);
} else {
entries.splice(0, entries.length, ...entries.filter(e => e.entryType !== 'measure'));
}
},
clearResourceTimings() {
entries.splice(0, entries.length, ...entries.filter(e => e.entryType !== 'resource'));
},
setResourceTimingBufferSize() {},
// Observer
observe() {},
// JSON
toJSON() {
return {
timeOrigin: this.timeOrigin,
timing: this.timing,
navigation: this.navigation,
};
},
};
}
module.exports = performanceMock;

View File

@@ -1,32 +1,22 @@
'use strict';
/**
* Screen Mock
* P1: Screen mock
* hsw 检测screen.width / height / colorDepth / pixelDepth / availWidth / availHeight
*/
import screenProps from '../stubs/screen_props.json' with { type: 'json' };
const screenMock = {
width: 1920,
height: 1080,
availWidth: 1920,
availHeight: 1040, // 减去任务栏高度
availLeft: 0,
availTop: 0,
colorDepth: 24,
pixelDepth: 24,
orientation: {
type: 'landscape-primary',
angle: 0,
},
};
export function createScreen(overrides = {}) {
const props = { ...screenProps, ...overrides };
const orientation = {
type: props.orientation?.type || 'landscape-primary',
angle: props.orientation?.angle || 0,
lock: () => Promise.resolve(),
unlock: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => true,
};
return {
width: props.width,
height: props.height,
availWidth: props.availWidth,
availHeight: props.availHeight,
availLeft: props.availLeft,
availTop: props.availTop,
colorDepth: props.colorDepth,
pixelDepth: props.pixelDepth,
orientation,
isExtended: props.isExtended,
};
}
module.exports = screenMock;

View File

@@ -1,82 +0,0 @@
/**
* Storage Mock
*
* localStorage and sessionStorage implementation.
*/
export function createStorage() {
const data = new Map();
const storage = {
get length() {
return data.size;
},
key(index) {
const keys = [...data.keys()];
return keys[index] ?? null;
},
getItem(key) {
return data.get(String(key)) ?? null;
},
setItem(key, value) {
data.set(String(key), String(value));
},
removeItem(key) {
data.delete(String(key));
},
clear() {
data.clear();
},
};
// Make it behave like real Storage (array-like access)
return new Proxy(storage, {
get(target, prop) {
if (prop in target) {
return target[prop];
}
if (typeof prop === 'string') {
return target.getItem(prop);
}
return undefined;
},
set(target, prop, value) {
if (typeof prop === 'string' && !(prop in target)) {
target.setItem(prop, value);
return true;
}
return false;
},
deleteProperty(target, prop) {
target.removeItem(prop);
return true;
},
has(target, prop) {
return prop in target || data.has(String(prop));
},
ownKeys(target) {
return [...data.keys()];
},
getOwnPropertyDescriptor(target, prop) {
if (data.has(String(prop))) {
return {
value: data.get(String(prop)),
writable: true,
enumerable: true,
configurable: true,
};
}
return undefined;
},
});
}

View File

@@ -0,0 +1,85 @@
'use strict';
/**
* P0: RTCPeerConnection mock
* P0: OfflineAudioContext mock
* hsw 检测:构造函数存在性 + 原型链 + toString() 不暴露源码
*/
const { createNative, nativeClass } = require('./native');
// ── RTCPeerConnection ────────────────────────────────────────
class RTCPeerConnection {
constructor(config) {
this.localDescription = null;
this.remoteDescription = null;
this.signalingState = 'stable';
this.iceConnectionState = 'new';
this.iceGatheringState = 'new';
this.connectionState = 'new';
this._config = config || {};
}
}
RTCPeerConnection.prototype.createOffer = createNative('createOffer', function (options) {
return Promise.resolve({ type: 'offer', sdp: 'v=0\r\n' });
});
RTCPeerConnection.prototype.createAnswer = createNative('createAnswer', function () {
return Promise.resolve({ type: 'answer', sdp: 'v=0\r\n' });
});
RTCPeerConnection.prototype.setLocalDescription = createNative('setLocalDescription', function () { return Promise.resolve(); });
RTCPeerConnection.prototype.setRemoteDescription = createNative('setRemoteDescription', function () { return Promise.resolve(); });
RTCPeerConnection.prototype.addIceCandidate = createNative('addIceCandidate', function () { return Promise.resolve(); });
RTCPeerConnection.prototype.createDataChannel = createNative('createDataChannel', function (label) {
return { label, readyState: 'open', close: createNative('close', function(){}) };
});
RTCPeerConnection.prototype.close = createNative('close', function () {});
RTCPeerConnection.prototype.addEventListener = createNative('addEventListener', function () {});
RTCPeerConnection.prototype.removeEventListener = createNative('removeEventListener', function () {});
nativeClass(RTCPeerConnection);
// ── OfflineAudioContext ──────────────────────────────────────
class OfflineAudioContext {
constructor(channels, length, sampleRate) {
this.length = length || 4096;
this.sampleRate = sampleRate || 44100;
this.channels = channels || 1;
this.state = 'suspended';
this.destination = { channelCount: channels || 1 };
}
}
OfflineAudioContext.prototype.createAnalyser = createNative('createAnalyser', function () {
return {
fftSize: 2048,
frequencyBinCount: 1024,
connect: createNative('connect', function () {}),
getFloatFrequencyData: createNative('getFloatFrequencyData', function (arr) {
for (let i = 0; i < arr.length; i++) arr[i] = -100 + Math.random() * 5;
}),
};
});
OfflineAudioContext.prototype.createOscillator = createNative('createOscillator', function () {
return {
type: 'triangle',
frequency: { value: 10000 },
connect: createNative('connect', function () {}),
start: createNative('start', function () {}),
};
});
OfflineAudioContext.prototype.createDynamicsCompressor = createNative('createDynamicsCompressor', function () {
return {
threshold: { value: -50 }, knee: { value: 40 },
ratio: { value: 12 }, attack: { value: 0 }, release: { value: 0.25 },
connect: createNative('connect', function () {}),
};
});
OfflineAudioContext.prototype.startRendering = createNative('startRendering', function () {
const len = this.length;
// 固定指纹数据,保持每次一致(稳定指纹)
const data = new Float32Array(len);
for (let i = 0; i < len; i++) data[i] = Math.sin(i * 0.001) * 0.01;
return Promise.resolve({ getChannelData: () => data });
});
OfflineAudioContext.prototype.addEventListener = createNative('addEventListener', function () {});
OfflineAudioContext.prototype.removeEventListener = createNative('removeEventListener', function () {});
nativeClass(OfflineAudioContext);
module.exports = { RTCPeerConnection, OfflineAudioContext };

View File

@@ -1,484 +1,239 @@
'use strict';
/**
* Window Mock
*
* The global object for browser environments.
* This ties everything together.
* 总装window 沙盒
* 按 P0→P1→P2 顺序挂载所有 mock并用 Proxy 屏蔽 bot 字段
*/
import windowStubs from '../stubs/window_stubs.json' with { type: 'json' };
import chromeProps from '../stubs/chrome_props.json' with { type: 'json' };
import { createScreen } from './screen.js';
import { createNavigator } from './navigator.js';
import { createDocument } from './document.js';
import { createPerformance } from './performance.js';
import { createCrypto } from './crypto.js';
import { createStorage } from './storage.js';
const { createNative, nativeClass } = require('./native');
const { isBotKey } = require('./bot_shield');
const performanceMock = require('./performance');
const navigatorMock = require('./navigator');
const { RTCPeerConnection, OfflineAudioContext } = require('./webapi');
const { HTMLCanvasElement, CanvasRenderingContext2D } = require('./canvas');
const { cryptoMock, Storage, IDBFactory, Notification, atob, btoa } = require('./crypto');
const screenMock = require('./screen');
const HTMLDocument = require('./document');
export function createWindow(fingerprint = {}) {
const stubs = { ...windowStubs, ...fingerprint.window };
// ── 基础 window 对象 ─────────────────────────────────────────
const _win = {
const screen = createScreen(fingerprint.screen);
const navigator = createNavigator(fingerprint.navigator);
const document = createDocument(fingerprint);
const performance = createPerformance(fingerprint);
const crypto = createCrypto();
const localStorage = createStorage();
const sessionStorage = createStorage();
// ── P0: 核心 API ──────────────────────────────────────
performance: performanceMock,
navigator: navigatorMock,
screen: screenMock,
crypto: cryptoMock,
const eventListeners = new Map();
let origin = fingerprint.origin || 'https://example.com';
RTCPeerConnection,
webkitRTCPeerConnection: RTCPeerConnection,
OfflineAudioContext,
const location = createLocation(fingerprint.url || 'https://example.com/');
// ── P1: Canvas ────────────────────────────────────────
HTMLCanvasElement,
CanvasRenderingContext2D,
const history = {
length: 1,
// ── P1: Storage / IDB ─────────────────────────────────
localStorage: new Storage(),
sessionStorage: new Storage(),
indexedDB: new IDBFactory(),
IDBFactory,
// ── P1: Notification ──────────────────────────────────
Notification,
// ── P1: atob / btoa ───────────────────────────────────
atob,
btoa,
// ── P1: Document ──────────────────────────────────────
document: new HTMLDocument(),
HTMLDocument,
// ── P2: 移动端触摸 → 桌面不存在 ──────────────────────
// ontouchstart: 不定义Proxy 返回 undefined
// ── 基础 JS 全局 ─────────────────────────────────────
Promise,
Object,
Array,
Function,
Number,
String,
Boolean,
Symbol,
Date,
RegExp,
Error,
Math,
JSON,
parseInt,
parseFloat,
isNaN,
isFinite,
decodeURI,
decodeURIComponent,
encodeURI,
encodeURIComponent,
escape,
unescape,
eval,
undefined,
Infinity,
NaN,
globalThis: null, // 在 Proxy 建好后回填
// ── 定时器Node 原生) ───────────────────────────────
setTimeout,
clearTimeout,
setInterval,
clearInterval,
queueMicrotask,
// ── 其他常见 window 属性 ──────────────────────────────
location: {
href: 'https://newassets.hcaptcha.com/captcha/v1/xxx/static/hcaptcha.html',
origin: 'https://newassets.hcaptcha.com',
protocol: 'https:',
host: 'newassets.hcaptcha.com',
hostname: 'newassets.hcaptcha.com',
port: '',
pathname: '/captcha/v1/xxx/static/hcaptcha.html',
search: '',
hash: '',
ancestorOrigins: { 0: 'https://b.stripecdn.com', 1: 'https://js.stripe.com', length: 2 },
},
innerWidth: 530,
innerHeight: 915,
outerWidth: 530,
outerHeight: 915,
devicePixelRatio: 2,
screenX: 0,
screenY: 0,
screenLeft: 0,
screenTop: 0,
scrollX: 0,
scrollY: 0,
pageXOffset: 0,
pageYOffset: 0,
closed: false,
name: '',
status: '',
opener: null,
parent: null, // 回填
top: null, // 回填
self: null, // 回填
frames: null, // 回填
length: 0,
isSecureContext: true,
crossOriginIsolated: false,
originAgentCluster: false,
history: {
length: 1,
state: null,
scrollRestoration: 'auto',
state: null,
back() {},
forward() {},
go() {},
pushState() {},
replaceState() {},
};
go: createNative('go', function () {}),
back: createNative('back', function () {}),
forward: createNative('forward', function () {}),
pushState: createNative('pushState', function () {}),
replaceState: createNative('replaceState', function () {}),
},
const win = {
// Window identity
window: null, // Self-reference, set below
self: null,
top: null,
parent: null,
globalThis: null,
frames: [],
length: 0,
frameElement: null,
opener: null,
closed: false,
name: '',
fetch: createNative('fetch', function (url, opts) {
// 沙盒里一般不真正发请求,返回 resolved 空 response
return Promise.resolve({
ok: true, status: 200,
json: () => Promise.resolve({}),
text: () => Promise.resolve(''),
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
});
}),
// Core objects
document,
navigator,
screen,
location,
history,
performance,
crypto,
localStorage,
sessionStorage,
Request: createNative('Request', function (url, opts) { this.url = url; this.method = opts?.method || 'GET'; }),
Response: createNative('Response', function (body, opts) { this.status = opts?.status || 200; }),
Headers: createNative('Headers', function () { this._h = {}; }),
// Visual viewport
visualViewport: {
width: stubs.innerWidth,
height: stubs.innerHeight,
offsetLeft: 0,
offsetTop: 0,
pageLeft: 0,
pageTop: 0,
scale: 1,
addEventListener() {},
removeEventListener() {},
},
URL: createNative('URL', function (url, base) {
const u = new (require('url').URL)(url, base);
Object.assign(this, u);
}),
URLSearchParams,
// Dimensions
innerWidth: stubs.innerWidth,
innerHeight: stubs.innerHeight,
outerWidth: stubs.outerWidth,
outerHeight: stubs.outerHeight,
devicePixelRatio: stubs.devicePixelRatio,
addEventListener: createNative('addEventListener', function () {}),
removeEventListener: createNative('removeEventListener', function () {}),
dispatchEvent: createNative('dispatchEvent', function () { return true; }),
postMessage: createNative('postMessage', function () {}),
// Scroll
pageXOffset: stubs.pageXOffset,
pageYOffset: stubs.pageYOffset,
scrollX: stubs.scrollX,
scrollY: stubs.scrollY,
alert: createNative('alert', function () {}),
confirm: createNative('confirm', function () { return false; }),
prompt: createNative('prompt', function () { return null; }),
// Screen position
screenX: stubs.screenX,
screenY: stubs.screenY,
screenLeft: stubs.screenLeft,
screenTop: stubs.screenTop,
requestAnimationFrame: createNative('requestAnimationFrame', function (cb) { return setTimeout(cb, 16); }),
cancelAnimationFrame: createNative('cancelAnimationFrame', function (id) { clearTimeout(id); }),
requestIdleCallback: createNative('requestIdleCallback', function (cb) { return setTimeout(() => cb({ timeRemaining: () => 50, didTimeout: false }), 1); }),
cancelIdleCallback: createNative('cancelIdleCallback', function (id) { clearTimeout(id); }),
// Security
origin,
isSecureContext: stubs.isSecureContext,
crossOriginIsolated: stubs.crossOriginIsolated,
originAgentCluster: stubs.originAgentCluster,
getComputedStyle: createNative('getComputedStyle', function () {
return new Proxy({}, { get: (_, p) => p === 'getPropertyValue' ? (() => '') : '' });
}),
// Chrome object
chrome: chromeProps,
structuredClone: createNative('structuredClone', (v) => JSON.parse(JSON.stringify(v))),
// Caches
caches: {
open: () => Promise.resolve({
match: () => Promise.resolve(undefined),
matchAll: () => Promise.resolve([]),
add: () => Promise.resolve(),
addAll: () => Promise.resolve(),
put: () => Promise.resolve(),
delete: () => Promise.resolve(false),
keys: () => Promise.resolve([]),
}),
match: () => Promise.resolve(undefined),
has: () => Promise.resolve(false),
delete: () => Promise.resolve(false),
keys: () => Promise.resolve([]),
},
TextEncoder,
TextDecoder,
Uint8Array,
Int8Array,
Uint16Array,
Int16Array,
Uint32Array,
Int32Array,
Uint8ClampedArray,
Float32Array,
Float64Array,
ArrayBuffer,
DataView,
Map,
Set,
WeakMap,
WeakSet,
Proxy,
Reflect,
BigInt,
Symbol,
WebAssembly,
};
// IndexedDB
indexedDB: createIndexedDB(),
// ── 建 Proxy屏蔽 bot 字段 + 回填自引用 ────────────────────
const windowProxy = new Proxy(_win, {
get(target, prop) {
if (isBotKey(prop)) return undefined; // 🚨 bot 字段全部返回 undefined
const val = target[prop];
if (val === null && ['self','window','frames','parent','top','globalThis'].includes(prop)) {
return windowProxy;
}
return val;
},
has(target, prop) {
if (isBotKey(prop)) return false; // 拦截 'webdriver' in window
return prop in target;
},
set(target, prop, val) {
if (isBotKey(prop)) return true; // 静默丢弃 bot 字段的写入
target[prop] = val;
return true;
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(k => !isBotKey(k));
},
});
// Scheduler
scheduler: {
postTask: (cb) => Promise.resolve(cb()),
},
// 回填自引用
_win.self = windowProxy;
_win.window = windowProxy;
_win.globalThis = windowProxy;
_win.frames = windowProxy;
_win.parent = windowProxy;
_win.top = windowProxy;
// Speech
speechSynthesis: {
pending: false,
speaking: false,
paused: false,
getVoices: () => [],
speak: () => {},
cancel: () => {},
pause: () => {},
resume: () => {},
addEventListener: () => {},
removeEventListener: () => {},
},
// CSS
CSS: {
supports: () => true,
escape: (str) => str,
px: (n) => `${n}px`,
em: (n) => `${n}em`,
rem: (n) => `${n}rem`,
vh: (n) => `${n}vh`,
vw: (n) => `${n}vw`,
percent: (n) => `${n}%`,
},
// Match media
matchMedia(query) {
const matches = query.includes('prefers-color-scheme: light') ||
query.includes('(min-width:') ||
query.includes('screen');
return {
matches,
media: query,
onchange: null,
addEventListener() {},
removeEventListener() {},
addListener() {},
removeListener() {},
};
},
// Computed style
getComputedStyle(element, pseudo) {
return new Proxy({}, {
get(target, prop) {
if (prop === 'getPropertyValue') return () => '';
if (prop === 'length') return 0;
if (prop === 'cssText') return '';
return '';
}
});
},
// Scroll methods
scroll() {},
scrollTo() {},
scrollBy() {},
// Focus
focus() {},
blur() {},
// Print
print() {},
// Alerts
alert() {},
confirm() { return false; },
prompt() { return null; },
// Open/Close
open() { return null; },
close() {},
stop() {},
// Animation
requestAnimationFrame(cb) {
return setTimeout(() => cb(performance.now()), 16);
},
cancelAnimationFrame(id) {
clearTimeout(id);
},
requestIdleCallback(cb) {
return setTimeout(() => cb({
didTimeout: false,
timeRemaining: () => 50,
}), 1);
},
cancelIdleCallback(id) {
clearTimeout(id);
},
// Timers
setTimeout: globalThis.setTimeout,
clearTimeout: globalThis.clearTimeout,
setInterval: globalThis.setInterval,
clearInterval: globalThis.clearInterval,
queueMicrotask: globalThis.queueMicrotask,
// Encoding
btoa(str) {
return Buffer.from(str, 'binary').toString('base64');
},
atob(str) {
return Buffer.from(str, 'base64').toString('binary');
},
// Fetch API
fetch: globalThis.fetch,
Request: globalThis.Request,
Response: globalThis.Response,
Headers: globalThis.Headers,
// URL
URL: globalThis.URL,
URLSearchParams: globalThis.URLSearchParams,
// Events
Event: globalThis.Event || class Event {
constructor(type, options = {}) {
this.type = type;
this.bubbles = options.bubbles || false;
this.cancelable = options.cancelable || false;
this.composed = options.composed || false;
this.defaultPrevented = false;
this.timeStamp = Date.now();
}
preventDefault() { this.defaultPrevented = true; }
stopPropagation() {}
stopImmediatePropagation() {}
},
CustomEvent: globalThis.CustomEvent || class CustomEvent extends Event {
constructor(type, options = {}) {
super(type, options);
this.detail = options.detail || null;
}
},
MessageEvent: class MessageEvent {
constructor(type, options = {}) {
this.type = type;
this.data = options.data;
this.origin = options.origin || '';
this.lastEventId = options.lastEventId || '';
this.source = options.source || null;
this.ports = options.ports || [];
}
},
// Event listener management
addEventListener(type, listener, options) {
if (!eventListeners.has(type)) {
eventListeners.set(type, []);
}
eventListeners.get(type).push(listener);
},
removeEventListener(type, listener, options) {
const listeners = eventListeners.get(type);
if (listeners) {
const idx = listeners.indexOf(listener);
if (idx > -1) listeners.splice(idx, 1);
}
},
dispatchEvent(event) {
const listeners = eventListeners.get(event.type);
if (listeners) {
listeners.forEach(fn => fn(event));
}
return true;
},
// Post message
postMessage(data, targetOrigin, transfer) {},
// Workers
Worker: class Worker {
constructor(url) {
this.onmessage = null;
this.onerror = null;
}
postMessage() {}
terminate() {}
addEventListener() {}
removeEventListener() {}
},
SharedWorker: undefined,
// Blob & File
Blob: globalThis.Blob,
File: globalThis.File || class File extends Blob {
constructor(bits, name, options = {}) {
super(bits, options);
this.name = name;
this.lastModified = options.lastModified || Date.now();
}
},
FileReader: class FileReader {
readAsText() { this.onload?.({ target: { result: '' } }); }
readAsDataURL() { this.onload?.({ target: { result: 'data:,' } }); }
readAsArrayBuffer() { this.onload?.({ target: { result: new ArrayBuffer(0) } }); }
readAsBinaryString() { this.onload?.({ target: { result: '' } }); }
abort() {}
},
// ArrayBuffer & TypedArrays
ArrayBuffer: globalThis.ArrayBuffer,
SharedArrayBuffer: globalThis.SharedArrayBuffer,
Uint8Array: globalThis.Uint8Array,
Uint16Array: globalThis.Uint16Array,
Uint32Array: globalThis.Uint32Array,
Int8Array: globalThis.Int8Array,
Int16Array: globalThis.Int16Array,
Int32Array: globalThis.Int32Array,
Float32Array: globalThis.Float32Array,
Float64Array: globalThis.Float64Array,
Uint8ClampedArray: globalThis.Uint8ClampedArray,
BigInt64Array: globalThis.BigInt64Array,
BigUint64Array: globalThis.BigUint64Array,
DataView: globalThis.DataView,
// Text encoding
TextEncoder: globalThis.TextEncoder,
TextDecoder: globalThis.TextDecoder,
// Intl
Intl: globalThis.Intl,
// WebAssembly
WebAssembly: globalThis.WebAssembly,
// Core language
Object: globalThis.Object,
Array: globalThis.Array,
String: globalThis.String,
Number: globalThis.Number,
Boolean: globalThis.Boolean,
Symbol: globalThis.Symbol,
BigInt: globalThis.BigInt,
Math: globalThis.Math,
Date: globalThis.Date,
JSON: globalThis.JSON,
RegExp: globalThis.RegExp,
Error: globalThis.Error,
TypeError: globalThis.TypeError,
RangeError: globalThis.RangeError,
SyntaxError: globalThis.SyntaxError,
ReferenceError: globalThis.ReferenceError,
EvalError: globalThis.EvalError,
URIError: globalThis.URIError,
AggregateError: globalThis.AggregateError,
Promise: globalThis.Promise,
Proxy: globalThis.Proxy,
Reflect: globalThis.Reflect,
Map: globalThis.Map,
Set: globalThis.Set,
WeakMap: globalThis.WeakMap,
WeakSet: globalThis.WeakSet,
WeakRef: globalThis.WeakRef,
FinalizationRegistry: globalThis.FinalizationRegistry,
// Functions
Function: globalThis.Function,
eval: globalThis.eval,
isNaN: globalThis.isNaN,
isFinite: globalThis.isFinite,
parseFloat: globalThis.parseFloat,
parseInt: globalThis.parseInt,
decodeURI: globalThis.decodeURI,
decodeURIComponent: globalThis.decodeURIComponent,
encodeURI: globalThis.encodeURI,
encodeURIComponent: globalThis.encodeURIComponent,
// Console
console: globalThis.console,
// Undefined/NaN/Infinity
undefined: undefined,
NaN: NaN,
Infinity: Infinity,
};
// Self-references
win.window = win;
win.self = win;
win.top = win;
win.parent = win;
win.globalThis = win;
// Connect document to window
document.defaultView = win;
return win;
}
function createLocation(url) {
const parsed = new URL(url);
return {
href: parsed.href,
protocol: parsed.protocol,
host: parsed.host,
hostname: parsed.hostname,
port: parsed.port,
pathname: parsed.pathname,
search: parsed.search,
hash: parsed.hash,
origin: parsed.origin,
ancestorOrigins: {
length: 0,
item: () => null,
contains: () => false,
},
assign() {},
replace() {},
reload() {},
toString() { return this.href; },
};
}
function createIndexedDB() {
return {
open() {
return {
result: null,
error: null,
readyState: 'done',
onsuccess: null,
onerror: null,
onupgradeneeded: null,
onblocked: null,
};
},
deleteDatabase() {
return {
result: undefined,
error: null,
readyState: 'done',
onsuccess: null,
onerror: null,
onblocked: null,
};
},
databases() {
return Promise.resolve([]);
},
cmp() {
return 0;
},
};
}
module.exports = windowProxy;

View File

@@ -1,59 +0,0 @@
{
"app": {
"isInstalled": false,
"InstallState": {
"DISABLED": "disabled",
"INSTALLED": "installed",
"NOT_INSTALLED": "not_installed"
},
"RunningState": {
"CANNOT_RUN": "cannot_run",
"READY_TO_RUN": "ready_to_run",
"RUNNING": "running"
}
},
"runtime": {
"OnInstalledReason": {
"CHROME_UPDATE": "chrome_update",
"INSTALL": "install",
"SHARED_MODULE_UPDATE": "shared_module_update",
"UPDATE": "update"
},
"OnRestartRequiredReason": {
"APP_UPDATE": "app_update",
"OS_UPDATE": "os_update",
"PERIODIC": "periodic"
},
"PlatformArch": {
"ARM": "arm",
"ARM64": "arm64",
"MIPS": "mips",
"MIPS64": "mips64",
"X86_32": "x86-32",
"X86_64": "x86-64"
},
"PlatformNaclArch": {
"ARM": "arm",
"MIPS": "mips",
"MIPS64": "mips64",
"X86_32": "x86-32",
"X86_64": "x86-64"
},
"PlatformOs": {
"ANDROID": "android",
"CROS": "cros",
"FUCHSIA": "fuchsia",
"LINUX": "linux",
"MAC": "mac",
"OPENBSD": "openbsd",
"WIN": "win"
},
"RequestUpdateCheckStatus": {
"NO_UPDATE": "no_update",
"THROTTLED": "throttled",
"UPDATE_AVAILABLE": "update_available"
}
},
"csi": {},
"loadTimes": {}
}

View File

@@ -1,73 +0,0 @@
{
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"appVersion": "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"platform": "Win32",
"vendor": "Google Inc.",
"vendorSub": "",
"product": "Gecko",
"productSub": "20030107",
"appName": "Netscape",
"appCodeName": "Mozilla",
"language": "en-US",
"languages": ["en-US", "en"],
"onLine": true,
"cookieEnabled": true,
"doNotTrack": null,
"maxTouchPoints": 0,
"hardwareConcurrency": 8,
"deviceMemory": 8,
"pdfViewerEnabled": true,
"webdriver": false,
"userAgentData": {
"brands": [
{ "brand": "Not_A Brand", "version": "8" },
{ "brand": "Chromium", "version": "120" },
{ "brand": "Google Chrome", "version": "120" }
],
"mobile": false,
"platform": "Windows"
},
"connection": {
"effectiveType": "4g",
"rtt": 50,
"downlink": 10,
"saveData": false
},
"plugins": {
"length": 5,
"items": [
{
"name": "PDF Viewer",
"filename": "internal-pdf-viewer",
"description": "Portable Document Format"
},
{
"name": "Chrome PDF Viewer",
"filename": "internal-pdf-viewer",
"description": "Portable Document Format"
},
{
"name": "Chromium PDF Viewer",
"filename": "internal-pdf-viewer",
"description": "Portable Document Format"
},
{
"name": "Microsoft Edge PDF Viewer",
"filename": "internal-pdf-viewer",
"description": "Portable Document Format"
},
{
"name": "WebKit built-in PDF",
"filename": "internal-pdf-viewer",
"description": "Portable Document Format"
}
]
},
"mimeTypes": {
"length": 2,
"items": [
{ "type": "application/pdf", "suffixes": "pdf", "description": "Portable Document Format" },
{ "type": "text/pdf", "suffixes": "pdf", "description": "Portable Document Format" }
]
}
}

View File

@@ -1,15 +0,0 @@
{
"width": 1920,
"height": 1080,
"availWidth": 1920,
"availHeight": 1040,
"availLeft": 0,
"availTop": 0,
"colorDepth": 24,
"pixelDepth": 24,
"orientation": {
"type": "landscape-primary",
"angle": 0
},
"isExtended": false
}

View File

@@ -1,58 +0,0 @@
{
"vendor": "WebKit",
"renderer": "WebKit WebGL",
"unmaskedVendor": "Google Inc. (Intel)",
"unmaskedRenderer": "ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E92), OpenGL 4.6)",
"version": "WebGL 1.0 (OpenGL ES 2.0 Chromium)",
"shadingLanguageVersion": "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)",
"maxTextureSize": 16384,
"maxCubeMapTextureSize": 16384,
"maxRenderbufferSize": 16384,
"maxViewportDims": [32767, 32767],
"maxVertexAttribs": 16,
"maxVertexUniformVectors": 4096,
"maxVaryingVectors": 30,
"maxFragmentUniformVectors": 1024,
"maxVertexTextureImageUnits": 16,
"maxTextureImageUnits": 16,
"maxCombinedTextureImageUnits": 32,
"aliasedLineWidthRange": [1, 1],
"aliasedPointSizeRange": [1, 1024],
"extensions": [
"ANGLE_instanced_arrays",
"EXT_blend_minmax",
"EXT_color_buffer_half_float",
"EXT_float_blend",
"EXT_frag_depth",
"EXT_shader_texture_lod",
"EXT_texture_compression_bptc",
"EXT_texture_compression_rgtc",
"EXT_texture_filter_anisotropic",
"EXT_sRGB",
"OES_element_index_uint",
"OES_fbo_render_mipmap",
"OES_standard_derivatives",
"OES_texture_float",
"OES_texture_float_linear",
"OES_texture_half_float",
"OES_texture_half_float_linear",
"OES_vertex_array_object",
"WEBGL_color_buffer_float",
"WEBGL_compressed_texture_s3tc",
"WEBGL_compressed_texture_s3tc_srgb",
"WEBGL_debug_renderer_info",
"WEBGL_debug_shaders",
"WEBGL_depth_texture",
"WEBGL_draw_buffers",
"WEBGL_lose_context",
"WEBGL_multi_draw"
],
"parameters": {
"37445": "Google Inc. (Intel)",
"37446": "ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E92), OpenGL 4.6)",
"7936": "WebKit",
"7937": "WebKit WebGL",
"7938": "WebGL 1.0 (OpenGL ES 2.0 Chromium)",
"35724": "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)"
}
}

View File

@@ -1,28 +0,0 @@
{
"innerWidth": 1920,
"innerHeight": 1080,
"outerWidth": 1920,
"outerHeight": 1040,
"devicePixelRatio": 1,
"screenX": 0,
"screenY": 0,
"screenLeft": 0,
"screenTop": 0,
"pageXOffset": 0,
"pageYOffset": 0,
"scrollX": 0,
"scrollY": 0,
"visualViewport": {
"width": 1920,
"height": 1080,
"offsetLeft": 0,
"offsetTop": 0,
"pageLeft": 0,
"pageTop": 0,
"scale": 1
},
"isSecureContext": true,
"crossOriginIsolated": false,
"originAgentCluster": false,
"scheduler": {}
}

View File

@@ -1,62 +1,39 @@
'use strict';
/**
* Logger - Because debugging blind is suffering
* Simple logger utility
*/
const COLORS = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
gray: '\x1b[90m',
cyan: '\x1b[36m',
gray: '\x1b[90m',
};
const LEVELS = {
debug: { color: COLORS.gray, priority: 0 },
info: { color: COLORS.cyan, priority: 1 },
warn: { color: COLORS.yellow, priority: 2 },
error: { color: COLORS.red, priority: 3 },
success: { color: COLORS.green, priority: 1 },
};
export class Logger {
static globalLevel = process.env.LOG_LEVEL || 'info';
function timestamp() {
return new Date().toISOString().replace('T', ' ').replace('Z', '');
}
class Logger {
constructor(name) {
this.name = name;
}
_log(level, message, ...args) {
const levelConfig = LEVELS[level];
const globalPriority = LEVELS[Logger.globalLevel]?.priority || 1;
if (levelConfig.priority < globalPriority) return;
const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
const prefix = `${COLORS.gray}[${timestamp}]${COLORS.reset} ${levelConfig.color}[${level.toUpperCase()}]${COLORS.reset} ${COLORS.magenta}[${this.name}]${COLORS.reset}`;
console.log(`${prefix} ${message}`, ...args);
_log(level, color, ...args) {
const ts = timestamp();
const prefix = `${COLORS.gray}${ts}${COLORS.reset} ${color}[${this.name}]${COLORS.reset} ${color}${level}:${COLORS.reset}`;
console.log(prefix, ...args);
}
debug(message, ...args) {
this._log('debug', message, ...args);
}
info(message, ...args) {
this._log('info', message, ...args);
}
warn(message, ...args) {
this._log('warn', message, ...args);
}
error(message, ...args) {
this._log('error', message, ...args);
}
success(message, ...args) {
this._log('success', message, ...args);
}
debug(...args) { this._log('DEBUG', COLORS.gray, ...args); }
info(...args) { this._log('INFO', COLORS.blue, ...args); }
warn(...args) { this._log('WARN', COLORS.yellow, ...args); }
error(...args) { this._log('ERROR', COLORS.red, ...args); }
success(...args) { this._log('SUCCESS', COLORS.green, ...args); }
}
module.exports = { Logger };

View File

@@ -1,89 +0,0 @@
/**
* Protobuf Utilities
*
* hCaptcha sometimes returns protobuf-encoded responses.
* This module handles parsing if needed.
*
* Note: Usually not required - we just need the generated_pass_UUID
* from the JSON response. But keeping this here for completeness.
*/
export class ProtobufParser {
/**
* Basic varint decoder
*/
static decodeVarint(buffer, offset = 0) {
let result = 0;
let shift = 0;
let byte;
do {
byte = buffer[offset++];
result |= (byte & 0x7f) << shift;
shift += 7;
} while (byte & 0x80);
return { value: result, bytesRead: offset };
}
/**
* Parse a simple protobuf message
* Returns an object with field numbers as keys
*/
static parse(buffer) {
const result = {};
let offset = 0;
while (offset < buffer.length) {
const tag = this.decodeVarint(buffer, offset);
offset = tag.bytesRead;
const fieldNumber = tag.value >> 3;
const wireType = tag.value & 0x7;
let value;
switch (wireType) {
case 0: // Varint
const varint = this.decodeVarint(buffer, offset);
value = varint.value;
offset = varint.bytesRead;
break;
case 1: // 64-bit
value = buffer.readBigUInt64LE(offset);
offset += 8;
break;
case 2: // Length-delimited
const length = this.decodeVarint(buffer, offset);
offset = length.bytesRead;
value = buffer.slice(offset, offset + length.value);
offset += length.value;
break;
case 5: // 32-bit
value = buffer.readUInt32LE(offset);
offset += 4;
break;
default:
throw new Error(`Unknown wire type: ${wireType}`);
}
result[fieldNumber] = value;
}
return result;
}
/**
* Try to decode a buffer as UTF-8 string
*/
static bufferToString(buffer) {
if (Buffer.isBuffer(buffer)) {
return buffer.toString('utf-8');
}
return String(buffer);
}
}