reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
// 基础 env 模板:提供网页端最小对象,并按描述符、原型链和 native-like 规则固化。
|
||||
// 复制到 case/result 或 case/env 后,必须根据 fixtures 和 RuyiTrace 证据调整具体字段。
|
||||
'use strict';
|
||||
|
||||
let protect = null;
|
||||
try { protect = require('./native-protect'); } catch { protect = null; }
|
||||
|
||||
function defineValue(obj, key, value, options = {}) {
|
||||
if (protect && protect.defineValue) return protect.defineValue(obj, key, value, options);
|
||||
Object.defineProperty(obj, key, {
|
||||
value,
|
||||
writable: options.writable ?? false,
|
||||
enumerable: options.enumerable ?? true,
|
||||
configurable: options.configurable ?? true,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
function defineGetter(obj, key, getter, options = {}) {
|
||||
Object.defineProperty(obj, key, {
|
||||
get: getter,
|
||||
enumerable: options.enumerable ?? true,
|
||||
configurable: options.configurable ?? true,
|
||||
});
|
||||
return getter;
|
||||
}
|
||||
|
||||
function markNativeFunction(fn, name) {
|
||||
if (protect && protect.markNativeFunction) return protect.markNativeFunction(fn, name);
|
||||
return fn;
|
||||
}
|
||||
|
||||
function markObjectToString(obj, tag) {
|
||||
if (protect && protect.markObjectToString) return protect.markObjectToString(obj, tag);
|
||||
try { Object.defineProperty(obj, Symbol.toStringTag, { value: tag, enumerable: false, configurable: true }); } catch {}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function defineNativeGetter(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeGetter) return protect.defineNativeGetter(obj, key, impl, options, addon);
|
||||
return defineGetter(obj, key, impl, options);
|
||||
}
|
||||
|
||||
function defineNativeValue(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeValue) return protect.defineNativeValue(obj, key, impl, options, addon);
|
||||
return defineValue(obj, key, impl, { writable: true, ...options });
|
||||
}
|
||||
|
||||
function createNativeConstructor(name, length, impl, addon) {
|
||||
if (protect && protect.createNativeConstructor) return protect.createNativeConstructor(name, length, impl, addon);
|
||||
function Ctor(...args) { return impl.call(this, new.target ? true : false, ...args); }
|
||||
try { Object.defineProperty(Ctor, 'name', { value: name, configurable: true }); } catch {}
|
||||
return markNativeFunction(Ctor, name);
|
||||
}
|
||||
|
||||
function throwIllegalConstructor(name, addon) {
|
||||
if (protect && protect.throwIllegalConstructor) return protect.throwIllegalConstructor(name, addon);
|
||||
throw new TypeError(`Failed to construct '${name}': Illegal constructor`);
|
||||
}
|
||||
|
||||
function installBaseEnv(globalObject, fixture = {}, options = {}) {
|
||||
const addon = options.addon || null;
|
||||
const browser = fixture.browser || {};
|
||||
const pageUrl = fixture.pageUrl || fixture.apiUrl || 'https://example.com/';
|
||||
const url = new URL(pageUrl);
|
||||
|
||||
const Window = createNativeConstructor('Window', 0, function Window(isNew) {
|
||||
return throwIllegalConstructor('Window', addon);
|
||||
}, addon);
|
||||
const Location = createNativeConstructor('Location', 0, function Location(isNew) {
|
||||
return throwIllegalConstructor('Location', addon);
|
||||
}, addon);
|
||||
const Navigator = createNativeConstructor('Navigator', 0, function Navigator(isNew) {
|
||||
return throwIllegalConstructor('Navigator', addon);
|
||||
}, addon);
|
||||
|
||||
defineValue(Window.prototype, 'constructor', Window, { writable: true, enumerable: false, configurable: true });
|
||||
defineValue(Location.prototype, 'constructor', Location, { writable: true, enumerable: false, configurable: true });
|
||||
defineValue(Navigator.prototype, 'constructor', Navigator, { writable: true, enumerable: false, configurable: true });
|
||||
markObjectToString(Window.prototype, 'Window');
|
||||
markObjectToString(Location.prototype, 'Location');
|
||||
markObjectToString(Navigator.prototype, 'Navigator');
|
||||
|
||||
const location = Object.create(Location.prototype);
|
||||
const locationState = {
|
||||
href: url.href,
|
||||
origin: url.origin,
|
||||
protocol: url.protocol,
|
||||
host: url.host,
|
||||
hostname: url.hostname,
|
||||
port: url.port,
|
||||
pathname: url.pathname,
|
||||
search: url.search,
|
||||
hash: url.hash,
|
||||
};
|
||||
for (const key of ['href', 'origin', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash']) {
|
||||
defineNativeGetter(Location.prototype, key, function () { return locationState[key]; }, { enumerable: true, configurable: true, name: key }, addon);
|
||||
}
|
||||
defineNativeValue(Location.prototype, 'toString', function toString() { return this.href; }, { enumerable: false, configurable: true, writable: true, name: 'toString', length: 0 }, addon);
|
||||
markObjectToString(location, 'Location');
|
||||
|
||||
const navigator = Object.create(Navigator.prototype);
|
||||
const navState = {
|
||||
userAgent: browser.userAgent || '',
|
||||
language: browser.language || 'zh-CN',
|
||||
languages: browser.languages || ['zh-CN', 'zh'],
|
||||
platform: browser.platform || 'Win32',
|
||||
hardwareConcurrency: browser.hardwareConcurrency ?? 8,
|
||||
deviceMemory: browser.deviceMemory ?? 8,
|
||||
webdriver: browser.webdriver ?? false,
|
||||
cookieEnabled: browser.cookieEnabled ?? true,
|
||||
};
|
||||
for (const key of Object.keys(navState)) {
|
||||
defineNativeGetter(Navigator.prototype, key, function () { return navState[key]; }, { enumerable: true, configurable: true, name: key }, addon);
|
||||
}
|
||||
markObjectToString(navigator, 'Navigator');
|
||||
|
||||
defineValue(globalObject, 'Window', Window, { writable: true, enumerable: false, configurable: true });
|
||||
defineValue(globalObject, 'Location', Location, { writable: true, enumerable: false, configurable: true });
|
||||
defineValue(globalObject, 'Navigator', Navigator, { writable: true, enumerable: false, configurable: true });
|
||||
|
||||
defineValue(globalObject, 'window', globalObject, { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(globalObject, 'self', globalObject, { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(globalObject, 'top', globalObject, { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(globalObject, 'parent', globalObject, { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(globalObject, 'location', location, { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(globalObject, 'navigator', navigator, { writable: false, enumerable: true, configurable: true });
|
||||
markObjectToString(globalObject, 'Window');
|
||||
|
||||
return { Window, Location, Navigator, location, navigator };
|
||||
}
|
||||
|
||||
module.exports = { installBaseEnv };
|
||||
@@ -0,0 +1,177 @@
|
||||
// Document 模块模板:提供最小 document / DOM 原型链,并优先用 addon.createUndetectable 处理 document.all。
|
||||
'use strict';
|
||||
|
||||
let protect = null;
|
||||
try { protect = require('./native-protect'); } catch { protect = null; }
|
||||
|
||||
function defineValue(obj, key, value, options = {}) {
|
||||
if (protect && protect.defineValue) return protect.defineValue(obj, key, value, options);
|
||||
Object.defineProperty(obj, key, {
|
||||
value,
|
||||
writable: options.writable ?? false,
|
||||
enumerable: options.enumerable ?? true,
|
||||
configurable: options.configurable ?? true,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
function markNativeFunction(fn, name) {
|
||||
if (protect && protect.markNativeFunction) return protect.markNativeFunction(fn, name);
|
||||
return fn;
|
||||
}
|
||||
|
||||
function markObjectToString(obj, tag) {
|
||||
if (protect && protect.markObjectToString) return protect.markObjectToString(obj, tag);
|
||||
try { Object.defineProperty(obj, Symbol.toStringTag, { value: tag, enumerable: false, configurable: true }); } catch {}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function defineNativeGetter(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeGetter) return protect.defineNativeGetter(obj, key, impl, options, addon);
|
||||
Object.defineProperty(obj, key, { get: impl, enumerable: options.enumerable ?? true, configurable: options.configurable ?? true });
|
||||
return impl;
|
||||
}
|
||||
|
||||
function defineNativeSetter(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeSetter) return protect.defineNativeSetter(obj, key, impl, options, addon);
|
||||
Object.defineProperty(obj, key, { set: impl, enumerable: options.enumerable ?? true, configurable: options.configurable ?? true });
|
||||
return impl;
|
||||
}
|
||||
|
||||
function defineNativeValue(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeValue) return protect.defineNativeValue(obj, key, impl, options, addon);
|
||||
return defineValue(obj, key, impl, { writable: true, ...options });
|
||||
}
|
||||
|
||||
function createNativeConstructor(name, length, impl, addon) {
|
||||
if (protect && protect.createNativeConstructor) return protect.createNativeConstructor(name, length, impl, addon);
|
||||
function Ctor(...args) { return impl.call(this, new.target ? true : false, ...args); }
|
||||
try { Object.defineProperty(Ctor, 'name', { value: name, configurable: true }); } catch {}
|
||||
return markNativeFunction(Ctor, name);
|
||||
}
|
||||
|
||||
function throwIllegalConstructor(name, addon) {
|
||||
if (protect && protect.throwIllegalConstructor) return protect.throwIllegalConstructor(name, addon);
|
||||
throw new TypeError(`Failed to construct '${name}': Illegal constructor`);
|
||||
}
|
||||
|
||||
function throwConstructorRequiresNew(name, addon) {
|
||||
if (protect && protect.throwConstructorRequiresNew) return protect.throwConstructorRequiresNew(name, addon);
|
||||
throw new TypeError(`Failed to construct '${name}': Please use the 'new' operator, this DOM object constructor cannot be called as a function.`);
|
||||
}
|
||||
|
||||
function installDocumentAll(document, addon) {
|
||||
let value;
|
||||
let exact = false;
|
||||
if (protect && typeof protect.createUndetectable === 'function') {
|
||||
value = protect.createUndetectable(function documentAll() { return undefined; }, addon);
|
||||
exact = value !== undefined;
|
||||
} else if (addon && typeof addon.createUndetectable === 'function') {
|
||||
try {
|
||||
value = addon.createUndetectable(function documentAll() { return undefined; });
|
||||
exact = true;
|
||||
} catch {}
|
||||
}
|
||||
if (!exact) value = undefined;
|
||||
Object.defineProperty(document, 'all', {
|
||||
value,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
return { exact, value };
|
||||
}
|
||||
|
||||
function installDocumentEnv(globalObject, fixture = {}, options = {}) {
|
||||
const addon = options.addon || null;
|
||||
const pageUrl = fixture.pageUrl || fixture.apiUrl || (globalObject.location && globalObject.location.href) || 'https://example.com/';
|
||||
const url = new URL(pageUrl);
|
||||
const cookieJar = new Map();
|
||||
const initialCookie = (fixture.request && fixture.request.cookie) || fixture.cookie || '';
|
||||
if (initialCookie) {
|
||||
for (const pair of String(initialCookie).split(';')) {
|
||||
const idx = pair.indexOf('=');
|
||||
if (idx > 0) cookieJar.set(pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
||||
}
|
||||
}
|
||||
|
||||
const EventTarget = createNativeConstructor('EventTarget', 0, function EventTarget(isNew) {
|
||||
if (!isNew) return throwConstructorRequiresNew('EventTarget', addon);
|
||||
}, addon);
|
||||
const Node = createNativeConstructor('Node', 0, function Node(isNew) {
|
||||
return throwIllegalConstructor('Node', addon);
|
||||
}, addon);
|
||||
const Document = createNativeConstructor('Document', 0, function Document(isNew) {
|
||||
return throwIllegalConstructor('Document', addon);
|
||||
}, addon);
|
||||
const HTMLDocument = createNativeConstructor('HTMLDocument', 0, function HTMLDocument(isNew) {
|
||||
return throwIllegalConstructor('HTMLDocument', addon);
|
||||
}, addon);
|
||||
const Element = createNativeConstructor('Element', 0, function Element(isNew) {
|
||||
return throwIllegalConstructor('Element', addon);
|
||||
}, addon);
|
||||
const HTMLElement = createNativeConstructor('HTMLElement', 0, function HTMLElement(isNew) {
|
||||
return throwIllegalConstructor('HTMLElement', addon);
|
||||
}, addon);
|
||||
const HTMLHtmlElement = createNativeConstructor('HTMLHtmlElement', 0, function HTMLHtmlElement(isNew) {
|
||||
return throwIllegalConstructor('HTMLHtmlElement', addon);
|
||||
}, addon);
|
||||
|
||||
for (const ctor of [EventTarget, Node, Document, HTMLDocument, Element, HTMLElement, HTMLHtmlElement]) {
|
||||
defineValue(ctor.prototype, 'constructor', ctor, { writable: true, enumerable: false, configurable: true });
|
||||
markObjectToString(ctor.prototype, ctor.name);
|
||||
}
|
||||
|
||||
Object.setPrototypeOf(Node.prototype, EventTarget.prototype);
|
||||
Object.setPrototypeOf(Document.prototype, Node.prototype);
|
||||
Object.setPrototypeOf(HTMLDocument.prototype, Document.prototype);
|
||||
Object.setPrototypeOf(Element.prototype, Node.prototype);
|
||||
Object.setPrototypeOf(HTMLElement.prototype, Element.prototype);
|
||||
Object.setPrototypeOf(HTMLHtmlElement.prototype, HTMLElement.prototype);
|
||||
|
||||
const document = Object.create(HTMLDocument.prototype);
|
||||
const documentElement = Object.create(HTMLHtmlElement.prototype);
|
||||
markObjectToString(document, 'HTMLDocument');
|
||||
markObjectToString(documentElement, 'HTMLHtmlElement');
|
||||
|
||||
defineNativeGetter(Document.prototype, 'URL', function () { return url.href; }, { enumerable: true, configurable: true, name: 'URL' }, addon);
|
||||
defineNativeGetter(Document.prototype, 'documentURI', function () { return url.href; }, { enumerable: true, configurable: true, name: 'documentURI' }, addon);
|
||||
defineNativeGetter(Document.prototype, 'referrer', function () { return fixture.referrer || ''; }, { enumerable: true, configurable: true, name: 'referrer' }, addon);
|
||||
defineNativeGetter(Document.prototype, 'hidden', function () { return false; }, { enumerable: true, configurable: true, name: 'hidden' }, addon);
|
||||
defineNativeGetter(Document.prototype, 'visibilityState', function () { return 'visible'; }, { enumerable: true, configurable: true, name: 'visibilityState' }, addon);
|
||||
defineNativeGetter(Document.prototype, 'documentElement', function () { return documentElement; }, { enumerable: true, configurable: true, name: 'documentElement' }, addon);
|
||||
defineNativeGetter(Document.prototype, 'defaultView', function () { return globalObject.window || globalObject; }, { enumerable: true, configurable: true, name: 'defaultView' }, addon);
|
||||
|
||||
const getCookie = function () {
|
||||
return Array.from(cookieJar.entries()).map(([k, v]) => `${k}=${v}`).join('; ');
|
||||
};
|
||||
const setCookie = function (value) {
|
||||
const first = String(value || '').split(';')[0];
|
||||
const idx = first.indexOf('=');
|
||||
if (idx > 0) cookieJar.set(first.slice(0, idx).trim(), first.slice(idx + 1).trim());
|
||||
};
|
||||
const cookieGetter = protect && protect.createNativeGetter ? protect.createNativeGetter('cookie', getCookie, addon) : getCookie;
|
||||
const cookieSetter = protect && protect.createNativeSetter ? protect.createNativeSetter('cookie', setCookie, addon) : setCookie;
|
||||
Object.defineProperty(Document.prototype, 'cookie', { get: cookieGetter, set: cookieSetter, enumerable: true, configurable: true });
|
||||
|
||||
defineNativeValue(Document.prototype, 'querySelector', function querySelector() { return null; }, { enumerable: true, configurable: true, writable: true, name: 'querySelector', length: 1 }, addon);
|
||||
defineNativeValue(Document.prototype, 'getElementById', function getElementById() { return null; }, { enumerable: true, configurable: true, writable: true, name: 'getElementById', length: 1 }, addon);
|
||||
defineNativeValue(Document.prototype, 'createElement', function createElement(tagName) {
|
||||
const el = Object.create(HTMLElement.prototype);
|
||||
defineValue(el, 'tagName', String(tagName || '').toUpperCase(), { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(el, 'nodeName', String(tagName || '').toUpperCase(), { writable: false, enumerable: true, configurable: true });
|
||||
markObjectToString(el, 'HTMLElement');
|
||||
return el;
|
||||
}, { enumerable: true, configurable: true, writable: true, name: 'createElement', length: 1 }, addon);
|
||||
|
||||
const allStatus = installDocumentAll(document, addon);
|
||||
|
||||
for (const ctor of [EventTarget, Node, Document, HTMLDocument, Element, HTMLElement, HTMLHtmlElement]) {
|
||||
defineValue(globalObject, ctor.name, ctor, { writable: true, enumerable: false, configurable: true });
|
||||
}
|
||||
defineValue(globalObject, 'document', document, { writable: false, enumerable: true, configurable: true });
|
||||
|
||||
return { document, documentElement, allStatus, EventTarget, Node, Document, HTMLDocument, Element, HTMLElement, HTMLHtmlElement };
|
||||
}
|
||||
|
||||
module.exports = { installDocumentEnv, installDocumentAll };
|
||||
@@ -0,0 +1,349 @@
|
||||
// 指纹值回放模板:用于 Canvas / WebGL / DOM 几何等终端 API 的真实浏览器采样值回放。
|
||||
// 使用前复制到 case/result/src/env/ 中,并接入 case/fixtures/fingerprint.fixture.json。
|
||||
// 目标:回放真实浏览器最终返回值,而不是在 Node.js 中真实模拟渲染。
|
||||
'use strict';
|
||||
|
||||
let native = {};
|
||||
try { native = require('./native-protect'); } catch {}
|
||||
|
||||
const defineValue = native.defineValue || function defineValue(obj, key, value, options = {}) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value,
|
||||
writable: options.writable ?? true,
|
||||
enumerable: options.enumerable ?? true,
|
||||
configurable: options.configurable ?? true,
|
||||
});
|
||||
return value;
|
||||
};
|
||||
|
||||
const defineNativeValue = native.defineNativeValue || function defineNativeValue(obj, key, impl, options = {}) {
|
||||
return defineValue(obj, key, impl, options);
|
||||
};
|
||||
|
||||
const defineNativeGetter = native.defineNativeGetter || function defineNativeGetter(obj, key, impl, options = {}) {
|
||||
Object.defineProperty(obj, key, {
|
||||
get: impl,
|
||||
enumerable: options.enumerable ?? true,
|
||||
configurable: options.configurable ?? true,
|
||||
});
|
||||
return impl;
|
||||
};
|
||||
|
||||
const markObjectToString = native.markObjectToString || function markObjectToString(obj, tag) {
|
||||
try {
|
||||
Object.defineProperty(obj, Symbol.toStringTag, {
|
||||
value: tag,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
} catch {}
|
||||
return obj;
|
||||
};
|
||||
|
||||
function stackText() {
|
||||
try { return String(new Error().stack || ''); } catch { return ''; }
|
||||
}
|
||||
|
||||
function b64ToBytes(value) {
|
||||
if (!value) return new Uint8ClampedArray();
|
||||
const buf = Buffer.from(String(value), 'base64');
|
||||
return new Uint8ClampedArray(buf.buffer, buf.byteOffset, buf.byteLength);
|
||||
}
|
||||
|
||||
function includesStack(match, stack) {
|
||||
if (!match || !match.stackIncludes) return true;
|
||||
const needles = Array.isArray(match.stackIncludes) ? match.stackIncludes : [match.stackIncludes];
|
||||
return needles.some(s => stack.includes(String(s)));
|
||||
}
|
||||
|
||||
function matchScalar(expected, actual) {
|
||||
if (expected === undefined) return true;
|
||||
return String(expected) === String(actual);
|
||||
}
|
||||
|
||||
function findReplay(list, ctx, options = {}) {
|
||||
const items = Array.isArray(list) ? list : [];
|
||||
const stack = ctx.stack || stackText();
|
||||
for (const item of items) {
|
||||
const match = item.match || {};
|
||||
let ok = includesStack(match, stack);
|
||||
for (const [key, expected] of Object.entries(match)) {
|
||||
if (key === 'stackIncludes') continue;
|
||||
ok = ok && matchScalar(expected, ctx[key]);
|
||||
}
|
||||
if (ok) return item.result;
|
||||
}
|
||||
if (options.strict !== false) {
|
||||
const brief = Object.fromEntries(Object.entries(ctx).filter(([k]) => k !== 'stack'));
|
||||
throw new Error(`缺少浏览器指纹回放样本:${ctx.api || 'unknown'} ${JSON.stringify(brief)}`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createImageData(result) {
|
||||
const width = Number(result && result.width) || 0;
|
||||
const height = Number(result && result.height) || 0;
|
||||
const data = result && result.dataBase64 ? b64ToBytes(result.dataBase64) : new Uint8ClampedArray(width * height * 4);
|
||||
return markObjectToString({ width, height, data }, 'ImageData');
|
||||
}
|
||||
|
||||
function ensureConstructors(g, options = {}) {
|
||||
const addon = options.addon;
|
||||
if (!g.HTMLCanvasElement) {
|
||||
const HTMLCanvasElement = native.createNativeConstructor ? native.createNativeConstructor('HTMLCanvasElement', 0, function HTMLCanvasElement() {}, addon) : function HTMLCanvasElement() {};
|
||||
defineValue(g, 'HTMLCanvasElement', HTMLCanvasElement, { enumerable: false, configurable: true });
|
||||
}
|
||||
if (!g.CanvasRenderingContext2D) {
|
||||
const CanvasRenderingContext2D = native.createNativeConstructor ? native.createNativeConstructor('CanvasRenderingContext2D', 0, function CanvasRenderingContext2D() {}, addon) : function CanvasRenderingContext2D() {};
|
||||
defineValue(g, 'CanvasRenderingContext2D', CanvasRenderingContext2D, { enumerable: false, configurable: true });
|
||||
}
|
||||
if (!g.WebGLRenderingContext) {
|
||||
const WebGLRenderingContext = native.createNativeConstructor ? native.createNativeConstructor('WebGLRenderingContext', 0, function WebGLRenderingContext() {}, addon) : function WebGLRenderingContext() {};
|
||||
defineValue(g, 'WebGLRenderingContext', WebGLRenderingContext, { enumerable: false, configurable: true });
|
||||
}
|
||||
if (!g.DOMRect) {
|
||||
const DOMRect = native.createNativeConstructor ? native.createNativeConstructor('DOMRect', 4, function DOMRect(isNew, x = 0, y = 0, width = 0, height = 0) {
|
||||
if (!isNew && !(this instanceof DOMRect)) return new g.DOMRect(x, y, width, height);
|
||||
defineValue(this, 'x', Number(x), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'y', Number(y), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'width', Number(width), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'height', Number(height), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'top', Number(y), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'left', Number(x), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'right', Number(x) + Number(width), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'bottom', Number(y) + Number(height), { enumerable: true, writable: false, configurable: true });
|
||||
markObjectToString(this, 'DOMRect');
|
||||
}, addon) : function DOMRect(x = 0, y = 0, width = 0, height = 0) {
|
||||
defineValue(this, 'x', Number(x), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'y', Number(y), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'width', Number(width), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'height', Number(height), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'top', Number(y), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'left', Number(x), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'right', Number(x) + Number(width), { enumerable: true, writable: false, configurable: true });
|
||||
defineValue(this, 'bottom', Number(y) + Number(height), { enumerable: true, writable: false, configurable: true });
|
||||
markObjectToString(this, 'DOMRect');
|
||||
};
|
||||
defineValue(g, 'DOMRect', DOMRect, { enumerable: false, configurable: true });
|
||||
}
|
||||
|
||||
for (const Ctor of [g.HTMLCanvasElement, g.CanvasRenderingContext2D, g.WebGLRenderingContext, g.DOMRect]) {
|
||||
try {
|
||||
defineValue(Ctor.prototype, 'constructor', Ctor, { enumerable: false, writable: true, configurable: true });
|
||||
} catch {}
|
||||
}
|
||||
try { markObjectToString(g.HTMLCanvasElement.prototype, 'HTMLCanvasElement'); } catch {}
|
||||
try { markObjectToString(g.CanvasRenderingContext2D.prototype, 'CanvasRenderingContext2D'); } catch {}
|
||||
try { markObjectToString(g.WebGLRenderingContext.prototype, 'WebGLRenderingContext'); } catch {}
|
||||
}
|
||||
|
||||
function create2DContext(g, canvas, state) {
|
||||
const ctx = Object.create(g.CanvasRenderingContext2D.prototype);
|
||||
ctx.__canvas = canvas;
|
||||
ctx.__state = state || { font: '10px sans-serif' };
|
||||
markObjectToString(ctx, 'CanvasRenderingContext2D');
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function createWebGLContext(g, canvas, state) {
|
||||
const gl = Object.create(g.WebGLRenderingContext.prototype);
|
||||
gl.__canvas = canvas;
|
||||
gl.__state = state || {};
|
||||
markObjectToString(gl, 'WebGLRenderingContext');
|
||||
return gl;
|
||||
}
|
||||
|
||||
function installCanvasReplay(g, fixture, options) {
|
||||
const canvasFx = fixture.canvas || {};
|
||||
const proto = g.HTMLCanvasElement.prototype;
|
||||
const ctx2dProto = g.CanvasRenderingContext2D.prototype;
|
||||
|
||||
defineNativeValue(proto, 'getContext', function getContext(type) {
|
||||
const kind = String(type || '').toLowerCase();
|
||||
if (kind === '2d') return this.__ctx2d || (this.__ctx2d = create2DContext(g, this, { font: '10px sans-serif' }));
|
||||
if (kind === 'webgl' || kind === 'experimental-webgl' || kind === 'webgl2') return this.__webgl || (this.__webgl = createWebGLContext(g, this, {}));
|
||||
return null;
|
||||
}, { name: 'getContext', length: 1, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'toDataURL', function toDataURL(type = 'image/png', quality) {
|
||||
const result = findReplay(canvasFx.toDataURL, {
|
||||
api: 'canvas.toDataURL',
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
type,
|
||||
quality,
|
||||
stack: stackText(),
|
||||
}, options);
|
||||
return result === undefined ? '' : String(result);
|
||||
}, { name: 'toDataURL', length: 0, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'toBlob', function toBlob(callback, type = 'image/png', quality) {
|
||||
const result = findReplay(canvasFx.toBlob, {
|
||||
api: 'canvas.toBlob',
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
type,
|
||||
quality,
|
||||
stack: stackText(),
|
||||
}, { ...options, strict: options.strictToBlob ?? options.strict });
|
||||
if (typeof callback === 'function') {
|
||||
const blob = markObjectToString({ type, size: result && result.size ? Number(result.size) : 0, __replay: result }, 'Blob');
|
||||
callback(blob);
|
||||
}
|
||||
}, { name: 'toBlob', length: 1, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeGetter(ctx2dProto, 'font', function getFont() {
|
||||
return this.__state && this.__state.font || '10px sans-serif';
|
||||
}, { name: 'font', enumerable: true, configurable: true }, options.addon);
|
||||
Object.defineProperty(ctx2dProto, 'font', {
|
||||
get: Object.getOwnPropertyDescriptor(ctx2dProto, 'font').get,
|
||||
set: function setFont(value) { this.__state.font = String(value); },
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
defineNativeValue(ctx2dProto, 'measureText', function measureText(text) {
|
||||
const result = findReplay(canvasFx.measureText, {
|
||||
api: 'canvas.measureText',
|
||||
text: String(text),
|
||||
font: this.font,
|
||||
stack: stackText(),
|
||||
}, options) || { width: 0 };
|
||||
return markObjectToString({ ...result }, 'TextMetrics');
|
||||
}, { name: 'measureText', length: 1, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(ctx2dProto, 'getImageData', function getImageData(sx, sy, sw, sh) {
|
||||
const result = findReplay(canvasFx.getImageData, {
|
||||
api: 'canvas.getImageData',
|
||||
sx, sy, sw, sh,
|
||||
stack: stackText(),
|
||||
}, options);
|
||||
return createImageData(result || { width: sw, height: sh });
|
||||
}, { name: 'getImageData', length: 4, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
for (const name of ['fillText', 'strokeText', 'drawImage', 'fillRect', 'clearRect', 'rect', 'arc', 'beginPath', 'closePath', 'stroke', 'fill', 'putImageData']) {
|
||||
if (!ctx2dProto[name]) {
|
||||
defineNativeValue(ctx2dProto, name, function noop() {}, { name, length: 0, enumerable: true, configurable: true }, options.addon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function installWebGLReplay(g, fixture, options) {
|
||||
const webglFx = fixture.webgl || {};
|
||||
const proto = g.WebGLRenderingContext.prototype;
|
||||
|
||||
defineNativeValue(proto, 'getParameter', function getParameter(pname) {
|
||||
return findReplay(webglFx.getParameter, { api: 'webgl.getParameter', pname, stack: stackText() }, options);
|
||||
}, { name: 'getParameter', length: 1, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'getSupportedExtensions', function getSupportedExtensions() {
|
||||
const configured = webglFx.getSupportedExtensions;
|
||||
if (configured && Array.isArray(configured.result)) return configured.result.slice();
|
||||
return findReplay(webglFx.getSupportedExtensions, { api: 'webgl.getSupportedExtensions', stack: stackText() }, options) || [];
|
||||
}, { name: 'getSupportedExtensions', length: 0, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'getExtension', function getExtension(name) {
|
||||
const configured = webglFx.getExtension;
|
||||
const result = findReplay(configured, { api: 'webgl.getExtension', name: String(name), stack: stackText() }, { ...options, strict: false });
|
||||
return result === undefined ? null : markObjectToString({ name: String(name), ...result }, String(name));
|
||||
}, { name: 'getExtension', length: 1, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'getShaderPrecisionFormat', function getShaderPrecisionFormat(shaderType, precisionType) {
|
||||
const result = findReplay(webglFx.getShaderPrecisionFormat, {
|
||||
api: 'webgl.getShaderPrecisionFormat',
|
||||
shaderType,
|
||||
precisionType,
|
||||
stack: stackText(),
|
||||
}, options) || { rangeMin: 0, rangeMax: 0, precision: 0 };
|
||||
return markObjectToString({ ...result }, 'WebGLShaderPrecisionFormat');
|
||||
}, { name: 'getShaderPrecisionFormat', length: 2, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'readPixels', function readPixels(x, y, width, height, format, type, pixels) {
|
||||
const result = findReplay(webglFx.readPixels, {
|
||||
api: 'webgl.readPixels',
|
||||
x, y, width, height, format, type,
|
||||
stack: stackText(),
|
||||
}, options);
|
||||
if (pixels && result && result.dataBase64) {
|
||||
const bytes = b64ToBytes(result.dataBase64);
|
||||
for (let i = 0; i < Math.min(pixels.length, bytes.length); i++) pixels[i] = bytes[i];
|
||||
}
|
||||
}, { name: 'readPixels', length: 7, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'getContextAttributes', function getContextAttributes() {
|
||||
return { alpha: true, antialias: true, depth: true, desynchronized: false, failIfMajorPerformanceCaveat: false, powerPreference: 'default', premultipliedAlpha: true, preserveDrawingBuffer: false, stencil: false, xrCompatible: false };
|
||||
}, { name: 'getContextAttributes', length: 0, enumerable: true, configurable: true }, options.addon);
|
||||
}
|
||||
|
||||
function installDomGeometryReplay(g, fixture, options) {
|
||||
const domFx = fixture.domGeometry || {};
|
||||
if (!g.Element) return;
|
||||
const proto = g.Element.prototype;
|
||||
|
||||
function selectorOf(el) {
|
||||
return el.__fingerprintSelector || el.id && `#${el.id}` || el.className && `.${String(el.className).split(/\s+/)[0]}` || String(el.tagName || '').toLowerCase();
|
||||
}
|
||||
|
||||
defineNativeValue(proto, 'getBoundingClientRect', function getBoundingClientRect() {
|
||||
const result = findReplay(domFx.getBoundingClientRect, {
|
||||
api: 'dom.getBoundingClientRect',
|
||||
selector: selectorOf(this),
|
||||
stack: stackText(),
|
||||
}, options) || {};
|
||||
return new g.DOMRect(result.x || result.left || 0, result.y || result.top || 0, result.width || 0, result.height || 0);
|
||||
}, { name: 'getBoundingClientRect', length: 0, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
defineNativeValue(proto, 'getClientRects', function getClientRects() {
|
||||
const rect = this.getBoundingClientRect();
|
||||
return markObjectToString({ 0: rect, length: 1, item(i) { return i === 0 ? rect : null; } }, 'DOMRectList');
|
||||
}, { name: 'getClientRects', length: 0, enumerable: true, configurable: true }, options.addon);
|
||||
|
||||
for (const key of ['offsetWidth', 'offsetHeight', 'scrollWidth', 'scrollHeight', 'clientWidth', 'clientHeight']) {
|
||||
defineNativeGetter(proto, key, function geometryGetter() {
|
||||
const result = findReplay(domFx.offset, {
|
||||
api: `dom.${key}`,
|
||||
selector: selectorOf(this),
|
||||
stack: stackText(),
|
||||
}, { ...options, strict: false });
|
||||
if (result && result[key] !== undefined) return Number(result[key]);
|
||||
const rect = this.getBoundingClientRect();
|
||||
return /height/i.test(key) ? Math.round(rect.height || 0) : Math.round(rect.width || 0);
|
||||
}, { name: key, enumerable: true, configurable: true }, options.addon);
|
||||
}
|
||||
}
|
||||
|
||||
function createCanvasElement(globalObj, width = 300, height = 150) {
|
||||
const g = globalObj || globalThis;
|
||||
ensureConstructors(g, options);
|
||||
const canvas = Object.create(g.HTMLCanvasElement.prototype);
|
||||
defineValue(canvas, 'width', Number(width), { enumerable: true, writable: true, configurable: true });
|
||||
defineValue(canvas, 'height', Number(height), { enumerable: true, writable: true, configurable: true });
|
||||
markObjectToString(canvas, 'HTMLCanvasElement');
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function installFingerprintValueReplay(globalObj, fixture = {}, options = {}) {
|
||||
const g = globalObj || globalThis;
|
||||
const replayOptions = { strict: true, ...options };
|
||||
ensureConstructors(g, replayOptions);
|
||||
installCanvasReplay(g, fixture, replayOptions);
|
||||
installWebGLReplay(g, fixture, replayOptions);
|
||||
installDomGeometryReplay(g, fixture, replayOptions);
|
||||
if (!g.document) return { createCanvasElement: (w, h) => createCanvasElement(g, w, h) };
|
||||
if (g.document && typeof g.document.createElement === 'function' && !g.document.__fingerprintCreateElementPatched) {
|
||||
const rawCreateElement = g.document.createElement;
|
||||
defineNativeValue(g.document, 'createElement', function createElement(tagName, optionsArg) {
|
||||
const tag = String(tagName || '').toLowerCase();
|
||||
if (tag === 'canvas') return createCanvasElement(g);
|
||||
return rawCreateElement.call(this, tagName, optionsArg);
|
||||
}, { name: 'createElement', length: 1, enumerable: true, configurable: true }, replayOptions.addon);
|
||||
defineValue(g.document, '__fingerprintCreateElementPatched', true, { enumerable: false, configurable: true });
|
||||
}
|
||||
return { createCanvasElement: (w, h) => createCanvasElement(g, w, h) };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
installFingerprintValueReplay,
|
||||
createCanvasElement,
|
||||
findReplay,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
// Storage 模块模板:按 fixture 注入 localStorage / sessionStorage,并固化原型链、描述符和 native-like 方法。
|
||||
'use strict';
|
||||
|
||||
let protect = null;
|
||||
try { protect = require('./native-protect'); } catch { protect = null; }
|
||||
|
||||
function defineValue(obj, key, value, options = {}) {
|
||||
if (protect && protect.defineValue) return protect.defineValue(obj, key, value, options);
|
||||
Object.defineProperty(obj, key, {
|
||||
value,
|
||||
writable: options.writable ?? false,
|
||||
enumerable: options.enumerable ?? true,
|
||||
configurable: options.configurable ?? true,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
function markNativeFunction(fn, name) {
|
||||
if (protect && protect.markNativeFunction) return protect.markNativeFunction(fn, name);
|
||||
return fn;
|
||||
}
|
||||
|
||||
function markObjectToString(obj, tag) {
|
||||
if (protect && protect.markObjectToString) return protect.markObjectToString(obj, tag);
|
||||
try { Object.defineProperty(obj, Symbol.toStringTag, { value: tag, enumerable: false, configurable: true }); } catch {}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function defineNativeGetter(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeGetter) return protect.defineNativeGetter(obj, key, impl, options, addon);
|
||||
Object.defineProperty(obj, key, { get: impl, enumerable: options.enumerable ?? true, configurable: options.configurable ?? true });
|
||||
return impl;
|
||||
}
|
||||
|
||||
function defineNativeValue(obj, key, impl, options = {}, addon) {
|
||||
if (protect && protect.defineNativeValue) return protect.defineNativeValue(obj, key, impl, options, addon);
|
||||
return defineValue(obj, key, impl, { writable: true, ...options });
|
||||
}
|
||||
|
||||
function createNativeConstructor(name, length, impl, addon) {
|
||||
if (protect && protect.createNativeConstructor) return protect.createNativeConstructor(name, length, impl, addon);
|
||||
function Ctor(...args) { return impl.call(this, new.target ? true : false, ...args); }
|
||||
try { Object.defineProperty(Ctor, 'name', { value: name, configurable: true }); } catch {}
|
||||
return markNativeFunction(Ctor, name);
|
||||
}
|
||||
|
||||
function throwIllegalConstructor(name, addon) {
|
||||
if (protect && protect.throwIllegalConstructor) return protect.throwIllegalConstructor(name, addon);
|
||||
throw new TypeError(`Failed to construct '${name}': Illegal constructor`);
|
||||
}
|
||||
|
||||
function installStorage(globalObject, fixture = {}, options = {}) {
|
||||
const addon = options.addon || null;
|
||||
const request = fixture.request || {};
|
||||
|
||||
const Storage = createNativeConstructor('Storage', 0, function Storage(isNew) {
|
||||
return throwIllegalConstructor('Storage', addon);
|
||||
}, addon);
|
||||
defineValue(Storage.prototype, 'constructor', Storage, { writable: true, enumerable: false, configurable: true });
|
||||
markObjectToString(Storage.prototype, 'Storage');
|
||||
|
||||
defineNativeGetter(Storage.prototype, 'length', function () { return this.__data.size; }, { enumerable: true, configurable: true, name: 'length' }, addon);
|
||||
defineNativeValue(Storage.prototype, 'key', function key(index) { return Array.from(this.__data.keys())[Number(index)] ?? null; }, { enumerable: true, configurable: true, writable: true, name: 'key', length: 1 }, addon);
|
||||
defineNativeValue(Storage.prototype, 'getItem', function getItem(key) { key = String(key); return this.__data.has(key) ? this.__data.get(key) : null; }, { enumerable: true, configurable: true, writable: true, name: 'getItem', length: 1 }, addon);
|
||||
defineNativeValue(Storage.prototype, 'setItem', function setItem(key, value) { this.__data.set(String(key), String(value)); }, { enumerable: true, configurable: true, writable: true, name: 'setItem', length: 2 }, addon);
|
||||
defineNativeValue(Storage.prototype, 'removeItem', function removeItem(key) { this.__data.delete(String(key)); }, { enumerable: true, configurable: true, writable: true, name: 'removeItem', length: 1 }, addon);
|
||||
defineNativeValue(Storage.prototype, 'clear', function clear() { this.__data.clear(); }, { enumerable: true, configurable: true, writable: true, name: 'clear', length: 0 }, addon);
|
||||
|
||||
function createStorage(initial = {}) {
|
||||
const data = new Map(Object.entries(initial).map(([k, v]) => [String(k), String(v)]));
|
||||
const storage = Object.create(Storage.prototype);
|
||||
defineValue(storage, '__data', data, { enumerable: false, configurable: false, writable: false });
|
||||
markObjectToString(storage, 'Storage');
|
||||
return storage;
|
||||
}
|
||||
|
||||
const localStorage = createStorage(request.localStorage || fixture.localStorage || {});
|
||||
const sessionStorage = createStorage(request.sessionStorage || fixture.sessionStorage || {});
|
||||
|
||||
defineValue(globalObject, 'Storage', Storage, { writable: true, enumerable: false, configurable: true });
|
||||
defineValue(globalObject, 'localStorage', localStorage, { writable: false, enumerable: true, configurable: true });
|
||||
defineValue(globalObject, 'sessionStorage', sessionStorage, { writable: false, enumerable: true, configurable: true });
|
||||
|
||||
return { Storage, localStorage, sessionStorage, createStorage };
|
||||
}
|
||||
|
||||
function createStorage(initial = {}, options = {}) {
|
||||
const context = {};
|
||||
return installStorage(context, { localStorage: initial }, options).localStorage;
|
||||
}
|
||||
|
||||
module.exports = { createStorage, installStorage };
|
||||
Reference in New Issue
Block a user