262 lines
9.1 KiB
JavaScript
262 lines
9.1 KiB
JavaScript
const { scaffoldModule } = require("./scaffold-module");
|
||
const { buildPluginGraph } = require("./build-plugin-graph");
|
||
|
||
function clone(value) {
|
||
return JSON.parse(JSON.stringify(value || {}));
|
||
}
|
||
|
||
function normalizeNavigatorSeed(seed = {}) {
|
||
const navigatorSeed = seed.navigator && typeof seed.navigator === "object" ? seed.navigator : seed;
|
||
const plugins = Array.isArray(seed.plugins) ? clone(seed.plugins) : [];
|
||
const mimeTypes = Array.isArray(seed.mimeTypes) ? clone(seed.mimeTypes) : [];
|
||
|
||
return {
|
||
navigator: {
|
||
userAgent: navigatorSeed.userAgent || "",
|
||
platform: navigatorSeed.platform || "",
|
||
vendor: navigatorSeed.vendor || "",
|
||
language: navigatorSeed.language || "",
|
||
languages: Array.isArray(navigatorSeed.languages) ? navigatorSeed.languages.slice() : [],
|
||
webdriver: typeof navigatorSeed.webdriver === "boolean" ? navigatorSeed.webdriver : false,
|
||
hardwareConcurrency: Number.isFinite(navigatorSeed.hardwareConcurrency)
|
||
? navigatorSeed.hardwareConcurrency
|
||
: 8,
|
||
deviceMemory: Number.isFinite(navigatorSeed.deviceMemory) ? navigatorSeed.deviceMemory : 8,
|
||
maxTouchPoints: Number.isFinite(navigatorSeed.maxTouchPoints) ? navigatorSeed.maxTouchPoints : 0,
|
||
cookieEnabled: navigatorSeed.cookieEnabled !== false,
|
||
pdfViewerEnabled: navigatorSeed.pdfViewerEnabled !== false,
|
||
appCodeName: navigatorSeed.appCodeName || "Mozilla",
|
||
appName: navigatorSeed.appName || "Netscape",
|
||
appVersion: navigatorSeed.appVersion || navigatorSeed.userAgent || "",
|
||
userAgentData: clone(seed.userAgentData || navigatorSeed.userAgentData || null),
|
||
connection: clone(seed.connection || navigatorSeed.connection || null),
|
||
},
|
||
plugins,
|
||
mimeTypes,
|
||
battery: clone(seed.battery || null),
|
||
};
|
||
}
|
||
|
||
function renderNavigatorPatch(seed) {
|
||
const serializedSeed = JSON.stringify(seed, null, 2);
|
||
return `(
|
||
function installNavigatorModule(globalObject) {
|
||
const seed = ${serializedSeed};
|
||
const protector = globalObject.__envNativeProtector || null;
|
||
const pluginSeed = Array.isArray(seed.plugins) ? seed.plugins.slice() : [];
|
||
const mimeSeed = Array.isArray(seed.mimeTypes) ? seed.mimeTypes.slice() : [];
|
||
|
||
function defineValue(target, key, value) {
|
||
Object.defineProperty(target, key, {
|
||
configurable: true,
|
||
enumerable: false,
|
||
writable: true,
|
||
value: value
|
||
});
|
||
}
|
||
|
||
function defineGetter(target, key, getter) {
|
||
Object.defineProperty(target, key, {
|
||
configurable: true,
|
||
enumerable: false,
|
||
get: getter
|
||
});
|
||
if (protector && protector.protectDescriptor) {
|
||
protector.protectDescriptor(target, key);
|
||
}
|
||
}
|
||
|
||
function createIndexedCollection(items, namedKey) {
|
||
const collection = {};
|
||
for (let index = 0; index < items.length; index += 1) {
|
||
Object.defineProperty(collection, index, {
|
||
configurable: true,
|
||
enumerable: false,
|
||
writable: false,
|
||
value: items[index]
|
||
});
|
||
if (items[index] && items[index][namedKey]) {
|
||
Object.defineProperty(collection, items[index][namedKey], {
|
||
configurable: true,
|
||
enumerable: false,
|
||
writable: false,
|
||
value: items[index]
|
||
});
|
||
}
|
||
}
|
||
defineValue(collection, "length", items.length);
|
||
defineValue(collection, "item", function item(index) {
|
||
return items[index] || null;
|
||
});
|
||
defineValue(collection, "namedItem", function namedItem(name) {
|
||
return items.find(function findItem(entry) {
|
||
return entry && entry[namedKey] === name;
|
||
}) || null;
|
||
});
|
||
defineValue(collection, "refresh", function refresh() {});
|
||
return collection;
|
||
}
|
||
|
||
const pluginArray = pluginSeed.map(function mapPlugin(plugin) {
|
||
return {
|
||
name: plugin.name || "",
|
||
filename: plugin.filename || "",
|
||
description: plugin.description || "",
|
||
length: 0
|
||
};
|
||
});
|
||
|
||
const mimeTypeArray = mimeSeed.map(function mapMime(mime) {
|
||
return {
|
||
type: mime.type || "",
|
||
suffixes: mime.suffixes || "",
|
||
description: mime.description || "",
|
||
enabledPlugin: null
|
||
};
|
||
});
|
||
|
||
for (let index = 0; index < mimeSeed.length; index += 1) {
|
||
const pluginName = mimeSeed[index].enabledPluginName || "";
|
||
const plugin = pluginArray.find(function findPlugin(entry) {
|
||
return entry.name === pluginName;
|
||
}) || null;
|
||
mimeTypeArray[index].enabledPlugin = plugin;
|
||
if (plugin) {
|
||
const itemIndex = plugin.length;
|
||
Object.defineProperty(plugin, itemIndex, {
|
||
configurable: true,
|
||
enumerable: false,
|
||
writable: false,
|
||
value: mimeTypeArray[index]
|
||
});
|
||
plugin.length += 1;
|
||
}
|
||
}
|
||
|
||
const plugins = createIndexedCollection(pluginArray, "name");
|
||
const mimeTypes = createIndexedCollection(mimeTypeArray, "type");
|
||
|
||
function Navigator() {}
|
||
const navigatorState = seed.navigator || {};
|
||
const navigatorInstance = Object.create(Navigator.prototype);
|
||
|
||
const scalarKeys = [
|
||
"userAgent",
|
||
"platform",
|
||
"vendor",
|
||
"language",
|
||
"webdriver",
|
||
"hardwareConcurrency",
|
||
"deviceMemory",
|
||
"maxTouchPoints",
|
||
"cookieEnabled",
|
||
"pdfViewerEnabled",
|
||
"appCodeName",
|
||
"appName",
|
||
"appVersion"
|
||
];
|
||
|
||
scalarKeys.forEach(function defineScalar(key) {
|
||
defineGetter(Navigator.prototype, key, function getScalar() {
|
||
return navigatorState[key];
|
||
});
|
||
});
|
||
|
||
defineGetter(Navigator.prototype, "languages", function getLanguages() {
|
||
return Array.isArray(navigatorState.languages) ? navigatorState.languages.slice() : [];
|
||
});
|
||
defineGetter(Navigator.prototype, "plugins", function getPlugins() {
|
||
return plugins;
|
||
});
|
||
defineGetter(Navigator.prototype, "mimeTypes", function getMimeTypes() {
|
||
return mimeTypes;
|
||
});
|
||
defineGetter(Navigator.prototype, "userAgentData", function getUserAgentData() {
|
||
return navigatorState.userAgentData || null;
|
||
});
|
||
defineGetter(Navigator.prototype, "connection", function getConnection() {
|
||
return navigatorState.connection || null;
|
||
});
|
||
|
||
defineValue(Navigator.prototype, "javaEnabled", function javaEnabled() {
|
||
return false;
|
||
});
|
||
defineValue(Navigator.prototype, "getBattery", function getBattery() {
|
||
return Promise.resolve(seed.battery || {
|
||
charging: true,
|
||
chargingTime: 0,
|
||
dischargingTime: Infinity,
|
||
level: 1
|
||
});
|
||
});
|
||
|
||
if (protector && protector.repairFunctionMeta) {
|
||
protector.repairFunctionMeta(Navigator, { name: "Navigator", length: 0 });
|
||
protector.repairFunctionMeta(Navigator.prototype.javaEnabled, { name: "javaEnabled", length: 0 });
|
||
protector.repairFunctionMeta(Navigator.prototype.getBattery, { name: "getBattery", length: 0 });
|
||
}
|
||
if (protector && protector.protectFunction) {
|
||
protector.protectFunction(Navigator, "Navigator");
|
||
protector.protectFunction(Navigator.prototype.javaEnabled, "javaEnabled");
|
||
protector.protectFunction(Navigator.prototype.getBattery, "getBattery");
|
||
}
|
||
|
||
defineValue(navigatorInstance, Symbol.toStringTag, "Navigator");
|
||
Object.defineProperty(globalObject, "Navigator", {
|
||
configurable: true,
|
||
enumerable: false,
|
||
writable: true,
|
||
value: Navigator
|
||
});
|
||
Object.defineProperty(globalObject, "navigator", {
|
||
configurable: true,
|
||
enumerable: false,
|
||
writable: true,
|
||
value: navigatorInstance
|
||
});
|
||
|
||
return navigatorInstance;
|
||
})(typeof globalThis !== "undefined" ? globalThis : window);
|
||
`;
|
||
}
|
||
|
||
function buildNavigatorModule(seed = {}) {
|
||
const module = scaffoldModule("navigator-module");
|
||
const normalizedSeed = normalizeNavigatorSeed(seed);
|
||
const pluginGraph = buildPluginGraph({
|
||
plugins: normalizedSeed.plugins,
|
||
mimeTypes: normalizedSeed.mimeTypes,
|
||
});
|
||
|
||
module.patchPlan = [
|
||
"构建 Navigator 构造器、prototype 和 navigator 实例。",
|
||
"把 userAgent/language/webdriver 等高频字段补成 prototype getter,而不是直接灌到实例上。",
|
||
"把 plugins/mimeTypes 构造成类数组图结构,并补 getBattery/userAgentData 等高频检测点。",
|
||
];
|
||
module.patchCode = renderNavigatorPatch(normalizedSeed);
|
||
module.runtimeState = {
|
||
navigator: normalizedSeed.navigator,
|
||
plugins: pluginGraph.pluginArray,
|
||
mimeTypes: pluginGraph.mimeTypeArray,
|
||
battery: normalizedSeed.battery,
|
||
};
|
||
module.validation = [
|
||
"检查 navigator.webdriver === false。",
|
||
"检查 navigator.plugins.length 与 navigator.mimeTypes.length。",
|
||
"检查 navigator.plugins.item(0) / namedItem(name) 与 mimeTypes.enabledPlugin 关系。",
|
||
"检查 navigator.getBattery() 返回 Promise。",
|
||
"检查 Navigator.prototype 上 getter 的描述符与 toString 保护是否生效。",
|
||
];
|
||
module.residualRisk = [
|
||
"这里只覆盖常见字段与图结构,未实现 permissions、mediaDevices、bluetooth 等更深层能力对象。",
|
||
"如果目标脚本会校验 brands/mobile/platformVersion 等 userAgentData 细节,仍需真实浏览器种子。",
|
||
];
|
||
|
||
return module;
|
||
}
|
||
|
||
module.exports = {
|
||
buildNavigatorModule,
|
||
normalizeNavigatorSeed,
|
||
};
|