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

@@ -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;