240 lines
8.2 KiB
JavaScript
240 lines
8.2 KiB
JavaScript
'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;
|