'use strict'; /** * 总装:window 沙盒 * 按 P0→P1→P2 顺序挂载所有 mock,并用 Proxy 屏蔽 bot 字段 */ 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'); // ── 基础 window 对象 ───────────────────────────────────────── const _win = { // ── P0: 核心 API ────────────────────────────────────── performance: performanceMock, navigator: navigatorMock, screen: screenMock, crypto: cryptoMock, RTCPeerConnection, webkitRTCPeerConnection: RTCPeerConnection, OfflineAudioContext, // ── P1: Canvas ──────────────────────────────────────── HTMLCanvasElement, CanvasRenderingContext2D, // ── 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', go: createNative('go', function () {}), back: createNative('back', function () {}), forward: createNative('forward', function () {}), pushState: createNative('pushState', function () {}), replaceState: createNative('replaceState', function () {}), }, 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)), }); }), 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 = {}; }), URL: createNative('URL', function (url, base) { const u = new (require('url').URL)(url, base); Object.assign(this, u); }), URLSearchParams, addEventListener: createNative('addEventListener', function () {}), removeEventListener: createNative('removeEventListener', function () {}), dispatchEvent: createNative('dispatchEvent', function () { return true; }), postMessage: createNative('postMessage', function () {}), alert: createNative('alert', function () {}), confirm: createNative('confirm', function () { return false; }), prompt: createNative('prompt', function () { return null; }), 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); }), getComputedStyle: createNative('getComputedStyle', function () { return new Proxy({}, { get: (_, p) => p === 'getPropertyValue' ? (() => '') : '' }); }), structuredClone: createNative('structuredClone', (v) => JSON.parse(JSON.stringify(v))), TextEncoder, TextDecoder, Uint8Array, Int8Array, Uint16Array, Int16Array, Uint32Array, Int32Array, Uint8ClampedArray, Float32Array, Float64Array, ArrayBuffer, DataView, Map, Set, WeakMap, WeakSet, Proxy, Reflect, BigInt, Symbol, WebAssembly, }; // ── 建 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)); }, }); // 回填自引用 _win.self = windowProxy; _win.window = windowProxy; _win.globalThis = windowProxy; _win.frames = windowProxy; _win.parent = windowProxy; _win.top = windowProxy; module.exports = windowProxy;