reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user