feat(coding-agent): enhanced browser stealth and automation capabilities
- Overhauled stealth spoofing mechanisms for WebGL, screen dimensions, Web workers, and iframe contexts using prototype-aware injection. - Centralized function string representation patching to improve mimicry of native browser behavior across global objects. - Patched puppeteer-core to remove detectable evaluation markers and implement lazy, pull-style execution context management. - Enabled support for capturing LLM request JSON dumps and adjusted launcher flags to improve organic request patterns.
This commit is contained in:
@@ -324,6 +324,7 @@
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@ark/schema@0.56.0": "patches/@ark%2Fschema@0.56.0.patch",
|
||||
"puppeteer-core@25.1.0": "patches/puppeteer-core@25.1.0.patch",
|
||||
},
|
||||
"catalog": {
|
||||
"@agentclientprotocol/sdk": "0.25.0",
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.14",
|
||||
"patchedDependencies": {
|
||||
"@ark/schema@0.56.0": "patches/@ark%2Fschema@0.56.0.patch"
|
||||
"@ark/schema@0.56.0": "patches/@ark%2Fschema@0.56.0.patch",
|
||||
"puppeteer-core@25.1.0": "patches/puppeteer-core@25.1.0.patch"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added LLM request JSON export functionality to `/dump`
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved browser stealth by suppressing common automation flags and patching property descriptors
|
||||
- Enhanced stealth for `WebGL`, `Worker`, `IFrame`, `Screen`, and `Audio` APIs to evade detection
|
||||
- Updated `toString` patching to register native function sources for improved fingerprint protection
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed `/debug dump-next-request` command
|
||||
|
||||
@@ -29,10 +29,23 @@ export const DEFAULT_VIEWPORT = { width: 1365, height: 768, deviceScaleFactor: 1
|
||||
* connection dropped, etc.).
|
||||
*/
|
||||
export const BROWSER_PROTOCOL_TIMEOUT_MS = 60_000;
|
||||
// Automation-tell launch flags that puppeteer-core adds by default. We suppress
|
||||
// them via `ignoreDefaultArgs` (the supported escape hatch) to mirror xxxx's
|
||||
// chromiumSwitches patch. `--enable-automation` is the loudest: it sets
|
||||
// navigator.webdriver=true and shows the "controlled by automated software" infobar.
|
||||
// `ignoreDefaultArgs` does exact-string matching, so each entry must be a flag that
|
||||
// puppeteer emits verbatim. The default `--disable-features=...` string can't be
|
||||
// matched this way; it is neutralized in the puppeteer-core patch (ChromeLauncher).
|
||||
const STEALTH_IGNORE_DEFAULT_ARGS = [
|
||||
"--enable-automation",
|
||||
"--disable-extensions",
|
||||
"--disable-default-apps",
|
||||
"--disable-component-extensions-with-background-pages",
|
||||
"--disable-popup-blocking",
|
||||
"--disable-client-side-phishing-detection",
|
||||
"--allow-pre-commit-input",
|
||||
"--disable-ipc-flooding-protection",
|
||||
"--metrics-recording-only",
|
||||
];
|
||||
const STEALTH_ACCEPT_LANGUAGE = "en-US,en";
|
||||
|
||||
@@ -309,9 +322,11 @@ export interface UserAgentOverride {
|
||||
userAgentMetadata: {
|
||||
brands: Array<{ brand: string; version: string }>;
|
||||
fullVersion: string;
|
||||
fullVersionList: Array<{ brand: string; version: string }>;
|
||||
platform: string;
|
||||
platformVersion: string;
|
||||
architecture: string;
|
||||
bitness: string;
|
||||
model: string;
|
||||
mobile: boolean;
|
||||
};
|
||||
@@ -371,6 +386,26 @@ function patchSourceUrl(page: Page): void {
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveMacOsProductVersion(): Promise<string> {
|
||||
if (os.platform() !== "darwin") return "";
|
||||
try {
|
||||
const plist = await Bun.file("/System/Library/CoreServices/SystemVersion.plist").text();
|
||||
return plist.match(/<key>ProductVersion<\/key>\s*<string>([^<]+)<\/string>/)?.[1] ?? "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function resolveHostArchitecture(): string {
|
||||
if (os.arch() === "arm64") return "arm";
|
||||
if (os.arch().includes("64")) return "x86";
|
||||
return "";
|
||||
}
|
||||
|
||||
function resolveHostBitness(): string {
|
||||
return os.arch().includes("64") ? "64" : "";
|
||||
}
|
||||
|
||||
async function resolveUserAgentOverride(page: Page): Promise<UserAgentOverride> {
|
||||
const rawUserAgent = await page.browser().userAgent();
|
||||
let userAgent = rawUserAgent.replace("HeadlessChrome/", "Chrome/");
|
||||
@@ -379,32 +414,24 @@ async function resolveUserAgentOverride(page: Page): Promise<UserAgentOverride>
|
||||
}
|
||||
|
||||
const uaVersionMatch = userAgent.match(/Chrome\/([\d|.]+)/);
|
||||
const fallbackVersionMatch = uaVersionMatch ?? (await page.browser().version()).match(/\/([\d|.]+)/);
|
||||
const uaVersion = fallbackVersionMatch?.[1] ?? "0";
|
||||
const majorVersion = Number.parseInt(uaVersion.split(".")[0] ?? "0", 10) || 0;
|
||||
const browserVersionMatch = (await page.browser().version()).match(/\/([\d|.]+)/);
|
||||
const legacyVersion = uaVersionMatch?.[1] ?? browserVersionMatch?.[1] ?? "0";
|
||||
const fullVersion = browserVersionMatch?.[1] ?? legacyVersion;
|
||||
const majorVersion = Number.parseInt(legacyVersion.split(".")[0] ?? "0", 10) || 0;
|
||||
const isAndroid = userAgent.includes("Android");
|
||||
const platform = userAgent.includes("Mac OS X")
|
||||
? "MacIntel"
|
||||
: isAndroid
|
||||
? "Android"
|
||||
: userAgent.includes("Linux")
|
||||
? "Linux"
|
||||
: "Win32";
|
||||
const platformFull = userAgent.includes("Mac OS X")
|
||||
? "Mac OS X"
|
||||
: isAndroid
|
||||
? "Android"
|
||||
: userAgent.includes("Linux")
|
||||
? "Linux"
|
||||
: "Windows";
|
||||
const platformVersion = userAgent.includes("Mac OS X ")
|
||||
? (userAgent.match(/Mac OS X ([^)]+)/)?.[1] ?? "")
|
||||
const isMac = userAgent.includes("Mac OS X");
|
||||
const isWindows = userAgent.includes("Windows");
|
||||
const platform = isMac ? "MacIntel" : isAndroid ? "Android" : userAgent.includes("Linux") ? "Linux" : "Win32";
|
||||
const platformFull = isMac ? "macOS" : isAndroid ? "Android" : userAgent.includes("Linux") ? "Linux" : "Windows";
|
||||
const platformVersion = isMac
|
||||
? await resolveMacOsProductVersion()
|
||||
: userAgent.includes("Android ")
|
||||
? (userAgent.match(/Android ([^;]+)/)?.[1] ?? "")
|
||||
: userAgent.includes("Windows ")
|
||||
? (userAgent.match(/Windows .*?([\d|.]+);?/)?.[1] ?? "")
|
||||
: isWindows
|
||||
? (userAgent.match(/Windows NT ([\d.]+)/)?.[1] ?? "")
|
||||
: "";
|
||||
const architecture = isAndroid ? "" : "x86";
|
||||
const architecture = isAndroid ? "" : resolveHostArchitecture();
|
||||
const bitness = isAndroid ? "" : resolveHostBitness();
|
||||
const model = isAndroid ? (userAgent.match(/Android.*?;\s([^)]+)/)?.[1] ?? "") : "";
|
||||
|
||||
const brandOrders = [
|
||||
@@ -422,6 +449,10 @@ async function resolveUserAgentOverride(page: Page): Promise<UserAgentOverride>
|
||||
brands[order[0]!] = { brand: greaseyBrand, version: "99" };
|
||||
brands[order[1]!] = { brand: "Chromium", version: String(majorVersion) };
|
||||
brands[order[2]!] = { brand: "Google Chrome", version: String(majorVersion) };
|
||||
const fullVersionList = brands.map(({ brand }) => ({
|
||||
brand,
|
||||
version: brand === greaseyBrand ? "99.0.0.0" : fullVersion,
|
||||
}));
|
||||
|
||||
return {
|
||||
userAgent,
|
||||
@@ -429,10 +460,12 @@ async function resolveUserAgentOverride(page: Page): Promise<UserAgentOverride>
|
||||
acceptLanguage: STEALTH_ACCEPT_LANGUAGE,
|
||||
userAgentMetadata: {
|
||||
brands,
|
||||
fullVersion: uaVersion,
|
||||
fullVersion,
|
||||
fullVersionList,
|
||||
platform: platformFull,
|
||||
platformVersion,
|
||||
architecture,
|
||||
bitness,
|
||||
model,
|
||||
mobile: isAndroid,
|
||||
},
|
||||
@@ -578,15 +611,29 @@ function buildStealthInjectionScript(scripts: readonly string[] = STEALTH_PATCH_
|
||||
.join(";\n");
|
||||
|
||||
return `(() => {
|
||||
// Native function cache - captured before any tampering
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.style.display = "none";
|
||||
const Page_Function_toString = Function.prototype.toString;
|
||||
const Page_FunctionToStringDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, "toString");
|
||||
const Page_Proxy = Proxy;
|
||||
const Page_WeakMap = WeakMap;
|
||||
const Page_WeakMap_get = Page_WeakMap.prototype.get;
|
||||
const Page_WeakMap_set = Page_WeakMap.prototype.set;
|
||||
// Native function cache - captured before any tampering.
|
||||
// A same-origin iframe yields natives uncontaminated by page-level
|
||||
// tampering, but at document-start (when this preload runs) there is
|
||||
// no documentElement to attach it to. In that case the page itself
|
||||
// hasn't executed yet, so window's own natives are still pristine —
|
||||
// fall back to window instead of bailing, otherwise none of the
|
||||
// fingerprint patches below would ever run.
|
||||
let iframe = null;
|
||||
const container = document.head ?? document.documentElement;
|
||||
if (!container) return;
|
||||
container.appendChild(iframe);
|
||||
if (container) {
|
||||
iframe = document.createElement("iframe");
|
||||
iframe.style.display = "none";
|
||||
container.appendChild(iframe);
|
||||
if (!iframe.contentWindow) iframe = null;
|
||||
}
|
||||
try {
|
||||
const nativeWindow = iframe.contentWindow;
|
||||
if (!nativeWindow) return;
|
||||
const nativeWindow = iframe ? iframe.contentWindow : window;
|
||||
|
||||
// Cache pristine native functions
|
||||
const Function_toString = nativeWindow.Function.prototype.toString;
|
||||
@@ -626,9 +673,38 @@ function buildStealthInjectionScript(scripts: readonly string[] = STEALTH_PATCH_
|
||||
const Intl_DateTimeFormat = nativeWindow.Intl.DateTimeFormat;
|
||||
const Date_constructor = nativeWindow.Date;
|
||||
|
||||
const nativeFunctionSources = new Page_WeakMap();
|
||||
const makeNativeString = (name) => "function " + (name || "") + "() { [native code] }";
|
||||
const registerNativeSource = (fn, source) => {
|
||||
if (typeof fn === "function") Reflect_apply(Page_WeakMap_set, nativeFunctionSources, [fn, source]);
|
||||
return fn;
|
||||
};
|
||||
const patchToString = (fn, name) => registerNativeSource(fn, makeNativeString(name));
|
||||
if (${scripts.length > 0 ? "true" : "false"}) {
|
||||
const functionToStringProxy = new Page_Proxy(Page_Function_toString, {
|
||||
apply(target, thisArg, args) {
|
||||
const source = Reflect_apply(Page_WeakMap_get, nativeFunctionSources, [thisArg]);
|
||||
if (source) return source;
|
||||
return Reflect_apply(target, thisArg, args || []);
|
||||
},
|
||||
get(target, key, receiver) {
|
||||
return Reflect_get(target, key, receiver);
|
||||
},
|
||||
});
|
||||
registerNativeSource(functionToStringProxy, makeNativeString("toString"));
|
||||
Object_defineProperty(Function.prototype, "toString", {
|
||||
...(Page_FunctionToStringDescriptor || {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
}),
|
||||
value: functionToStringProxy,
|
||||
});
|
||||
}
|
||||
|
||||
${joint}
|
||||
} finally {
|
||||
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
|
||||
if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe);
|
||||
}})();`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// Helper to generate native code string
|
||||
const makeNativeString = (name) =>
|
||||
"function " + (name || "") + "() { [native code] }";
|
||||
|
||||
// Patch toString for common fingerprinted functions
|
||||
// Register native-looking source for common fingerprinted functions without
|
||||
// adding own toString properties.
|
||||
const patchedFns = [
|
||||
[window.alert, "alert"],
|
||||
[window.prompt, "prompt"],
|
||||
@@ -20,44 +17,28 @@ const patchedFns = [
|
||||
];
|
||||
|
||||
for (const [fn, name] of patchedFns) {
|
||||
if (typeof fn === "function") {
|
||||
const nativeStr = makeNativeString(name);
|
||||
Object_defineProperty(fn, "toString", {
|
||||
value: function toString() { return nativeStr; },
|
||||
writable: false,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
});
|
||||
}
|
||||
patchToString(fn, name);
|
||||
}
|
||||
|
||||
// Patch Object.getOwnPropertyDescriptor to return native-looking descriptors
|
||||
Object.getOwnPropertyDescriptor = function (obj, prop) {
|
||||
const descriptor = Object_getOwnPropertyDescriptor.call(this, obj, prop);
|
||||
// Patch Object.getOwnPropertyDescriptor to return native-looking accessor
|
||||
// source through the shared Function.prototype.toString registry.
|
||||
const patchedGetOwnPropertyDescriptor = function getOwnPropertyDescriptor(obj, prop) {
|
||||
const descriptor = Reflect_apply(Object_getOwnPropertyDescriptor, this, [obj, prop]);
|
||||
if (!descriptor) return descriptor;
|
||||
|
||||
// Make patched descriptors look native
|
||||
if (descriptor.get && typeof descriptor.get === "function") {
|
||||
const getStr = makeNativeString("get " + String(prop));
|
||||
Object_defineProperty(descriptor.get, "toString", {
|
||||
value: function toString() { return getStr; },
|
||||
writable: false,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
});
|
||||
patchToString(descriptor.get, "get " + String(prop));
|
||||
}
|
||||
if (descriptor.set && typeof descriptor.set === "function") {
|
||||
const setStr = makeNativeString("set " + String(prop));
|
||||
Object_defineProperty(descriptor.set, "toString", {
|
||||
value: function toString() { return setStr; },
|
||||
writable: false,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
});
|
||||
patchToString(descriptor.set, "set " + String(prop));
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
|
||||
// Cleanup
|
||||
document.head.removeChild(iframe);
|
||||
patchToString(patchedGetOwnPropertyDescriptor, "getOwnPropertyDescriptor");
|
||||
Object_defineProperty(Object, "getOwnPropertyDescriptor", {
|
||||
value: patchedGetOwnPropertyDescriptor,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
});
|
||||
|
||||
@@ -1,20 +1,80 @@
|
||||
const scheduleActivity = () => {
|
||||
const delay = 3000 + Math_random() * 4000;
|
||||
Window_setTimeout(() => {
|
||||
Object_defineProperty(document, "hidden", { get: () => false });
|
||||
Object_defineProperty(document, "visibilityState", {
|
||||
get: () => "visible",
|
||||
{
|
||||
const visibilityDescriptors = Object_getOwnPropertyDescriptors({
|
||||
get hidden() {
|
||||
return false;
|
||||
},
|
||||
get visibilityState() {
|
||||
return "visible";
|
||||
},
|
||||
get webkitHidden() {
|
||||
return false;
|
||||
},
|
||||
get webkitVisibilityState() {
|
||||
return "visible";
|
||||
},
|
||||
});
|
||||
for (const key of Object_keys(visibilityDescriptors)) {
|
||||
const descriptor = visibilityDescriptors[key];
|
||||
if (descriptor && typeof descriptor.get === "function") {
|
||||
patchToString(descriptor.get, "get " + key);
|
||||
}
|
||||
}
|
||||
const focusDescriptor = Object_getOwnPropertyDescriptor(
|
||||
{
|
||||
hasFocus() {
|
||||
return document.visibilityState === "visible";
|
||||
},
|
||||
},
|
||||
"hasFocus",
|
||||
);
|
||||
if (focusDescriptor && typeof focusDescriptor.value === "function") {
|
||||
patchToString(focusDescriptor.value, "hasFocus");
|
||||
}
|
||||
|
||||
const clearOwnSlot = (name) => {
|
||||
const ownDescriptor = Object_getOwnPropertyDescriptor(document, name);
|
||||
if (!ownDescriptor) return true;
|
||||
if (ownDescriptor.configurable !== true) return false;
|
||||
return Reflect_deleteProperty(document, name);
|
||||
};
|
||||
|
||||
const inheritedSlot = (name, expectedKind) => {
|
||||
let proto = Object_getPrototypeOf(document);
|
||||
while (proto) {
|
||||
const descriptor = Object_getOwnPropertyDescriptor(proto, name);
|
||||
if (descriptor && typeof descriptor[expectedKind] === "function") {
|
||||
return [proto, descriptor];
|
||||
}
|
||||
proto = Object_getPrototypeOf(proto);
|
||||
}
|
||||
};
|
||||
|
||||
const defineAccessor = (name) => {
|
||||
if (!clearOwnSlot(name)) return;
|
||||
const slot = inheritedSlot(name, "get");
|
||||
if (!slot || slot[1].configurable !== true) return;
|
||||
|
||||
Object_defineProperty(slot[0], name, {
|
||||
get: visibilityDescriptors[name].get,
|
||||
enumerable: slot[1].enumerable,
|
||||
configurable: true,
|
||||
});
|
||||
Object_defineProperty(document, "webkitVisibilityState", {
|
||||
get: () => "visible",
|
||||
});
|
||||
document.dispatchEvent(new Window_Event("visibilitychange"));
|
||||
if (Math_random() < 0.4) window.dispatchEvent(new Window_Event("focus"));
|
||||
document.hasFocus = () => true;
|
||||
if (Math_random() < 0.3) window.dispatchEvent(new Window_Event("scroll"));
|
||||
if (navigator.wakeLock)
|
||||
navigator.wakeLock.request("screen").catch(() => {});
|
||||
scheduleActivity();
|
||||
}, delay);
|
||||
};
|
||||
scheduleActivity();
|
||||
};
|
||||
|
||||
defineAccessor("hidden");
|
||||
defineAccessor("visibilityState");
|
||||
defineAccessor("webkitHidden");
|
||||
defineAccessor("webkitVisibilityState");
|
||||
|
||||
if (clearOwnSlot("hasFocus")) {
|
||||
const slot = inheritedSlot("hasFocus", "value");
|
||||
if (slot && slot[1].configurable === true) {
|
||||
Object_defineProperty(slot[0], "hasFocus", {
|
||||
value: focusDescriptor.value,
|
||||
writable: slot[1].writable === true,
|
||||
enumerable: slot[1].enumerable,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,57 @@
|
||||
const elementDescriptor = Object_getOwnPropertyDescriptor(
|
||||
HTMLElement.prototype,
|
||||
"offsetHeight",
|
||||
);
|
||||
Object_defineProperty(HTMLDivElement.prototype, "offsetHeight", {
|
||||
...elementDescriptor,
|
||||
get: function () {
|
||||
if (this.id === "modernizr") return 1;
|
||||
return Reflect_apply(elementDescriptor.get, this, []);
|
||||
},
|
||||
});
|
||||
{
|
||||
const htmlElementOffsetHeightDescriptor =
|
||||
typeof HTMLElement === "undefined"
|
||||
? undefined
|
||||
: Object_getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
|
||||
const htmlDivElementOffsetHeightDescriptor =
|
||||
typeof HTMLDivElement === "undefined"
|
||||
? undefined
|
||||
: Object_getOwnPropertyDescriptor(HTMLDivElement.prototype, "offsetHeight");
|
||||
const offsetHeightDescriptor =
|
||||
htmlDivElementOffsetHeightDescriptor || htmlElementOffsetHeightDescriptor;
|
||||
const offsetHeightPrototype = htmlDivElementOffsetHeightDescriptor
|
||||
? HTMLDivElement.prototype
|
||||
: htmlElementOffsetHeightDescriptor
|
||||
? HTMLElement.prototype
|
||||
: undefined;
|
||||
const elementIdDescriptor =
|
||||
typeof Element === "undefined"
|
||||
? undefined
|
||||
: Object_getOwnPropertyDescriptor(Element.prototype, "id");
|
||||
|
||||
if (
|
||||
typeof HTMLDivElement !== "undefined" &&
|
||||
offsetHeightPrototype &&
|
||||
offsetHeightDescriptor &&
|
||||
typeof offsetHeightDescriptor.get === "function" &&
|
||||
offsetHeightDescriptor.configurable &&
|
||||
elementIdDescriptor &&
|
||||
typeof elementIdDescriptor.get === "function"
|
||||
) {
|
||||
const offsetHeightGetter = new Window_Proxy(offsetHeightDescriptor.get, {
|
||||
apply(target, thisArg, args) {
|
||||
const height = Reflect_apply(target, thisArg, args);
|
||||
|
||||
if (
|
||||
height === 0 &&
|
||||
Object_getPrototypeOf(thisArg) === HTMLDivElement.prototype &&
|
||||
Reflect_apply(elementIdDescriptor.get, thisArg, []) === "modernizr"
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return height;
|
||||
},
|
||||
});
|
||||
|
||||
patchToString(offsetHeightGetter, "get offsetHeight");
|
||||
|
||||
Object_defineProperty(
|
||||
offsetHeightPrototype,
|
||||
"offsetHeight",
|
||||
Object_assign({}, offsetHeightDescriptor, {
|
||||
get: offsetHeightGetter,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
const makeNativeString = (name) => "function " + (name || "") + "() { [native code] }";
|
||||
const patchToString = (fn, name) => {
|
||||
if (typeof fn !== "function") return;
|
||||
Object_defineProperty(fn, "toString", {
|
||||
value: function toString() {
|
||||
return makeNativeString(name);
|
||||
},
|
||||
writable: false,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
});
|
||||
};
|
||||
|
||||
// Ensure navigator.webdriver behaves like real Chrome
|
||||
if (navigator.webdriver !== false && navigator.webdriver !== undefined) {
|
||||
@@ -356,13 +344,19 @@ if (window.chrome && !("runtime" in window.chrome) && isSecureOrigin) {
|
||||
// Suppress Permission.query for automation-controlled
|
||||
if (navigator.permissions?.query) {
|
||||
if (isSecureOrigin && "Notification" in window) {
|
||||
const notificationPermissionGetter = Object_getOwnPropertyDescriptor({
|
||||
get permission() {
|
||||
return "default";
|
||||
},
|
||||
}, "permission").get;
|
||||
patchToString(notificationPermissionGetter, "get permission");
|
||||
Object_defineProperty(Notification, "permission", {
|
||||
get: () => "default",
|
||||
get: notificationPermissionGetter,
|
||||
configurable: true,
|
||||
});
|
||||
} else if (!isSecureOrigin) {
|
||||
const originalQuery = navigator.permissions.query;
|
||||
navigator.permissions.query = function (parameters) {
|
||||
const patchedPermissionsQuery = function query(parameters) {
|
||||
if (parameters?.name === "notifications") {
|
||||
const status = { state: "denied", onchange: null };
|
||||
if (typeof PermissionStatus !== "undefined") {
|
||||
@@ -372,6 +366,8 @@ if (navigator.permissions?.query) {
|
||||
}
|
||||
return originalQuery.call(this, parameters);
|
||||
};
|
||||
patchToString(patchedPermissionsQuery, "query");
|
||||
navigator.permissions.query = patchedPermissionsQuery;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,81 +1,174 @@
|
||||
const addContentWindowProxy = (iframe) => {
|
||||
const contentWindowProxy = {
|
||||
get(target, key) {
|
||||
if (key === "self") return this;
|
||||
if (key === "frameElement") return iframe;
|
||||
if (key === "0") return undefined;
|
||||
return Reflect_get(target, key);
|
||||
},
|
||||
};
|
||||
const iframeWindowProxies = new WeakMap();
|
||||
|
||||
if (!iframe.contentWindow) {
|
||||
const proxy = new Window_Proxy(window, contentWindowProxy);
|
||||
Object_defineProperty(iframe, "contentWindow", {
|
||||
get() {
|
||||
return proxy;
|
||||
},
|
||||
set(newValue) {
|
||||
return newValue;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
});
|
||||
const isFrameIndexKey = (key) => {
|
||||
if (typeof key !== "string" || key === "") return false;
|
||||
const number = +key;
|
||||
return number >= 0 && number < 4294967295 && Math_floor(number) === number && `${number}` === key;
|
||||
};
|
||||
|
||||
const getPropertyDescriptor = (object, key) => {
|
||||
let current = object;
|
||||
while (current) {
|
||||
const descriptor = Object_getOwnPropertyDescriptor(current, key);
|
||||
if (descriptor) return descriptor;
|
||||
current = Object_getPrototypeOf(current);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIframeCreation = (target, thisArg, args) => {
|
||||
const iframe = Reflect_apply(target, thisArg, args);
|
||||
const originalIframe = iframe;
|
||||
const originalSrcdoc = originalIframe.srcdoc;
|
||||
const descriptorWithValue = (target, key, value, writable) => {
|
||||
const descriptor = Reflect_getOwnPropertyDescriptor(target, key);
|
||||
if (descriptor && descriptor.configurable === false) return descriptor;
|
||||
const next = descriptor ? Object_assign({}, descriptor) : { configurable: true, enumerable: true };
|
||||
Reflect_deleteProperty(next, "get");
|
||||
Reflect_deleteProperty(next, "set");
|
||||
next.value = value;
|
||||
if (!Reflect_has(next, "writable")) next.writable = writable;
|
||||
return next;
|
||||
};
|
||||
const iframeContentWindowDescriptor =
|
||||
typeof HTMLIFrameElement === "undefined"
|
||||
? undefined
|
||||
: getPropertyDescriptor(HTMLIFrameElement.prototype, "contentWindow");
|
||||
const iframeSrcdocDescriptor =
|
||||
typeof HTMLIFrameElement === "undefined"
|
||||
? undefined
|
||||
: getPropertyDescriptor(HTMLIFrameElement.prototype, "srcdoc");
|
||||
|
||||
Object_defineProperty(iframe, "srcdoc", {
|
||||
configurable: true,
|
||||
get() {
|
||||
return originalSrcdoc;
|
||||
},
|
||||
set(newValue) {
|
||||
addContentWindowProxy(this);
|
||||
Object_defineProperty(iframe, "srcdoc", {
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: originalSrcdoc,
|
||||
});
|
||||
originalIframe.srcdoc = newValue;
|
||||
},
|
||||
});
|
||||
|
||||
return iframe;
|
||||
const getNativeContentWindow = (iframe) => {
|
||||
if (iframeContentWindowDescriptor && iframeContentWindowDescriptor.get) {
|
||||
return Reflect_apply(iframeContentWindowDescriptor.get, iframe, []);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const addIframeCreationSniffer = () => {
|
||||
const originalCreateElement = document.createElement;
|
||||
const handler = {
|
||||
apply(target, thisArg, args) {
|
||||
const isIframe = args && args.length && `${args[0]}`.toLowerCase() === "iframe";
|
||||
if (!isIframe) {
|
||||
return Reflect_apply(target, thisArg, args);
|
||||
}
|
||||
return handleIframeCreation(target, thisArg, args);
|
||||
},
|
||||
const addContentWindowProxy = (iframe) => {
|
||||
let state = Reflect_apply(Page_WeakMap_get, iframeWindowProxies, [iframe]);
|
||||
if (state) return state.proxy;
|
||||
|
||||
state = { proxy: undefined, target: undefined, wasConnected: false, setProxyTarget: undefined };
|
||||
|
||||
const isDiscarded = () => {
|
||||
if (iframe.isConnected) state.wasConnected = true;
|
||||
return state.wasConnected && !iframe.isConnected;
|
||||
};
|
||||
|
||||
const currentFrameElement = () => {
|
||||
if (iframe.isConnected) {
|
||||
state.wasConnected = true;
|
||||
return iframe;
|
||||
}
|
||||
return state.wasConnected ? null : iframe;
|
||||
};
|
||||
|
||||
const contentWindowProxy = {
|
||||
get(target, key) {
|
||||
if (key === "self" || key === "window" || key === "frames" || key === "globalThis") return state.proxy;
|
||||
if (key === "frameElement") return currentFrameElement();
|
||||
if (key === "closed" && isDiscarded()) return true;
|
||||
if (isFrameIndexKey(key)) return undefined;
|
||||
if (key === "length") return 0;
|
||||
return Reflect_get(target, key);
|
||||
},
|
||||
getOwnPropertyDescriptor(target, key) {
|
||||
if (key === "self" || key === "window" || key === "frames" || key === "globalThis") {
|
||||
return descriptorWithValue(target, key, state.proxy, true);
|
||||
}
|
||||
if (key === "frameElement") {
|
||||
return descriptorWithValue(target, key, currentFrameElement(), false);
|
||||
}
|
||||
if (key === "closed" && isDiscarded()) {
|
||||
return descriptorWithValue(target, key, true, false);
|
||||
}
|
||||
if (isFrameIndexKey(key)) return undefined;
|
||||
if (key === "length") return descriptorWithValue(target, key, 0, false);
|
||||
return Reflect_getOwnPropertyDescriptor(target, key);
|
||||
},
|
||||
has(target, key) {
|
||||
if (key === "self" || key === "window" || key === "frames" || key === "globalThis" || key === "frameElement") return true;
|
||||
if (isFrameIndexKey(key)) return false;
|
||||
return Reflect_has(target, key);
|
||||
},
|
||||
ownKeys(target) {
|
||||
const keys = Reflect_ownKeys(target);
|
||||
const filtered = [];
|
||||
for (let index = 0; index < keys.length; index++) {
|
||||
if (!isFrameIndexKey(keys[index])) filtered.push(keys[index]);
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
};
|
||||
|
||||
const proxied = new Window_Proxy(originalCreateElement, handler);
|
||||
Object_defineProperty(document, "createElement", {
|
||||
value: proxied,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
Object_defineProperty(document.createElement, "toString", {
|
||||
value: Function_toString.bind(originalCreateElement),
|
||||
writable: false,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
});
|
||||
};
|
||||
const setProxyTarget = (target) => {
|
||||
if (state.target === target && state.proxy) return state.proxy;
|
||||
state.target = target;
|
||||
state.proxy = new Window_Proxy(target, contentWindowProxy);
|
||||
return state.proxy;
|
||||
};
|
||||
state.setProxyTarget = setProxyTarget;
|
||||
const initialContentWindow = getNativeContentWindow(iframe);
|
||||
if (initialContentWindow) setProxyTarget(initialContentWindow);
|
||||
if (iframe.isConnected) state.wasConnected = true;
|
||||
Reflect_apply(Page_WeakMap_set, iframeWindowProxies, [iframe, state]);
|
||||
|
||||
try {
|
||||
addIframeCreationSniffer();
|
||||
} catch {}
|
||||
|
||||
return state.proxy;
|
||||
};
|
||||
if (
|
||||
iframeContentWindowDescriptor &&
|
||||
iframeContentWindowDescriptor.get &&
|
||||
iframeContentWindowDescriptor.configurable !== false
|
||||
) {
|
||||
const contentWindowAccessors = {
|
||||
get contentWindow() {
|
||||
const state = Reflect_apply(Page_WeakMap_get, iframeWindowProxies, [this]);
|
||||
if (!state) return getNativeContentWindow(this);
|
||||
if (this.isConnected) state.wasConnected = true;
|
||||
if (state.wasConnected && !this.isConnected) return null;
|
||||
const nativeContentWindow = getNativeContentWindow(this);
|
||||
if (!nativeContentWindow) return nativeContentWindow;
|
||||
return state.setProxyTarget(nativeContentWindow);
|
||||
},
|
||||
};
|
||||
const contentWindowGetter = Object_getOwnPropertyDescriptor(contentWindowAccessors, "contentWindow").get;
|
||||
patchToString(contentWindowGetter, "get contentWindow");
|
||||
Object_defineProperty(HTMLIFrameElement.prototype, "contentWindow", {
|
||||
get: contentWindowGetter,
|
||||
set: iframeContentWindowDescriptor.set,
|
||||
enumerable: iframeContentWindowDescriptor.enumerable,
|
||||
configurable: iframeContentWindowDescriptor.configurable,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
iframeSrcdocDescriptor &&
|
||||
iframeSrcdocDescriptor.configurable !== false
|
||||
) {
|
||||
const srcdocAccessors = {
|
||||
get srcdoc() {
|
||||
if (iframeSrcdocDescriptor.get) {
|
||||
return Reflect_apply(iframeSrcdocDescriptor.get, this, []);
|
||||
}
|
||||
const value = this.getAttribute("srcdoc");
|
||||
return value === null ? "" : value;
|
||||
},
|
||||
set srcdoc(newValue) {
|
||||
addContentWindowProxy(this);
|
||||
if (iframeSrcdocDescriptor.set) {
|
||||
return Reflect_apply(iframeSrcdocDescriptor.set, this, [newValue]);
|
||||
}
|
||||
return this.setAttribute("srcdoc", newValue);
|
||||
},
|
||||
};
|
||||
const srcdocDescriptor = Object_getOwnPropertyDescriptor(srcdocAccessors, "srcdoc");
|
||||
const srcdocGetter = srcdocDescriptor.get;
|
||||
const srcdocSetter = srcdocDescriptor.set;
|
||||
patchToString(srcdocGetter, "get srcdoc");
|
||||
patchToString(srcdocSetter, "set srcdoc");
|
||||
Object_defineProperty(HTMLIFrameElement.prototype, "srcdoc", {
|
||||
get: srcdocGetter,
|
||||
set: srcdocSetter,
|
||||
enumerable: iframeSrcdocDescriptor.enumerable,
|
||||
configurable: iframeSrcdocDescriptor.configurable,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,75 +1,233 @@
|
||||
const vendors = ["Intel Inc.", "NVIDIA Corporation", "AMD"];
|
||||
const renderers = [
|
||||
"Intel(R) Iris(TM) Plus Graphics 640",
|
||||
"Intel(R) HD Graphics 630",
|
||||
"NVIDIA GeForce GTX 1050 Ti",
|
||||
"NVIDIA GeForce GTX 1060",
|
||||
"NVIDIA GeForce RTX 3060",
|
||||
"AMD Radeon RX 580",
|
||||
"AMD Radeon Pro 560",
|
||||
];
|
||||
const vendor = vendors[Math_floor(Math_random() * vendors.length)];
|
||||
const renderer = renderers[Math_floor(Math_random() * renderers.length)];
|
||||
const webglPlatformSource =
|
||||
String(navigator.userAgentData?.platform || navigator.platform || "") +
|
||||
" " +
|
||||
String(navigator.userAgent || "");
|
||||
const webglPlatformText = webglPlatformSource.toLowerCase();
|
||||
const webglPlatformKind = webglPlatformText.includes("android")
|
||||
? "android"
|
||||
: webglPlatformText.includes("iphone") ||
|
||||
webglPlatformText.includes("ipad") ||
|
||||
webglPlatformText.includes("ipod")
|
||||
? "ios"
|
||||
: webglPlatformText.includes("mac")
|
||||
? "mac"
|
||||
: webglPlatformText.includes("win")
|
||||
? "windows"
|
||||
: webglPlatformText.includes("cros")
|
||||
? "cros"
|
||||
: "linux";
|
||||
|
||||
const getParameterProxyHandler = {
|
||||
apply(target, thisArg, args) {
|
||||
const param = args[0];
|
||||
// VENDOR = 0x1F00
|
||||
if (param === 0x1F00) return vendor;
|
||||
// RENDERER = 0x1F01
|
||||
if (param === 0x1F01) return renderer;
|
||||
// UNMASKED_VENDOR_WEBGL = 0x9245
|
||||
if (param === 0x9245) return vendor;
|
||||
// UNMASKED_RENDERER_WEBGL = 0x9246
|
||||
if (param === 0x9246) return renderer;
|
||||
return Reflect_apply(target, thisArg, args);
|
||||
},
|
||||
const webglFallbackProfiles = {
|
||||
android: {
|
||||
vendor: "Qualcomm",
|
||||
renderer: "Adreno (TM) 640",
|
||||
},
|
||||
ios: {
|
||||
vendor: "Apple Inc.",
|
||||
renderer: "Apple GPU",
|
||||
},
|
||||
mac: {
|
||||
vendor: "Google Inc. (Intel Inc.)",
|
||||
renderer: "ANGLE (Intel Inc., Intel(R) Iris(TM) Plus Graphics 640 OpenGL Engine, OpenGL 4.1)",
|
||||
},
|
||||
windows: {
|
||||
vendor: "Google Inc. (Intel)",
|
||||
renderer: "ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
},
|
||||
cros: {
|
||||
vendor: "Google Inc. (Intel)",
|
||||
renderer: "ANGLE (Intel, Mesa Intel(R) UHD Graphics 620 (KBL GT2), OpenGL 4.6)",
|
||||
},
|
||||
linux: {
|
||||
vendor: "Google Inc. (Intel)",
|
||||
renderer: "ANGLE (Intel, Mesa Intel(R) UHD Graphics 620 (KBL GT2), OpenGL 4.6)",
|
||||
},
|
||||
};
|
||||
const webglFallbackProfile = webglFallbackProfiles[webglPlatformKind] || webglFallbackProfiles.linux;
|
||||
const webglContextProfiles = new WeakMap();
|
||||
const webglIsObjectKey = (value) =>
|
||||
(typeof value === "object" && value !== null) || typeof value === "function";
|
||||
|
||||
const webglLooksSoftware = (value) => {
|
||||
const text = String(value || "").toLowerCase();
|
||||
return (
|
||||
text.includes("swiftshader") ||
|
||||
text.includes("llvmpipe") ||
|
||||
text.includes("lavapipe") ||
|
||||
text.includes("software") ||
|
||||
text.includes("mesa offscreen") ||
|
||||
text.includes("google inc. (google)")
|
||||
);
|
||||
};
|
||||
|
||||
// Hook WebGL contexts
|
||||
const hookWebGL = (proto) => {
|
||||
const originalGetParameter = proto.getParameter;
|
||||
Object_defineProperty(proto, "getParameter", {
|
||||
value: new Window_Proxy(originalGetParameter, getParameterProxyHandler),
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
const webglMatchesPlatform = (renderer) => {
|
||||
const text = String(renderer || "").toLowerCase();
|
||||
if (webglPlatformKind === "windows") {
|
||||
return !text.includes("apple") && !text.includes("mesa") && !text.includes("opengl engine");
|
||||
}
|
||||
if (webglPlatformKind === "mac") {
|
||||
return !text.includes("direct3d") && !text.includes("d3d") && !text.includes("mesa");
|
||||
}
|
||||
if (webglPlatformKind === "android") {
|
||||
return (
|
||||
text.includes("adreno") ||
|
||||
text.includes("mali") ||
|
||||
text.includes("powervr") ||
|
||||
text.includes("qualcomm")
|
||||
);
|
||||
}
|
||||
if (webglPlatformKind === "ios") {
|
||||
return text.includes("apple");
|
||||
}
|
||||
return !text.includes("direct3d") && !text.includes("d3d") && !text.includes("apple");
|
||||
};
|
||||
|
||||
const webglGetContextProfile = (target, thisArg) => {
|
||||
if (!webglIsObjectKey(thisArg)) return webglFallbackProfile;
|
||||
const cached = Reflect_apply(Page_WeakMap_get, webglContextProfiles, [thisArg]);
|
||||
if (cached) return cached;
|
||||
|
||||
let nativeVendor;
|
||||
let nativeRenderer;
|
||||
try {
|
||||
nativeVendor = Reflect_apply(target, thisArg, [0x9245]);
|
||||
nativeRenderer = Reflect_apply(target, thisArg, [0x9246]);
|
||||
} catch {}
|
||||
|
||||
const nativeProfile =
|
||||
typeof nativeVendor === "string" &&
|
||||
typeof nativeRenderer === "string" &&
|
||||
nativeVendor &&
|
||||
nativeRenderer &&
|
||||
!webglLooksSoftware(nativeVendor) &&
|
||||
!webglLooksSoftware(nativeRenderer) &&
|
||||
webglMatchesPlatform(nativeRenderer)
|
||||
? { vendor: nativeVendor, renderer: nativeRenderer }
|
||||
: webglFallbackProfile;
|
||||
|
||||
Reflect_apply(Page_WeakMap_set, webglContextProfiles, [thisArg, nativeProfile]);
|
||||
return nativeProfile;
|
||||
};
|
||||
|
||||
const webglGetParameterHandler = {
|
||||
apply(target, thisArg, args) {
|
||||
const nativeValue = Reflect_apply(target, thisArg, args);
|
||||
const param = args[0];
|
||||
if (param === 0x1f00 && typeof nativeValue === "string") return "WebKit";
|
||||
if (param === 0x1f01 && typeof nativeValue === "string") return "WebKit WebGL";
|
||||
if ((param === 0x9245 || param === 0x9246) && typeof nativeValue === "string") {
|
||||
const profile = webglGetContextProfile(target, thisArg);
|
||||
return param === 0x9245 ? profile.vendor : profile.renderer;
|
||||
}
|
||||
return nativeValue;
|
||||
},
|
||||
};
|
||||
|
||||
const webglFloatPrecisionTypes = {
|
||||
0x8df0: true,
|
||||
0x8df1: true,
|
||||
0x8df2: true,
|
||||
};
|
||||
|
||||
const webglClonePrecisionFormat = (result, values) => {
|
||||
const ownKeys = Reflect_ownKeys(result);
|
||||
let hasRangeMin = false;
|
||||
let hasRangeMax = false;
|
||||
let hasPrecision = false;
|
||||
for (let index = 0; index < ownKeys.length; index += 1) {
|
||||
if (ownKeys[index] === "rangeMin") hasRangeMin = true;
|
||||
if (ownKeys[index] === "rangeMax") hasRangeMax = true;
|
||||
if (ownKeys[index] === "precision") hasPrecision = true;
|
||||
}
|
||||
if (!hasRangeMin || !hasRangeMax || !hasPrecision) return result;
|
||||
|
||||
const clone = Object_create(Object_getPrototypeOf(result));
|
||||
for (let index = 0; index < ownKeys.length; index += 1) {
|
||||
const key = ownKeys[index];
|
||||
const descriptor = Object_getOwnPropertyDescriptor(result, key);
|
||||
if (!descriptor) return result;
|
||||
if (key === "rangeMin" || key === "rangeMax" || key === "precision") {
|
||||
if (!("value" in descriptor)) return result;
|
||||
descriptor.value = values[key];
|
||||
}
|
||||
try {
|
||||
Object_defineProperty(clone, key, descriptor);
|
||||
} catch {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return clone;
|
||||
};
|
||||
|
||||
const webglGetShaderPrecisionFormatHandler = {
|
||||
apply(target, thisArg, args) {
|
||||
const result = Reflect_apply(target, thisArg, args);
|
||||
const precisionType = args[1];
|
||||
if (
|
||||
!result ||
|
||||
webglPlatformKind === "android" ||
|
||||
webglPlatformKind === "ios" ||
|
||||
!webglFloatPrecisionTypes[precisionType]
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const rangeMin = result.rangeMin;
|
||||
const rangeMax = result.rangeMax;
|
||||
const precision = result.precision;
|
||||
if (
|
||||
typeof rangeMin !== "number" ||
|
||||
typeof rangeMax !== "number" ||
|
||||
typeof precision !== "number"
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const values = {
|
||||
rangeMin: Math_max(rangeMin, 127),
|
||||
rangeMax: Math_max(rangeMax, 127),
|
||||
precision: Math_max(precision, 23),
|
||||
};
|
||||
if (
|
||||
values.rangeMin === rangeMin &&
|
||||
values.rangeMax === rangeMax &&
|
||||
values.precision === precision
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
return webglClonePrecisionFormat(result, values);
|
||||
},
|
||||
};
|
||||
|
||||
const webglInstallMethodProxy = (proto, name, handler) => {
|
||||
if (!proto) return;
|
||||
const descriptor = Object_getOwnPropertyDescriptor(proto, name);
|
||||
if (!descriptor || typeof descriptor.value !== "function") return;
|
||||
const proxy = new Window_Proxy(descriptor.value, handler);
|
||||
patchToString(proxy, name);
|
||||
try {
|
||||
Object_defineProperty(proto, name, {
|
||||
value: proxy,
|
||||
writable: descriptor.writable,
|
||||
configurable: descriptor.configurable,
|
||||
enumerable: descriptor.enumerable,
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
|
||||
if (window.WebGLRenderingContext) {
|
||||
hookWebGL(WebGLRenderingContext.prototype);
|
||||
webglInstallMethodProxy(WebGLRenderingContext.prototype, "getParameter", webglGetParameterHandler);
|
||||
webglInstallMethodProxy(
|
||||
WebGLRenderingContext.prototype,
|
||||
"getShaderPrecisionFormat",
|
||||
webglGetShaderPrecisionFormatHandler,
|
||||
);
|
||||
}
|
||||
if (window.WebGL2RenderingContext) {
|
||||
hookWebGL(WebGL2RenderingContext.prototype);
|
||||
}
|
||||
|
||||
// Also mask getShaderPrecisionFormat for software rendering detection
|
||||
const precisionMask = (proto) => {
|
||||
const original = proto.getShaderPrecisionFormat;
|
||||
if (!original) return;
|
||||
Object_defineProperty(proto, "getShaderPrecisionFormat", {
|
||||
value: function (shaderType, precisionType) {
|
||||
const result = original.call(this, shaderType, precisionType);
|
||||
if (result) {
|
||||
// Hardware typically has higher precision than SwiftShader defaults
|
||||
return {
|
||||
precision: Math_max(result.precision, 23),
|
||||
rangeMin: Math_min(result.rangeMin, 127),
|
||||
rangeMax: Math_max(result.rangeMax, 127),
|
||||
};
|
||||
}
|
||||
return result;
|
||||
},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
};
|
||||
|
||||
if (window.WebGLRenderingContext) {
|
||||
precisionMask(WebGLRenderingContext.prototype);
|
||||
}
|
||||
if (window.WebGL2RenderingContext) {
|
||||
precisionMask(WebGL2RenderingContext.prototype);
|
||||
webglInstallMethodProxy(WebGL2RenderingContext.prototype, "getParameter", webglGetParameterHandler);
|
||||
webglInstallMethodProxy(
|
||||
WebGL2RenderingContext.prototype,
|
||||
"getShaderPrecisionFormat",
|
||||
webglGetShaderPrecisionFormatHandler,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,72 +1,260 @@
|
||||
// Generate consistent "real" screen dimensions based on viewport
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
const availWidth = width;
|
||||
const availHeight = Math_max(height - 40, 0); // Account for taskbar
|
||||
const colorDepth = 24;
|
||||
const pixelDepth = 24;
|
||||
const devicePixelRatio = window.devicePixelRatio && window.devicePixelRatio > 1 ? window.devicePixelRatio : 1.25;
|
||||
;(() => {
|
||||
if (typeof Window_Proxy !== "function" || typeof Reflect_apply !== "function") return;
|
||||
|
||||
const defineScreenProp = (prop, value) => {
|
||||
try {
|
||||
Object_defineProperty(window.Screen?.prototype ?? window.screen, prop, {
|
||||
get: () => value,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
const screenObject = window.screen;
|
||||
if (!screenObject) return;
|
||||
|
||||
// Override screen properties
|
||||
for (const [prop, descriptor] of Object_entries({
|
||||
width,
|
||||
height,
|
||||
availWidth,
|
||||
availHeight,
|
||||
availLeft: 0,
|
||||
availTop: 0,
|
||||
colorDepth,
|
||||
pixelDepth,
|
||||
})) {
|
||||
defineScreenProp(prop, descriptor);
|
||||
}
|
||||
const isFiniteNumber = (value) =>
|
||||
typeof value === "number" &&
|
||||
value === value &&
|
||||
value !== Infinity &&
|
||||
value !== -Infinity;
|
||||
|
||||
// Ensure outer dimensions match screen for consistency
|
||||
const chromeFrameHeight = 85;
|
||||
Object_defineProperty(window, "outerWidth", {
|
||||
get: () => window.innerWidth,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
Object_defineProperty(window, "outerHeight", {
|
||||
get: () => window.innerHeight + chromeFrameHeight,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
if (window.visualViewport) {
|
||||
const defineVvpProp = (prop, value) => {
|
||||
const readValue = (object, prop, fallback) => {
|
||||
try {
|
||||
Object_defineProperty(window.visualViewport, prop, {
|
||||
get: () => value,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
} catch {}
|
||||
const value = object[prop];
|
||||
return value === undefined ? fallback : value;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
defineVvpProp("width", width);
|
||||
defineVvpProp("height", height);
|
||||
defineVvpProp("scale", 1);
|
||||
defineVvpProp("offsetLeft", 0);
|
||||
defineVvpProp("offsetTop", 0);
|
||||
defineVvpProp("pageLeft", 0);
|
||||
defineVvpProp("pageTop", 0);
|
||||
}
|
||||
const finiteNumber = (value, fallback) =>
|
||||
isFiniteNumber(value) ? value : fallback;
|
||||
|
||||
// Consistent devicePixelRatio
|
||||
Object_defineProperty(window, "devicePixelRatio", {
|
||||
get: () => devicePixelRatio,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
const positiveNumber = (value, fallback) =>
|
||||
isFiniteNumber(value) && value > 0 ? value : fallback;
|
||||
|
||||
const integerNumber = (value, fallback) =>
|
||||
isFiniteNumber(value) ? Math_floor(value) : fallback;
|
||||
|
||||
const positiveInteger = (value, fallback) =>
|
||||
isFiniteNumber(value) && value > 0 ? Math_floor(value) : fallback;
|
||||
|
||||
const findDescriptorOwner = (object, prop) => {
|
||||
let owner = object;
|
||||
while (owner) {
|
||||
let descriptor;
|
||||
try {
|
||||
descriptor = Object_getOwnPropertyDescriptor(owner, prop);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (descriptor) return [owner, descriptor];
|
||||
try {
|
||||
owner = Object_getPrototypeOf(owner);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const patchGetter = (object, prop, getValue) => {
|
||||
const found = findDescriptorOwner(object, prop);
|
||||
if (!found) return false;
|
||||
|
||||
const owner = found[0];
|
||||
const descriptor = found[1];
|
||||
if (descriptor.configurable !== true || typeof descriptor.get !== "function") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const originalGet = descriptor.get;
|
||||
const patchedGet = new Window_Proxy(originalGet, {
|
||||
apply(target, thisArg, args) {
|
||||
const nativeValue = Reflect_apply(target, thisArg, args);
|
||||
return getValue(nativeValue, thisArg);
|
||||
},
|
||||
});
|
||||
patchToString(patchedGet, "get " + prop);
|
||||
try {
|
||||
Object_defineProperty(owner, prop, {
|
||||
configurable: descriptor.configurable,
|
||||
enumerable: descriptor.enumerable,
|
||||
get: patchedGet,
|
||||
set: descriptor.set,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const patchStableNumber = (object, prop, value) => {
|
||||
const current = readValue(object, prop, undefined);
|
||||
if (current === value) return;
|
||||
patchGetter(object, prop, () => value);
|
||||
};
|
||||
|
||||
const initialInnerWidth = positiveInteger(readValue(window, "innerWidth", 0), 0);
|
||||
const initialInnerHeight = positiveInteger(readValue(window, "innerHeight", 0), 0);
|
||||
const initialOuterWidth = positiveInteger(
|
||||
readValue(window, "outerWidth", Math_max(initialInnerWidth, 1)),
|
||||
Math_max(initialInnerWidth, 1),
|
||||
);
|
||||
const initialOuterHeight = positiveInteger(
|
||||
readValue(window, "outerHeight", Math_max(initialInnerHeight, 1)),
|
||||
Math_max(initialInnerHeight, 1),
|
||||
);
|
||||
// Real Chrome reserves vertical space for the tab strip + URL bar, so
|
||||
// outerHeight is always taller than innerHeight (~88px on a stock window).
|
||||
// Headless reports outerHeight === innerHeight, which is a well-known tell, so
|
||||
// synthesize a realistic chrome height when the window has no visible chrome.
|
||||
const browserChromeHeight = 88;
|
||||
const targetOuterHeight =
|
||||
initialOuterHeight > initialInnerHeight
|
||||
? initialOuterHeight
|
||||
: initialInnerHeight + browserChromeHeight;
|
||||
const requiredWidth = Math_max(initialInnerWidth, initialOuterWidth, 1);
|
||||
const requiredHeight = Math_max(initialInnerHeight, initialOuterHeight, targetOuterHeight, 1);
|
||||
|
||||
const screenWidth = Math_max(
|
||||
positiveInteger(readValue(screenObject, "width", requiredWidth), requiredWidth),
|
||||
requiredWidth,
|
||||
);
|
||||
const screenHeight = Math_max(
|
||||
positiveInteger(readValue(screenObject, "height", requiredHeight), requiredHeight),
|
||||
requiredHeight,
|
||||
);
|
||||
|
||||
let screenAvailWidth = positiveInteger(
|
||||
readValue(screenObject, "availWidth", screenWidth),
|
||||
screenWidth,
|
||||
);
|
||||
if (screenAvailWidth < requiredWidth || screenAvailWidth > screenWidth) {
|
||||
screenAvailWidth = screenWidth;
|
||||
}
|
||||
|
||||
let screenAvailHeight = positiveInteger(
|
||||
readValue(screenObject, "availHeight", screenHeight),
|
||||
screenHeight,
|
||||
);
|
||||
if (screenAvailHeight < requiredHeight || screenAvailHeight > screenHeight) {
|
||||
screenAvailHeight = screenHeight;
|
||||
}
|
||||
|
||||
const screenAvailLeft = integerNumber(readValue(screenObject, "availLeft", 0), 0);
|
||||
const screenAvailTop = integerNumber(readValue(screenObject, "availTop", 0), 0);
|
||||
const screenColorDepth = positiveInteger(readValue(screenObject, "colorDepth", 24), 24);
|
||||
const screenPixelDepth = positiveInteger(
|
||||
readValue(screenObject, "pixelDepth", screenColorDepth),
|
||||
screenColorDepth,
|
||||
);
|
||||
|
||||
for (const entry of [
|
||||
["width", screenWidth],
|
||||
["height", screenHeight],
|
||||
["availWidth", screenAvailWidth],
|
||||
["availHeight", screenAvailHeight],
|
||||
["availLeft", screenAvailLeft],
|
||||
["availTop", screenAvailTop],
|
||||
["colorDepth", screenColorDepth],
|
||||
["pixelDepth", screenPixelDepth],
|
||||
]) {
|
||||
patchStableNumber(screenObject, entry[0], entry[1]);
|
||||
}
|
||||
|
||||
const outerWidthNeedsPatch =
|
||||
!isFiniteNumber(readValue(window, "outerWidth", undefined)) ||
|
||||
readValue(window, "outerWidth", 0) <= 0 ||
|
||||
Math_floor(readValue(window, "outerWidth", 0)) < initialInnerWidth;
|
||||
if (outerWidthNeedsPatch) {
|
||||
patchGetter(window, "outerWidth", (nativeValue) =>
|
||||
Math_max(
|
||||
positiveInteger(nativeValue, initialOuterWidth),
|
||||
positiveInteger(readValue(window, "innerWidth", requiredWidth), requiredWidth),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const outerHeightNeedsPatch =
|
||||
!isFiniteNumber(readValue(window, "outerHeight", undefined)) ||
|
||||
readValue(window, "outerHeight", 0) <= 0 ||
|
||||
Math_floor(readValue(window, "outerHeight", 0)) <= initialInnerHeight;
|
||||
if (outerHeightNeedsPatch) {
|
||||
patchGetter(window, "outerHeight", (nativeValue) =>
|
||||
Math_max(
|
||||
positiveInteger(nativeValue, initialOuterHeight),
|
||||
positiveInteger(readValue(window, "innerHeight", requiredHeight), requiredHeight) +
|
||||
browserChromeHeight,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const initialDevicePixelRatio = positiveNumber(
|
||||
readValue(window, "devicePixelRatio", 1),
|
||||
1,
|
||||
);
|
||||
if (readValue(window, "devicePixelRatio", undefined) !== initialDevicePixelRatio) {
|
||||
patchGetter(window, "devicePixelRatio", () => initialDevicePixelRatio);
|
||||
}
|
||||
|
||||
const visualViewportObject = readValue(window, "visualViewport", null);
|
||||
if (visualViewportObject) {
|
||||
const initialScale = positiveNumber(readValue(visualViewportObject, "scale", 1), 1);
|
||||
if (readValue(visualViewportObject, "scale", undefined) !== initialScale) {
|
||||
patchGetter(visualViewportObject, "scale", (nativeValue) =>
|
||||
positiveNumber(nativeValue, initialScale),
|
||||
);
|
||||
}
|
||||
|
||||
const getViewportScale = () =>
|
||||
positiveNumber(readValue(visualViewportObject, "scale", initialScale), initialScale);
|
||||
|
||||
const getViewportWidthFallback = () =>
|
||||
positiveNumber(readValue(window, "innerWidth", requiredWidth), requiredWidth) /
|
||||
getViewportScale();
|
||||
|
||||
const getViewportHeightFallback = () =>
|
||||
positiveNumber(readValue(window, "innerHeight", requiredHeight), requiredHeight) /
|
||||
getViewportScale();
|
||||
|
||||
if (positiveNumber(readValue(visualViewportObject, "width", 0), 0) <= 0) {
|
||||
patchGetter(visualViewportObject, "width", (nativeValue) =>
|
||||
positiveNumber(nativeValue, getViewportWidthFallback()),
|
||||
);
|
||||
}
|
||||
|
||||
if (positiveNumber(readValue(visualViewportObject, "height", 0), 0) <= 0) {
|
||||
patchGetter(visualViewportObject, "height", (nativeValue) =>
|
||||
positiveNumber(nativeValue, getViewportHeightFallback()),
|
||||
);
|
||||
}
|
||||
|
||||
const getViewportOffsetLeft = () =>
|
||||
finiteNumber(readValue(visualViewportObject, "offsetLeft", 0), 0);
|
||||
const getViewportOffsetTop = () =>
|
||||
finiteNumber(readValue(visualViewportObject, "offsetTop", 0), 0);
|
||||
|
||||
if (!isFiniteNumber(readValue(visualViewportObject, "offsetLeft", undefined))) {
|
||||
patchGetter(visualViewportObject, "offsetLeft", (nativeValue) =>
|
||||
finiteNumber(nativeValue, 0),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isFiniteNumber(readValue(visualViewportObject, "offsetTop", undefined))) {
|
||||
patchGetter(visualViewportObject, "offsetTop", (nativeValue) =>
|
||||
finiteNumber(nativeValue, 0),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isFiniteNumber(readValue(visualViewportObject, "pageLeft", undefined))) {
|
||||
patchGetter(visualViewportObject, "pageLeft", (nativeValue) =>
|
||||
finiteNumber(
|
||||
nativeValue,
|
||||
finiteNumber(readValue(window, "scrollX", 0), 0) + getViewportOffsetLeft(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isFiniteNumber(readValue(visualViewportObject, "pageTop", undefined))) {
|
||||
patchGetter(visualViewportObject, "pageTop", (nativeValue) =>
|
||||
finiteNumber(
|
||||
nativeValue,
|
||||
finiteNumber(readValue(window, "scrollY", 0), 0) + getViewportOffsetTop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -47,7 +47,6 @@ const commonFonts = [
|
||||
"Wingdings 3",
|
||||
"Apple Color Emoji",
|
||||
"Apple SD Gothic Neo",
|
||||
"Helvetica Neue",
|
||||
"Hoefler Text",
|
||||
"Menlo",
|
||||
"Monaco",
|
||||
@@ -56,32 +55,31 @@ const commonFonts = [
|
||||
"SF Pro Text",
|
||||
];
|
||||
|
||||
// Override queryLocalFonts if present (Local Font Access API)
|
||||
if ("queryLocalFonts" in window) {
|
||||
const queryLocalFonts = async function queryLocalFonts() {
|
||||
return commonFonts.map((family) => ({
|
||||
family,
|
||||
fullName: family,
|
||||
postscriptName: family.replace(/\s+/g, ""),
|
||||
style: "Regular",
|
||||
blob: () => Promise_resolve(new Window_Blob([])),
|
||||
}));
|
||||
};
|
||||
patchToString(queryLocalFonts, "queryLocalFonts");
|
||||
Object_defineProperty(window, "queryLocalFonts", {
|
||||
value: async () => {
|
||||
return commonFonts.map((family) => ({
|
||||
family,
|
||||
fullName: family,
|
||||
postscriptName: family.replace(/\s+/g, ""),
|
||||
style: "Regular",
|
||||
blob: () => Promise_resolve(new Window_Blob([])),
|
||||
}));
|
||||
},
|
||||
value: queryLocalFonts,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Hide fonts-unique tracking via canvas
|
||||
const originalGetContext = HTMLCanvasElement.prototype.getContext;
|
||||
HTMLCanvasElement.prototype.getContext = function (type, options) {
|
||||
const patchedGetContext = function getContext(type, options) {
|
||||
const ctx = originalGetContext.call(this, type, options);
|
||||
if (ctx && type === "2d") {
|
||||
const originalFillText = ctx.fillText;
|
||||
ctx.fillText = function (text, x, y, maxWidth) {
|
||||
// Add tiny imperceptible noise to text rendering
|
||||
const patchedFillText = function fillText(text, x, y, maxWidth) {
|
||||
const noiseX = (Math_random() - 0.5) * 0.02;
|
||||
const noiseY = (Math_random() - 0.5) * 0.02;
|
||||
return originalFillText.call(
|
||||
@@ -92,6 +90,10 @@ HTMLCanvasElement.prototype.getContext = function (type, options) {
|
||||
maxWidth,
|
||||
);
|
||||
};
|
||||
patchToString(patchedFillText, "fillText");
|
||||
ctx.fillText = patchedFillText;
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
patchToString(patchedGetContext, "getContext");
|
||||
HTMLCanvasElement.prototype.getContext = patchedGetContext;
|
||||
@@ -1,32 +1,45 @@
|
||||
// Spoof AudioContext latency values to look like real hardware
|
||||
const spoofLatency = (proto) => {
|
||||
Object_defineProperty(proto, "baseLatency", {
|
||||
get: () => 0.005, // ~5ms typical for real hardware
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
Object_defineProperty(proto, "outputLatency", {
|
||||
get: () => 0.01, // ~10ms typical
|
||||
// Spoof AudioContext latency values to look like real hardware.
|
||||
const audioLatencyAccessors = {
|
||||
get baseLatency() {
|
||||
return 0.005;
|
||||
},
|
||||
get outputLatency() {
|
||||
return 0.01;
|
||||
},
|
||||
get sampleRate() {
|
||||
return 48000;
|
||||
},
|
||||
};
|
||||
|
||||
const defineAudioGetter = (proto, name) => {
|
||||
const descriptor = Object_getOwnPropertyDescriptor(audioLatencyAccessors, name);
|
||||
if (!descriptor || !descriptor.get) return;
|
||||
patchToString(descriptor.get, "get " + name);
|
||||
Object_defineProperty(proto, name, {
|
||||
get: descriptor.get,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
};
|
||||
|
||||
const spoofLatency = (proto) => {
|
||||
defineAudioGetter(proto, "baseLatency");
|
||||
defineAudioGetter(proto, "outputLatency");
|
||||
};
|
||||
|
||||
if (window.AudioContext) {
|
||||
spoofLatency(AudioContext.prototype);
|
||||
}
|
||||
|
||||
if (window.OfflineAudioContext) {
|
||||
// For offline context, add subtle randomness to prevent deterministic fingerprints
|
||||
const OriginalOfflineAudioContext = window.OfflineAudioContext;
|
||||
window.OfflineAudioContext = class extends OriginalOfflineAudioContext {
|
||||
const PatchedOfflineAudioContext = class OfflineAudioContext extends OriginalOfflineAudioContext {
|
||||
constructor(numberOfChannels, length, sampleRate) {
|
||||
super(numberOfChannels, length, sampleRate);
|
||||
|
||||
// Hook startRendering to add noise
|
||||
const originalStartRendering = this.startRendering.bind(this);
|
||||
this.startRendering = async () => {
|
||||
const patchedStartRendering = async function startRendering() {
|
||||
const buffer = await originalStartRendering();
|
||||
// Add imperceptible noise to prevent deterministic hash
|
||||
for (let c = 0; c < buffer.numberOfChannels; c++) {
|
||||
const channel = buffer.getChannelData(c);
|
||||
for (let i = 0; i < channel.length; i++) {
|
||||
@@ -37,15 +50,14 @@ if (window.OfflineAudioContext) {
|
||||
}
|
||||
return buffer;
|
||||
};
|
||||
patchToString(patchedStartRendering, "startRendering");
|
||||
this.startRendering = patchedStartRendering;
|
||||
}
|
||||
};
|
||||
patchToString(PatchedOfflineAudioContext, "OfflineAudioContext");
|
||||
window.OfflineAudioContext = PatchedOfflineAudioContext;
|
||||
}
|
||||
|
||||
// Also spoof sampleRate consistency
|
||||
if (window.AudioContext) {
|
||||
Object_defineProperty(AudioContext.prototype, "sampleRate", {
|
||||
get: () => 48000, // Common hardware rate
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
defineAudioGetter(AudioContext.prototype, "sampleRate");
|
||||
}
|
||||
|
||||
@@ -1,46 +1,51 @@
|
||||
// Define a consistent locale profile
|
||||
const locale = "en-US";
|
||||
const languages = ["en-US", "en"];
|
||||
const timezone = "America/New_York";
|
||||
const languages = [locale, "en"];
|
||||
|
||||
// Override navigator language properties
|
||||
Object_defineProperty(navigator, "language", {
|
||||
get: () => locale,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
Object_defineProperty(navigator, "languages", {
|
||||
get: () => [...languages],
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
// Override Intl.DateTimeFormat for timezone consistency
|
||||
const OriginalDateTimeFormat = Intl_DateTimeFormat;
|
||||
Intl.DateTimeFormat = class extends OriginalDateTimeFormat {
|
||||
constructor(locales, options) {
|
||||
const mergedOptions = { ...options, timeZone: timezone };
|
||||
super(locales, mergedOptions);
|
||||
const sameLanguages = (value) => {
|
||||
if (!value || value.length !== languages.length) return false;
|
||||
for (let index = 0; index < languages.length; index += 1) {
|
||||
if (value[index] !== languages[index]) return false;
|
||||
}
|
||||
resolvedOptions() {
|
||||
const options = super.resolvedOptions();
|
||||
options.timeZone = timezone;
|
||||
return options;
|
||||
return true;
|
||||
};
|
||||
|
||||
const navigatorProto = Object_getPrototypeOf(navigator);
|
||||
const navigatorAccessors = {
|
||||
get language() {
|
||||
return locale;
|
||||
},
|
||||
get languages() {
|
||||
return [locale, "en"];
|
||||
},
|
||||
};
|
||||
|
||||
const defineNavigatorAccessor = (name) => {
|
||||
const owner = navigatorProto || navigator;
|
||||
const descriptor =
|
||||
(navigatorProto && Object_getOwnPropertyDescriptor(navigatorProto, name)) ||
|
||||
Object_getOwnPropertyDescriptor(navigator, name);
|
||||
if (descriptor && descriptor.configurable === false) return;
|
||||
|
||||
const accessor = Object_getOwnPropertyDescriptor(navigatorAccessors, name);
|
||||
if (!accessor || !accessor.get) return;
|
||||
patchToString(accessor.get, "get " + name);
|
||||
|
||||
Object_defineProperty(owner, name, {
|
||||
get: accessor.get,
|
||||
configurable: descriptor ? descriptor.configurable : true,
|
||||
enumerable: descriptor ? descriptor.enumerable : true,
|
||||
});
|
||||
|
||||
const ownDescriptor = Object_getOwnPropertyDescriptor(navigator, name);
|
||||
if (owner !== navigator && ownDescriptor && ownDescriptor.configurable !== false) {
|
||||
Reflect_deleteProperty(navigator, name);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure Date timezone is consistent
|
||||
const originalDateConstructor = Date_constructor;
|
||||
const originalToString = originalDateConstructor.prototype.toString;
|
||||
const originalToTimeString = originalDateConstructor.prototype.toTimeString;
|
||||
if (navigator.language !== locale) {
|
||||
defineNavigatorAccessor("language");
|
||||
}
|
||||
|
||||
Date.prototype.toString = function () {
|
||||
return originalToString
|
||||
.call(this)
|
||||
.replace(/\(.*\)$/, "(Eastern Standard Time)");
|
||||
};
|
||||
Date.prototype.toTimeString = function () {
|
||||
return originalToTimeString
|
||||
.call(this)
|
||||
.replace(/\(.*\)$/, "(Eastern Standard Time)");
|
||||
};
|
||||
if (!sameLanguages(navigator.languages)) {
|
||||
defineNavigatorAccessor("languages");
|
||||
}
|
||||
|
||||
@@ -54,8 +54,8 @@ const defineProp = (obj, prop, value) =>
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const generateFunctionMocks = (proto, itemMainProp, dataArray) => ({
|
||||
item: new Window_Proxy(proto.item, {
|
||||
const generateFunctionMocks = (proto, itemMainProp, dataArray) => {
|
||||
const item = new Window_Proxy(proto.item, {
|
||||
apply(target, ctx, args) {
|
||||
if (!args.length) {
|
||||
throw new TypeError(
|
||||
@@ -65,8 +65,8 @@ const generateFunctionMocks = (proto, itemMainProp, dataArray) => ({
|
||||
const isInteger = args[0] && Number.isInteger(Number(args[0]));
|
||||
return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null;
|
||||
},
|
||||
}),
|
||||
namedItem: new Window_Proxy(proto.namedItem, {
|
||||
});
|
||||
const namedItem = new Window_Proxy(proto.namedItem, {
|
||||
apply(target, ctx, args) {
|
||||
if (!args.length) {
|
||||
throw new TypeError(
|
||||
@@ -75,15 +75,19 @@ const generateFunctionMocks = (proto, itemMainProp, dataArray) => ({
|
||||
}
|
||||
return dataArray.find(item => item[itemMainProp] === args[0]) || null;
|
||||
},
|
||||
}),
|
||||
refresh: proto.refresh
|
||||
});
|
||||
patchToString(item, "item");
|
||||
patchToString(namedItem, "namedItem");
|
||||
const refresh = proto.refresh
|
||||
? new Window_Proxy(proto.refresh, {
|
||||
apply() {
|
||||
return undefined;
|
||||
},
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
: undefined;
|
||||
patchToString(refresh, "refresh");
|
||||
return { item, namedItem, refresh };
|
||||
};
|
||||
|
||||
const generateMagicArray = (dataArray, proto, itemProto, itemMainProp) => {
|
||||
const makeItem = (data) => {
|
||||
|
||||
@@ -1,8 +1,59 @@
|
||||
const navigatorProto = Object_getPrototypeOf(navigator);
|
||||
if (navigatorProto && "hardwareConcurrency" in navigatorProto) {
|
||||
Object_defineProperty(navigatorProto, "hardwareConcurrency", {
|
||||
get: () => 4,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
const hardwareConcurrencyName = "hardwareConcurrency";
|
||||
let hardwareConcurrencyProto = Object_getPrototypeOf(navigator);
|
||||
let hardwareConcurrencyOwner;
|
||||
let hardwareConcurrencyDescriptor;
|
||||
|
||||
while (hardwareConcurrencyProto && !hardwareConcurrencyDescriptor) {
|
||||
hardwareConcurrencyDescriptor = Object_getOwnPropertyDescriptor(
|
||||
hardwareConcurrencyProto,
|
||||
hardwareConcurrencyName,
|
||||
);
|
||||
if (hardwareConcurrencyDescriptor) {
|
||||
hardwareConcurrencyOwner = hardwareConcurrencyProto;
|
||||
} else {
|
||||
hardwareConcurrencyProto = Object_getPrototypeOf(hardwareConcurrencyProto);
|
||||
}
|
||||
}
|
||||
|
||||
const hardwareConcurrencyValue = 8;
|
||||
let shouldPatchHardwareConcurrency = false;
|
||||
|
||||
if (
|
||||
hardwareConcurrencyOwner &&
|
||||
hardwareConcurrencyDescriptor &&
|
||||
hardwareConcurrencyDescriptor.configurable &&
|
||||
typeof hardwareConcurrencyDescriptor.get === "function"
|
||||
) {
|
||||
shouldPatchHardwareConcurrency = true;
|
||||
try {
|
||||
shouldPatchHardwareConcurrency =
|
||||
Reflect_apply(hardwareConcurrencyDescriptor.get, navigator, []) !==
|
||||
hardwareConcurrencyValue;
|
||||
} catch {
|
||||
shouldPatchHardwareConcurrency = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldPatchHardwareConcurrency) {
|
||||
const hardwareConcurrencyAccessors = {
|
||||
get hardwareConcurrency() {
|
||||
Reflect_apply(hardwareConcurrencyDescriptor.get, this, []);
|
||||
return hardwareConcurrencyValue;
|
||||
},
|
||||
};
|
||||
const getHardwareConcurrency = Object_getOwnPropertyDescriptor(
|
||||
hardwareConcurrencyAccessors,
|
||||
hardwareConcurrencyName,
|
||||
).get;
|
||||
|
||||
if (typeof patchToString === "function") {
|
||||
patchToString(getHardwareConcurrency, "get hardwareConcurrency");
|
||||
}
|
||||
|
||||
Object_defineProperty(hardwareConcurrencyOwner, hardwareConcurrencyName, {
|
||||
get: getHardwareConcurrency,
|
||||
set: hardwareConcurrencyDescriptor.set,
|
||||
enumerable: hardwareConcurrencyDescriptor.enumerable,
|
||||
configurable: hardwareConcurrencyDescriptor.configurable,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,25 +15,27 @@ const parseInput = (arg) => {
|
||||
};
|
||||
|
||||
const originalCanPlayType = HTMLMediaElement.prototype.canPlayType;
|
||||
const proxiedCanPlayType = new Window_Proxy(originalCanPlayType, {
|
||||
apply(target, ctx, args) {
|
||||
if (!args || !args.length) {
|
||||
return Reflect_apply(target, ctx, args);
|
||||
}
|
||||
const { mime, codecs } = parseInput(args[0]);
|
||||
if (mime === "video/mp4" && codecs.includes("avc1.42E01E")) {
|
||||
return "probably";
|
||||
}
|
||||
if (mime === "audio/x-m4a" && !codecs.length) {
|
||||
return "maybe";
|
||||
}
|
||||
if (mime === "audio/aac" && !codecs.length) {
|
||||
return "probably";
|
||||
}
|
||||
return Reflect_apply(target, ctx, args);
|
||||
},
|
||||
});
|
||||
patchToString(proxiedCanPlayType, "canPlayType");
|
||||
Object_defineProperty(HTMLMediaElement.prototype, "canPlayType", {
|
||||
value: new Window_Proxy(originalCanPlayType, {
|
||||
apply(target, ctx, args) {
|
||||
if (!args || !args.length) {
|
||||
return Reflect_apply(target, ctx, args);
|
||||
}
|
||||
const { mime, codecs } = parseInput(args[0]);
|
||||
if (mime === "video/mp4" && codecs.includes("avc1.42E01E")) {
|
||||
return "probably";
|
||||
}
|
||||
if (mime === "audio/x-m4a" && !codecs.length) {
|
||||
return "maybe";
|
||||
}
|
||||
if (mime === "audio/aac" && !codecs.length) {
|
||||
return "probably";
|
||||
}
|
||||
return Reflect_apply(target, ctx, args);
|
||||
},
|
||||
}),
|
||||
value: proxiedCanPlayType,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
|
||||
@@ -1,52 +1,214 @@
|
||||
const patchWorkerConstructor = (name, OriginalWorker) => {
|
||||
if (typeof OriginalWorker !== "function") return;
|
||||
|
||||
const buildWrappedUrl = (scriptURL, options) => {
|
||||
const ua = navigator.userAgent;
|
||||
const platform = navigator.platform;
|
||||
const uaData = navigator.userAgentData && typeof navigator.userAgentData.toJSON === "function"
|
||||
? navigator.userAgentData.toJSON()
|
||||
: navigator.userAgentData;
|
||||
const windowDescriptor = Object_getOwnPropertyDescriptor(window, name);
|
||||
if (windowDescriptor && windowDescriptor.configurable === false) return;
|
||||
|
||||
const preludeLines = [
|
||||
"try {",
|
||||
`const ua = ${JSON.stringify(ua)};`,
|
||||
`const platform = ${JSON.stringify(platform)};`,
|
||||
"Object_defineProperty(self.navigator, 'userAgent', { get: () => ua, configurable: true });",
|
||||
"Object_defineProperty(self.navigator, 'platform', { get: () => platform, configurable: true });",
|
||||
];
|
||||
const NativeURL = window.URL;
|
||||
const URL_createObjectURL = NativeURL && NativeURL.createObjectURL;
|
||||
const URL_revokeObjectURL = NativeURL && NativeURL.revokeObjectURL;
|
||||
if (
|
||||
typeof NativeURL !== "function" ||
|
||||
typeof URL_createObjectURL !== "function" ||
|
||||
typeof URL_revokeObjectURL !== "function"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (uaData) {
|
||||
preludeLines.push(`const uaData = ${JSON.stringify(uaData)};`);
|
||||
preludeLines.push(
|
||||
"Object_defineProperty(self.navigator, 'userAgentData', { get: () => uaData, configurable: true });",
|
||||
);
|
||||
const sharedWorkerUrls = name === "SharedWorker" ? new Map() : undefined;
|
||||
|
||||
const revokeUrl = (url) => {
|
||||
try {
|
||||
Reflect_apply(URL_revokeObjectURL, NativeURL, [url]);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const scheduleWorkerUrlRevoke = (worker, url) => {
|
||||
let revoked = false;
|
||||
const revokeOnce = () => {
|
||||
if (revoked) return;
|
||||
revoked = true;
|
||||
revokeUrl(url);
|
||||
};
|
||||
try {
|
||||
if (typeof worker.addEventListener === "function") {
|
||||
Reflect_apply(worker.addEventListener, worker, ["error", revokeOnce, { once: true }]);
|
||||
}
|
||||
} catch {}
|
||||
Window_setTimeout(revokeOnce, 1000);
|
||||
};
|
||||
|
||||
if (sharedWorkerUrls) {
|
||||
try {
|
||||
window.addEventListener("pagehide", (event) => {
|
||||
if (event && event.persisted) return;
|
||||
for (const url of sharedWorkerUrls.values()) {
|
||||
revokeUrl(url);
|
||||
}
|
||||
sharedWorkerUrls.clear();
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const canWrapArguments = (args) => {
|
||||
if (!args || args.length !== 1) {
|
||||
return name === "SharedWorker" && args && args.length === 2 && typeof args[1] === "string";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const resolveWorkerUrl = (scriptURL) => {
|
||||
const baseUrl = document.baseURI || window.location.href;
|
||||
const scriptUrlString =
|
||||
typeof scriptURL === "string"
|
||||
? scriptURL
|
||||
: scriptURL instanceof NativeURL
|
||||
? scriptURL.href
|
||||
: undefined;
|
||||
if (scriptUrlString === undefined) return undefined;
|
||||
let absoluteUrl;
|
||||
try {
|
||||
absoluteUrl = new NativeURL(scriptUrlString, baseUrl);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (absoluteUrl.origin !== window.location.origin) return undefined;
|
||||
if (absoluteUrl.protocol !== "http:" && absoluteUrl.protocol !== "https:") return undefined;
|
||||
if (absoluteUrl.username || absoluteUrl.password) return undefined;
|
||||
return absoluteUrl.href;
|
||||
};
|
||||
|
||||
const buildWorkerPrelude = () => {
|
||||
const values = [];
|
||||
const addNavigatorString = (prop) => {
|
||||
try {
|
||||
const value = navigator[prop];
|
||||
if (typeof value === "string") values.push([prop, value]);
|
||||
} catch {}
|
||||
};
|
||||
addNavigatorString("userAgent");
|
||||
addNavigatorString("platform");
|
||||
if (!values.length) return "";
|
||||
|
||||
return `(() => {
|
||||
try {
|
||||
const values = ${JSON.stringify(values)};
|
||||
const nav = self.navigator;
|
||||
if (!nav) return;
|
||||
const defineProperty = Object.defineProperty;
|
||||
const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
||||
const getPrototypeOf = Object.getPrototypeOf;
|
||||
const Reflect_apply = Reflect.apply;
|
||||
const nativeFunctionToString = Function.prototype.toString;
|
||||
const nativeSources = new WeakMap();
|
||||
const WeakMap_get = WeakMap.prototype.get;
|
||||
const WeakMap_has = WeakMap.prototype.has;
|
||||
const WeakMap_set = WeakMap.prototype.set;
|
||||
const rememberNative = (fn, source) => {
|
||||
if (typeof fn === "function") Reflect_apply(WeakMap_set, nativeSources, [fn, source]);
|
||||
return fn;
|
||||
};
|
||||
const findDescriptor = (prop) => {
|
||||
let owner = nav;
|
||||
while (owner) {
|
||||
const descriptor = getOwnPropertyDescriptor(owner, prop);
|
||||
if (descriptor) return [owner, descriptor];
|
||||
owner = getPrototypeOf(owner);
|
||||
}
|
||||
return [getPrototypeOf(nav) || nav, undefined];
|
||||
};
|
||||
let patched = false;
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
const prop = values[i][0];
|
||||
const value = values[i][1];
|
||||
try {
|
||||
if (nav[prop] === value) continue;
|
||||
} catch (_) {}
|
||||
const found = findDescriptor(prop);
|
||||
const owner = found[0];
|
||||
const descriptor = found[1];
|
||||
if (!owner || (descriptor && descriptor.configurable === false)) continue;
|
||||
const getterDescriptor = getOwnPropertyDescriptor({ get [prop]() { return value; } }, prop);
|
||||
const getter = getterDescriptor && getterDescriptor.get;
|
||||
if (typeof getter !== "function") continue;
|
||||
rememberNative(getter, "function get " + prop + "() { [native code] }");
|
||||
defineProperty(owner, prop, {
|
||||
get: getter,
|
||||
enumerable: descriptor ? descriptor.enumerable : true,
|
||||
configurable: true,
|
||||
});
|
||||
patched = true;
|
||||
}
|
||||
if (!patched) return;
|
||||
const toStringDescriptor = getOwnPropertyDescriptor(Function.prototype, "toString");
|
||||
if (toStringDescriptor && toStringDescriptor.configurable === false) return;
|
||||
const functionToString = new Proxy(nativeFunctionToString, {
|
||||
apply(target, thisArg, args) {
|
||||
if (Reflect_apply(WeakMap_has, nativeSources, [thisArg])) return Reflect_apply(WeakMap_get, nativeSources, [thisArg]);
|
||||
return Reflect_apply(target, thisArg, args);
|
||||
},
|
||||
});
|
||||
rememberNative(functionToString, "function toString() { [native code] }");
|
||||
defineProperty(Function.prototype, "toString", {
|
||||
value: functionToString,
|
||||
writable: toStringDescriptor ? toStringDescriptor.writable : true,
|
||||
configurable: toStringDescriptor ? toStringDescriptor.configurable : true,
|
||||
enumerable: toStringDescriptor ? toStringDescriptor.enumerable : false,
|
||||
});
|
||||
} catch (_) {}
|
||||
})();`;
|
||||
};
|
||||
|
||||
const buildWrappedUrl = (scriptURL) => {
|
||||
const originalUrl = resolveWorkerUrl(scriptURL);
|
||||
if (!originalUrl) return undefined;
|
||||
if (sharedWorkerUrls) {
|
||||
const cachedUrl = sharedWorkerUrls.get(originalUrl);
|
||||
if (cachedUrl) return { url: cachedUrl, cacheKey: originalUrl };
|
||||
}
|
||||
|
||||
preludeLines.push("} catch (e) {};");
|
||||
const prelude = preludeLines.join("\n");
|
||||
const importLine = options?.type === "module"
|
||||
? `import ${JSON.stringify(String(scriptURL))};`
|
||||
: `importScripts(${JSON.stringify(String(scriptURL))});`;
|
||||
const blob = new Window_Blob([prelude, "\n", importLine], { type: "application/javascript" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
return url;
|
||||
const prelude = buildWorkerPrelude();
|
||||
if (!prelude) return undefined;
|
||||
|
||||
try {
|
||||
const blob = new Window_Blob(
|
||||
[prelude, "\n", `importScripts(${JSON.stringify(originalUrl)});`],
|
||||
{ type: "application/javascript" },
|
||||
);
|
||||
const url = Reflect_apply(URL_createObjectURL, NativeURL, [blob]);
|
||||
if (sharedWorkerUrls) sharedWorkerUrls.set(originalUrl, url);
|
||||
return { url, cacheKey: sharedWorkerUrls ? originalUrl : undefined };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const handler = {
|
||||
construct(target, args) {
|
||||
construct(target, args, newTarget) {
|
||||
if (!canWrapArguments(args)) {
|
||||
return Reflect_construct(target, args || [], newTarget);
|
||||
}
|
||||
|
||||
const wrapped = buildWrappedUrl(args[0]);
|
||||
if (!wrapped) {
|
||||
return Reflect_construct(target, args || [], newTarget);
|
||||
}
|
||||
|
||||
const wrappedArgs = [wrapped.url];
|
||||
for (let i = 1; i < args.length; i += 1) {
|
||||
wrappedArgs[i] = args[i];
|
||||
}
|
||||
|
||||
try {
|
||||
const scriptURL = args?.[0];
|
||||
const options = args?.[1];
|
||||
if (!scriptURL) {
|
||||
return new target(...(args || []));
|
||||
const worker = Reflect_construct(target, wrappedArgs, newTarget);
|
||||
if (!sharedWorkerUrls) {
|
||||
scheduleWorkerUrlRevoke(worker, wrapped.url);
|
||||
}
|
||||
const wrappedUrl = buildWrappedUrl(scriptURL, options);
|
||||
const worker = new target(wrappedUrl, options);
|
||||
URL.revokeObjectURL(wrappedUrl);
|
||||
return worker;
|
||||
} catch {
|
||||
return new target(...(args || []));
|
||||
if (wrapped.cacheKey && sharedWorkerUrls) sharedWorkerUrls.delete(wrapped.cacheKey);
|
||||
revokeUrl(wrapped.url);
|
||||
return Reflect_construct(target, args || [], newTarget);
|
||||
}
|
||||
},
|
||||
apply(target, thisArg, args) {
|
||||
@@ -55,16 +217,15 @@ const patchWorkerConstructor = (name, OriginalWorker) => {
|
||||
};
|
||||
|
||||
const proxied = new Window_Proxy(OriginalWorker, handler);
|
||||
patchToString(proxied, name);
|
||||
Object_defineProperty(window, name, {
|
||||
value: proxied,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
Object_defineProperty(window[name], "toString", {
|
||||
value: Function_toString.bind(OriginalWorker),
|
||||
writable: false,
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
writable:
|
||||
windowDescriptor && "writable" in windowDescriptor
|
||||
? windowDescriptor.writable
|
||||
: true,
|
||||
configurable: windowDescriptor ? windowDescriptor.configurable : true,
|
||||
enumerable: windowDescriptor ? windowDescriptor.enumerable : false,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,648 @@
|
||||
diff --git a/lib/puppeteer/cdp/ExecutionContext.js b/lib/puppeteer/cdp/ExecutionContext.js
|
||||
index d2cfb72b63070527185c8d2e3b9bf7b19e91baa4..439821d543413ea620833ebe94908fe44e70b836 100644
|
||||
--- a/lib/puppeteer/cdp/ExecutionContext.js
|
||||
+++ b/lib/puppeteer/cdp/ExecutionContext.js
|
||||
@@ -326,14 +326,19 @@ export class ExecutionContext extends EventEmitter {
|
||||
return await this.#evaluate(false, pageFunction, ...args);
|
||||
}
|
||||
async #evaluate(returnByValue, pageFunction, ...args) {
|
||||
- const sourceUrlComment = getSourceUrlComment(getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
|
||||
- PuppeteerURL.INTERNAL_URL);
|
||||
+ // xxx-stealth: never append the synthetic
|
||||
+ // `//# sourceURL=__puppeteer_evaluation_script__` marker. That constant string
|
||||
+ // leaks automation through V8 error stacks / debugger script listings. Any
|
||||
+ // genuine user-supplied sourceURL already in the source is preserved untouched.
|
||||
+ void getSourceUrlComment;
|
||||
+ void PuppeteerURL;
|
||||
+ const sourceUrlComment = '';
|
||||
if (isString(pageFunction)) {
|
||||
const contextId = this.#id;
|
||||
const expression = pageFunction;
|
||||
const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression)
|
||||
? expression
|
||||
- : `${expression}\n${sourceUrlComment}\n`;
|
||||
+ : expression;
|
||||
const { exceptionDetails, result: remoteObject } = await this.#client
|
||||
.send('Runtime.evaluate', {
|
||||
expression: expressionWithSourceUrl,
|
||||
@@ -352,9 +357,8 @@ export class ExecutionContext extends EventEmitter {
|
||||
return this.#world.createCdpHandle(remoteObject);
|
||||
}
|
||||
const functionDeclaration = stringifyFunction(pageFunction);
|
||||
- const functionDeclarationWithSourceUrl = SOURCE_URL_REGEX.test(functionDeclaration)
|
||||
- ? functionDeclaration
|
||||
- : `${functionDeclaration}\n${sourceUrlComment}\n`;
|
||||
+ void sourceUrlComment;
|
||||
+ const functionDeclarationWithSourceUrl = functionDeclaration;
|
||||
let callFunctionOnPromise;
|
||||
try {
|
||||
callFunctionOnPromise = this.#client.send('Runtime.callFunctionOn', {
|
||||
diff --git a/lib/puppeteer/cdp/FrameManager.js b/lib/puppeteer/cdp/FrameManager.js
|
||||
index 23afcd3def20ff2f578bf14692de1c86365e95c8..26925d30746cb83d0322ea8b4e93993ff4a28a8e 100644
|
||||
--- a/lib/puppeteer/cdp/FrameManager.js
|
||||
+++ b/lib/puppeteer/cdp/FrameManager.js
|
||||
@@ -13,6 +13,7 @@ import { disposeSymbol } from '../util/disposable.js';
|
||||
import { isErrorLike } from '../util/ErrorLike.js';
|
||||
import { CdpIssue } from './CdpIssue.js';
|
||||
import { CdpPreloadScript } from './CdpPreloadScript.js';
|
||||
+import { CDP_BINDING_PREFIX } from './utils.js';
|
||||
import { isTargetClosedError } from './Connection.js';
|
||||
import { CdpDeviceRequestPromptManager } from './DeviceRequestPrompt.js';
|
||||
import { ExecutionContext } from './ExecutionContext.js';
|
||||
@@ -44,6 +45,10 @@ export class FrameManager extends EventEmitter {
|
||||
* frameNavigated event usually contains the latest information.
|
||||
*/
|
||||
#frameNavigatedReceived = new Set();
|
||||
+ // xxx-stealth: coalesce concurrent world re-acquisitions per frame so the
|
||||
+ // frameNavigated/init/load triggers don't stomp each other's contexts.
|
||||
+ #acquireQueued = new Set();
|
||||
+ #acquirePromises = new Map();
|
||||
#deviceRequestPromptManagerMap = new WeakMap();
|
||||
#frameTreeHandled;
|
||||
get timeoutSettings() {
|
||||
@@ -181,9 +186,18 @@ export class FrameManager extends EventEmitter {
|
||||
this.#frameTreeHandled?.resolve();
|
||||
}),
|
||||
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
|
||||
- client.send('Runtime.enable').then(() => {
|
||||
- return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME);
|
||||
- }),
|
||||
+ // xxx-stealth: do NOT send Runtime.enable. It is the single
|
||||
+ // most-detected automation tell (Brotector/CreepJS/Cloudflare probe
|
||||
+ // it). Execution contexts are instead acquired pull-style in
|
||||
+ // #acquireWorlds (main world via Runtime.evaluate globalThis idOnly,
|
||||
+ // utility world via the Page.createIsolatedWorld response) and fed
|
||||
+ // into the existing push pipeline via #onExecutionContextCreated.
|
||||
+ // The utility-world preload sentinel is kept so world-scoped init
|
||||
+ // scripts still attach on navigation.
|
||||
+ client.send('Page.addScriptToEvaluateOnNewDocument', {
|
||||
+ source: `//# sourceURL=${PuppeteerURL.INTERNAL_URL}`,
|
||||
+ worldName: UTILITY_WORLD_NAME,
|
||||
+ }).catch(debugError),
|
||||
...(frame
|
||||
? Array.from(this.#scriptsToEvaluateOnNewDocument.values())
|
||||
: []).map(script => {
|
||||
@@ -336,6 +350,7 @@ export class FrameManager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
frame = new CdpFrame(this, frameId, parentFrameId, session);
|
||||
+ this.#installContextProviders(frame);
|
||||
this._frameTree.addFrame(frame);
|
||||
this.emit(FrameManagerEvent.FrameAttached, frame);
|
||||
}
|
||||
@@ -366,6 +381,177 @@ export class FrameManager extends EventEmitter {
|
||||
frame._navigated(framePayload);
|
||||
this.emit(FrameManagerEvent.FrameNavigated, frame);
|
||||
frame.emit(FrameEvent.FrameNavigated, navigationType);
|
||||
+ // xxx-stealth: install lazy context providers and invalidate the
|
||||
+ // pre-navigation contexts. With Runtime.enable off there is no
|
||||
+ // executionContextDestroyed event, so dispose synchronously here; this makes
|
||||
+ // IsolatedWorld.#context undefined so the next evaluate pulls a fresh context
|
||||
+ // via its provider (resolved after the navigation has settled) instead of
|
||||
+ // using a dead one. We intentionally do NOT proactively acquire — proactive
|
||||
+ // contexts captured mid-navigation go stale silently. Resolution is lazy.
|
||||
+ this.#installContextProviders(frame);
|
||||
+ for (const world of this.#frameWorlds(frame)) {
|
||||
+ world?.context?.[disposeSymbol]();
|
||||
+ }
|
||||
+ }
|
||||
+ // xxx-stealth: the main + utility worlds. worlds is keyed by Symbols, so
|
||||
+ // Object.values misses them — enumerate the known world symbols explicitly.
|
||||
+ #frameWorlds(frame) {
|
||||
+ return [frame.worlds[MAIN_WORLD], frame.worlds[PUPPETEER_WORLD]];
|
||||
+ }
|
||||
+ // xxx-stealth: point each of the frame's worlds at the coalesced acquirer
|
||||
+ // so IsolatedWorld can pull its context on demand.
|
||||
+ #installContextProviders(frame) {
|
||||
+ for (const world of this.#frameWorlds(frame)) {
|
||||
+ world?.setContextProvider?.(() => this.#acquireWorlds(frame));
|
||||
+ }
|
||||
+ }
|
||||
+ // xxx-stealth: coalescing acquirer. Returns a promise that resolves when
|
||||
+ // the current (or freshly started) acquisition for this frame completes, so a
|
||||
+ // lazy provider can await it. Concurrent callers share the in-flight promise
|
||||
+ // rather than racing — concurrent acquires resolve different transient contexts
|
||||
+ // and blank each other.
|
||||
+ #acquireWorlds(frame) {
|
||||
+ const id = frame._id;
|
||||
+ const existing = this.#acquirePromises.get(id);
|
||||
+ if (existing) {
|
||||
+ this.#acquireQueued.add(id);
|
||||
+ return existing;
|
||||
+ }
|
||||
+ const promise = this.#doAcquireWorlds(frame).finally(() => {
|
||||
+ this.#acquirePromises.delete(id);
|
||||
+ if (this.#acquireQueued.delete(id) && this.frame(id)) {
|
||||
+ void this.#acquireWorlds(frame);
|
||||
+ }
|
||||
+ });
|
||||
+ this.#acquirePromises.set(id, promise);
|
||||
+ return promise;
|
||||
+ }
|
||||
+ // xxx-stealth: true when `frame` is the top frame of its CDP session
|
||||
+ // (the page main frame, or an OOP iframe root). Only such frames can resolve
|
||||
+ // their main world via a context-less Runtime.evaluate, because that targets
|
||||
+ // the session's default context. Same-process sub-frames share the parent's
|
||||
+ // session, so a context-less evaluate would resolve the WRONG frame — we skip
|
||||
+ // proactive main-world acquisition for them rather than mis-register.
|
||||
+ #frameIsTopOfSession(frame) {
|
||||
+ const parentId = frame._parentId;
|
||||
+ if (!parentId) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ const parent = this.frame(parentId);
|
||||
+ return !parent || parent.client !== frame.client;
|
||||
+ }
|
||||
+ // xxx-stealth: pull-acquire a frame's main + utility execution contexts
|
||||
+ // without Runtime.enable, then feed them into the normal push pipeline.
|
||||
+ async #doAcquireWorlds(frame) {
|
||||
+ const session = frame.client;
|
||||
+ // xxx-stealth: never pre-dispose here. IsolatedWorld.setContext
|
||||
+ // already disposes the previous context when a fresh one is installed, and
|
||||
+ // a transiently-failed resolve (common while a navigation is mid-flight)
|
||||
+ // must leave the last good context intact rather than blank the world.
|
||||
+ // Stale invalidation on navigation is handled once in #onFrameNavigated.
|
||||
+ try {
|
||||
+ // Utility (PUPPETEER) world: Page.createIsolatedWorld returns the new
|
||||
+ // context id directly — works for any frameId on the session.
|
||||
+ const iso = await session
|
||||
+ .send('Page.createIsolatedWorld', {
|
||||
+ frameId: frame._id,
|
||||
+ worldName: UTILITY_WORLD_NAME,
|
||||
+ grantUniveralAccess: true,
|
||||
+ })
|
||||
+ .catch(debugError);
|
||||
+ const utilityId = iso && typeof iso.executionContextId === 'number' ? iso.executionContextId : undefined;
|
||||
+ if (utilityId !== undefined) {
|
||||
+ this.#onExecutionContextCreated({
|
||||
+ id: utilityId,
|
||||
+ name: UTILITY_WORLD_NAME,
|
||||
+ origin: '',
|
||||
+ auxData: { frameId: frame._id, isDefault: false },
|
||||
+ }, session);
|
||||
+ }
|
||||
+ // Main world: resolve this frame's main execution context id.
|
||||
+ const id = await this.#resolveMainContextId(session, frame, utilityId);
|
||||
+ if (id !== undefined) {
|
||||
+ this.#onExecutionContextCreated({
|
||||
+ id,
|
||||
+ name: '',
|
||||
+ origin: '',
|
||||
+ auxData: { frameId: frame._id, isDefault: true },
|
||||
+ }, session);
|
||||
+ // xxx-stealth: re-install exposed-function bindings into the
|
||||
+ // freshly acquired main world. Normally the binding wrapper is
|
||||
+ // (re)installed when Chrome fires executionContextCreated; with that
|
||||
+ // event silenced we must re-add the native binding for this context
|
||||
+ // id and re-run the wrapper init source ourselves on every navigation.
|
||||
+ for (const binding of this.#bindings) {
|
||||
+ void session
|
||||
+ .send('Runtime.addBinding', {
|
||||
+ name: CDP_BINDING_PREFIX + binding.name,
|
||||
+ executionContextId: id,
|
||||
+ })
|
||||
+ .catch(() => { });
|
||||
+ void session
|
||||
+ .send('Runtime.evaluate', {
|
||||
+ expression: binding.initSource,
|
||||
+ contextId: id,
|
||||
+ })
|
||||
+ .catch(() => { });
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ catch (error) {
|
||||
+ debugError(error);
|
||||
+ }
|
||||
+ }
|
||||
+ // xxx-stealth: resolve a frame's MAIN-world execution context id without
|
||||
+ // Runtime.enable. For the top frame of a session a context-less
|
||||
+ // `Runtime.evaluate globalThis` resolves the session default (cheap, 1 RTT). For
|
||||
+ // same-process sub-frames that would resolve the PARENT, so instead we take the
|
||||
+ // frame's document node (via the utility world we just created) and DOM.resolveNode
|
||||
+ // it with no executionContextId — CDP resolves it in the owning frame's main world,
|
||||
+ // whose objectId encodes the main context id. The objectId format is
|
||||
+ // `<backend>.<contextId>.<n>`.
|
||||
+ async #resolveMainContextId(session, frame, utilityId) {
|
||||
+ const parse = (objectId) => {
|
||||
+ if (typeof objectId !== 'string') {
|
||||
+ return undefined;
|
||||
+ }
|
||||
+ const id = Number.parseInt(objectId.split('.')[1] ?? '', 10);
|
||||
+ return Number.isNaN(id) ? undefined : id;
|
||||
+ };
|
||||
+ if (this.#frameIsTopOfSession(frame)) {
|
||||
+ const globalThis = await session
|
||||
+ .send('Runtime.evaluate', {
|
||||
+ expression: 'globalThis',
|
||||
+ serializationOptions: { serialization: 'idOnly' },
|
||||
+ })
|
||||
+ .catch(debugError);
|
||||
+ return parse(globalThis?.result?.objectId);
|
||||
+ }
|
||||
+ if (utilityId === undefined) {
|
||||
+ return undefined;
|
||||
+ }
|
||||
+ const utilDoc = await session
|
||||
+ .send('Runtime.evaluate', {
|
||||
+ expression: 'document',
|
||||
+ contextId: utilityId,
|
||||
+ serializationOptions: { serialization: 'idOnly' },
|
||||
+ })
|
||||
+ .catch(debugError);
|
||||
+ const utilDocObjectId = utilDoc?.result?.objectId;
|
||||
+ if (typeof utilDocObjectId !== 'string') {
|
||||
+ return undefined;
|
||||
+ }
|
||||
+ const described = await session
|
||||
+ .send('DOM.describeNode', { objectId: utilDocObjectId })
|
||||
+ .catch(debugError);
|
||||
+ const backendNodeId = described?.node?.backendNodeId;
|
||||
+ if (typeof backendNodeId !== 'number') {
|
||||
+ return undefined;
|
||||
+ }
|
||||
+ const mainNode = await session
|
||||
+ .send('DOM.resolveNode', { backendNodeId })
|
||||
+ .catch(debugError);
|
||||
+ return parse(mainNode?.object?.objectId);
|
||||
}
|
||||
async #createIsolatedWorld(session, name) {
|
||||
const key = `${session.id()}:${name}`;
|
||||
diff --git a/lib/puppeteer/cdp/IsolatedWorld.js b/lib/puppeteer/cdp/IsolatedWorld.js
|
||||
index b0619734a9eeb884f3ac4ffff39b540226aaf818..0ef7ce4616a27ff9aabd0747937b593f28bb36de 100644
|
||||
--- a/lib/puppeteer/cdp/IsolatedWorld.js
|
||||
+++ b/lib/puppeteer/cdp/IsolatedWorld.js
|
||||
@@ -21,6 +21,13 @@ export class IsolatedWorld extends Realm {
|
||||
#worldId;
|
||||
#origin;
|
||||
#frameOrWorker;
|
||||
+ // xxx-stealth: lazy context provider. With Runtime.enable disabled there
|
||||
+ // are no executionContextCreated events to push contexts, so the world resolves
|
||||
+ // its context on demand the first time it is needed after a navigation.
|
||||
+ #contextProvider;
|
||||
+ setContextProvider(provider) {
|
||||
+ this.#contextProvider = provider;
|
||||
+ }
|
||||
constructor(frameOrWorker, timeoutSettings, worldId) {
|
||||
super(timeoutSettings);
|
||||
this.#frameOrWorker = frameOrWorker;
|
||||
@@ -72,6 +79,19 @@ export class IsolatedWorld extends Realm {
|
||||
* Waits for the next context to be set on the isolated world.
|
||||
*/
|
||||
async #waitForExecutionContext() {
|
||||
+ // xxx-stealth: pull the context on demand before falling back to
|
||||
+ // waiting for a (never-arriving) push event.
|
||||
+ if (this.#contextProvider && !this.#context && !this.disposed) {
|
||||
+ try {
|
||||
+ await this.#contextProvider();
|
||||
+ }
|
||||
+ catch {
|
||||
+ // fall through to the event wait below
|
||||
+ }
|
||||
+ if (this.#context) {
|
||||
+ return this.#context;
|
||||
+ }
|
||||
+ }
|
||||
const error = new Error('Execution context was destroyed');
|
||||
const result = await firstValueFrom(fromEmitterEvent(this.#emitter, 'context').pipe(raceWith(fromEmitterEvent(this.#emitter, 'disposed').pipe(map(() => {
|
||||
// The message has to match the CDP message expected by the WaitTask class.
|
||||
diff --git a/lib/puppeteer/cdp/WebWorker.js b/lib/puppeteer/cdp/WebWorker.js
|
||||
index e3ee6673245e92faa53a028867e7dbb3a1aed089..4fcc9785077ca868fd0fa892b4ec907cd807ed96 100644
|
||||
--- a/lib/puppeteer/cdp/WebWorker.js
|
||||
+++ b/lib/puppeteer/cdp/WebWorker.js
|
||||
@@ -27,9 +27,21 @@ export class CdpWebWorker extends WebWorker {
|
||||
this.#targetType = targetType;
|
||||
this.#world = new IsolatedWorld(this, new TimeoutSettings(), MAIN_WORLD);
|
||||
this.#emitter = new EventEmitter();
|
||||
- this.#client.once('Runtime.executionContextCreated', async (event) => {
|
||||
- this.#world.setContext(new ExecutionContext(client, event.context, this.#world));
|
||||
- });
|
||||
+ // xxx-stealth: acquire the worker's execution context via an idOnly
|
||||
+ // globalThis evaluate instead of Runtime.enable + executionContextCreated.
|
||||
+ void this.#client
|
||||
+ .send('Runtime.evaluate', {
|
||||
+ expression: 'globalThis',
|
||||
+ serializationOptions: { serialization: 'idOnly' },
|
||||
+ })
|
||||
+ .then((res) => {
|
||||
+ const objectId = res?.result?.objectId;
|
||||
+ const id = typeof objectId === 'string' ? Number.parseInt(objectId.split('.')[1] ?? '', 10) : NaN;
|
||||
+ if (!Number.isNaN(id)) {
|
||||
+ this.#world.setContext(new ExecutionContext(client, { id }, this.#world));
|
||||
+ }
|
||||
+ })
|
||||
+ .catch(debugError);
|
||||
this.#world.emitter.on('consoleapicalled', async (event) => {
|
||||
try {
|
||||
const values = event.args.map(arg => {
|
||||
@@ -61,7 +73,6 @@ export class CdpWebWorker extends WebWorker {
|
||||
});
|
||||
// This might fail if the target is closed before we receive all execution contexts.
|
||||
networkManager?.addClient(this.#client).catch(debugError);
|
||||
- this.#client.send('Runtime.enable').catch(debugError);
|
||||
}
|
||||
mainRealm() {
|
||||
return this.#world;
|
||||
diff --git a/lib/puppeteer/node/ChromeLauncher.js b/lib/puppeteer/node/ChromeLauncher.js
|
||||
index 94db7a76f81ecdcff59a620c1fd5b65957e22e68..fdd8de60c97b7c3037c0d7676fabf150e190751c 100644
|
||||
--- a/lib/puppeteer/node/ChromeLauncher.js
|
||||
+++ b/lib/puppeteer/node/ChromeLauncher.js
|
||||
@@ -126,22 +126,12 @@ export class ChromeLauncher extends BrowserLauncher {
|
||||
].filter(feature => {
|
||||
return feature !== '';
|
||||
});
|
||||
- // Merge default disabled features with user-provided ones, if any.
|
||||
+ // xxx-stealth: drop puppeteer's default --disable-features list. That
|
||||
+ // list (Translate, AcceptCHFrame, MediaRouter, ...) is a non-default flag
|
||||
+ // fingerprint vs a real user-launched Chrome. Only honor user-supplied
|
||||
+ // disabled features so the assembled --disable-features looks organic.
|
||||
+ void turnOnExperimentalFeaturesForTesting;
|
||||
const disabledFeatures = [
|
||||
- 'Translate',
|
||||
- // AcceptCHFrame disabled because of crbug.com/1348106.
|
||||
- 'AcceptCHFrame',
|
||||
- 'MediaRouter',
|
||||
- 'OptimizationHints',
|
||||
- 'WebUIReloadButton',
|
||||
- ...(turnOnExperimentalFeaturesForTesting
|
||||
- ? []
|
||||
- : [
|
||||
- // https://crbug.com/1492053
|
||||
- 'ProcessPerSiteUpToMainFrameThreshold',
|
||||
- // https://github.com/puppeteer/puppeteer/issues/10715
|
||||
- 'IsolateSandboxedIframes',
|
||||
- ]),
|
||||
...userDisabledFeatures,
|
||||
]
|
||||
.filter(feature => {
|
||||
diff --git a/lib/puppeteer/api/Frame.js b/lib/puppeteer/api/Frame.js
|
||||
--- a/lib/puppeteer/api/Frame.js
|
||||
+++ b/lib/puppeteer/api/Frame.js
|
||||
@@ -119,6 +119,31 @@
|
||||
export const throwIfDetached = throwIfDisposed(frame => {
|
||||
return `Attempted to use detached Frame '${frame._id}'.`;
|
||||
});
|
||||
+const MAIN_WORLD_DIRECTIVE = /^\s*(?:(?:\/\/!world=main(?=$|\s))|(?:\/\*!world=main\s*\*\/))/;
|
||||
+const shouldEvaluateInMainWorld = (pageFunction) => {
|
||||
+ if (typeof pageFunction !== 'function' && typeof pageFunction !== 'string') {
|
||||
+ return false;
|
||||
+ }
|
||||
+ let source;
|
||||
+ try {
|
||||
+ source =
|
||||
+ typeof pageFunction === 'string'
|
||||
+ ? pageFunction
|
||||
+ : Function.prototype.toString.call(pageFunction);
|
||||
+ }
|
||||
+ catch {
|
||||
+ return false;
|
||||
+ }
|
||||
+ if (MAIN_WORLD_DIRECTIVE.test(source)) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ if (typeof pageFunction !== 'function') {
|
||||
+ return false;
|
||||
+ }
|
||||
+ const arrowIndex = source.indexOf('=>');
|
||||
+ const bodyStart = source.indexOf('{', arrowIndex >= 0 ? arrowIndex : 0);
|
||||
+ return bodyStart >= 0 && MAIN_WORLD_DIRECTIVE.test(source.slice(bodyStart + 1));
|
||||
+};
|
||||
/**
|
||||
* Represents a DOM frame.
|
||||
*
|
||||
@@ -277,12 +302,21 @@
|
||||
super();
|
||||
}
|
||||
#_document;
|
||||
+ #_mainDocument;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
- #document() {
|
||||
+ #document(mainWorld = false) {
|
||||
+ if (mainWorld) {
|
||||
+ if (!this.#_mainDocument) {
|
||||
+ this.#_mainDocument = this.mainRealm().evaluateHandle(() => {
|
||||
+ return document;
|
||||
+ });
|
||||
+ }
|
||||
+ return this.#_mainDocument;
|
||||
+ }
|
||||
if (!this.#_document) {
|
||||
- this.#_document = this.mainRealm().evaluateHandle(() => {
|
||||
+ this.#_document = this.isolatedRealm().evaluateHandle(() => {
|
||||
return document;
|
||||
});
|
||||
}
|
||||
@@ -295,6 +329,7 @@
|
||||
*/
|
||||
clearDocumentHandle() {
|
||||
this.#_document = undefined;
|
||||
+ this.#_mainDocument = undefined;
|
||||
}
|
||||
/**
|
||||
* @returns The frame element associated with this frame (if any).
|
||||
@@ -345,8 +380,9 @@
|
||||
* See {@link Page.evaluateHandle} for details.
|
||||
*/
|
||||
async evaluateHandle(pageFunction, ...args) {
|
||||
+ const realm = shouldEvaluateInMainWorld(pageFunction) ? this.mainRealm() : this.isolatedRealm();
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.evaluateHandle.name, pageFunction);
|
||||
- return await this.mainRealm().evaluateHandle(pageFunction, ...args);
|
||||
+ return await realm.evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
/**
|
||||
* Behaves identically to {@link Page.evaluate} except it's run within
|
||||
@@ -355,8 +391,9 @@
|
||||
* See {@link Page.evaluate} for details.
|
||||
*/
|
||||
async evaluate(pageFunction, ...args) {
|
||||
+ const realm = shouldEvaluateInMainWorld(pageFunction) ? this.mainRealm() : this.isolatedRealm();
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.evaluate.name, pageFunction);
|
||||
- return await this.mainRealm().evaluate(pageFunction, ...args);
|
||||
+ return await realm.evaluate(pageFunction, ...args);
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
@@ -458,9 +495,10 @@
|
||||
* @returns A promise to the result of the function.
|
||||
*/
|
||||
async $eval(selector, pageFunction, ...args) {
|
||||
+ const mainWorld = shouldEvaluateInMainWorld(pageFunction);
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
|
||||
// eslint-disable-next-line @puppeteer/use-using -- This is cached.
|
||||
- const document = await this.#document();
|
||||
+ const document = await this.#document(mainWorld);
|
||||
return await document.$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
/**
|
||||
@@ -498,9 +536,10 @@
|
||||
* @returns A promise to the result of the function.
|
||||
*/
|
||||
async $$eval(selector, pageFunction, ...args) {
|
||||
+ const mainWorld = shouldEvaluateInMainWorld(pageFunction);
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
|
||||
// eslint-disable-next-line @puppeteer/use-using -- This is cached.
|
||||
- const document = await this.#document();
|
||||
+ const document = await this.#document(mainWorld);
|
||||
return await document.$$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
/**
|
||||
@@ -577,7 +616,8 @@
|
||||
* @returns the promise which resolve when the `pageFunction` returns a truthy value.
|
||||
*/
|
||||
async waitForFunction(pageFunction, options = {}, ...args) {
|
||||
- return await this.mainRealm().waitForFunction(pageFunction, options, ...args);
|
||||
+ const realm = shouldEvaluateInMainWorld(pageFunction) ? this.mainRealm() : this.isolatedRealm();
|
||||
+ return await realm.waitForFunction(pageFunction, options, ...args);
|
||||
}
|
||||
/**
|
||||
* The full HTML contents of the frame, including the DOCTYPE.
|
||||
diff --git a/lib/puppeteer/cdp/Frame.js b/lib/puppeteer/cdp/Frame.js
|
||||
--- a/lib/puppeteer/cdp/Frame.js
|
||||
+++ b/lib/puppeteer/cdp/Frame.js
|
||||
@@ -276,7 +276,7 @@
|
||||
this.#client.send('Runtime.addBinding', {
|
||||
name: CDP_BINDING_PREFIX + binding.name,
|
||||
}),
|
||||
- this.evaluate(binding.initSource).catch(debugError),
|
||||
+ this.mainRealm().evaluate(binding.initSource).catch(debugError),
|
||||
]);
|
||||
}
|
||||
async removeExposedFunctionBinding(binding) {
|
||||
@@ -289,7 +289,7 @@
|
||||
this.#client.send('Runtime.removeBinding', {
|
||||
name: CDP_BINDING_PREFIX + binding.name,
|
||||
}),
|
||||
- this.evaluate(name => {
|
||||
+ this.mainRealm().evaluate(name => {
|
||||
// Removes the dangling Puppeteer binding wrapper.
|
||||
// @ts-expect-error: In a different context.
|
||||
globalThis[name] = undefined;
|
||||
diff --git a/lib/puppeteer/api/ElementHandle.js b/lib/puppeteer/api/ElementHandle.js
|
||||
--- a/lib/puppeteer/api/ElementHandle.js
|
||||
+++ b/lib/puppeteer/api/ElementHandle.js
|
||||
@@ -102,6 +102,31 @@
|
||||
import { _isElementHandle } from './ElementHandleSymbol.js';
|
||||
import { JSHandle } from './JSHandle.js';
|
||||
import { NodeLocator } from './locators/locators.js';
|
||||
+const MAIN_WORLD_DIRECTIVE = /^\s*(?:(?:\/\/!world=main(?=$|\s))|(?:\/\*!world=main\s*\*\/))/;
|
||||
+const shouldEvaluateInMainWorld = (pageFunction) => {
|
||||
+ if (typeof pageFunction !== 'function' && typeof pageFunction !== 'string') {
|
||||
+ return false;
|
||||
+ }
|
||||
+ let source;
|
||||
+ try {
|
||||
+ source =
|
||||
+ typeof pageFunction === 'string'
|
||||
+ ? pageFunction
|
||||
+ : Function.prototype.toString.call(pageFunction);
|
||||
+ }
|
||||
+ catch {
|
||||
+ return false;
|
||||
+ }
|
||||
+ if (MAIN_WORLD_DIRECTIVE.test(source)) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ if (typeof pageFunction !== 'function') {
|
||||
+ return false;
|
||||
+ }
|
||||
+ const arrowIndex = source.indexOf('=>');
|
||||
+ const bodyStart = source.indexOf('{', arrowIndex >= 0 ? arrowIndex : 0);
|
||||
+ return bodyStart >= 0 && MAIN_WORLD_DIRECTIVE.test(source.slice(bodyStart + 1));
|
||||
+};
|
||||
/**
|
||||
* A given method will have it's `this` replaced with an isolated version of
|
||||
* `this` when decorated with this decorator.
|
||||
@@ -299,6 +324,7 @@
|
||||
* trying to adopt it multiple times
|
||||
*/
|
||||
isolatedHandle = __runInitializers(this, _instanceExtraInitializers);
|
||||
+ mainHandle;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -335,19 +361,37 @@
|
||||
async getProperties() {
|
||||
return await this.handle.getProperties();
|
||||
}
|
||||
+ async #handleForPageFunction(pageFunction) {
|
||||
+ const realm = shouldEvaluateInMainWorld(pageFunction) ? this.frame.mainRealm() : this.frame.isolatedRealm();
|
||||
+ if (this.realm === realm) {
|
||||
+ return this;
|
||||
+ }
|
||||
+ if (realm === this.frame.isolatedRealm()) {
|
||||
+ if (!this.isolatedHandle) {
|
||||
+ this.isolatedHandle = await realm.adoptHandle(this);
|
||||
+ }
|
||||
+ return this.isolatedHandle;
|
||||
+ }
|
||||
+ if (!this.mainHandle) {
|
||||
+ this.mainHandle = await realm.adoptHandle(this);
|
||||
+ }
|
||||
+ return this.mainHandle;
|
||||
+ }
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async evaluate(pageFunction, ...args) {
|
||||
+ const handle = await this.#handleForPageFunction(pageFunction);
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.evaluate.name, pageFunction);
|
||||
- return await this.handle.evaluate(pageFunction, ...args);
|
||||
+ return await handle.handle.evaluate(pageFunction, ...args);
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async evaluateHandle(pageFunction, ...args) {
|
||||
+ const handle = await this.#handleForPageFunction(pageFunction);
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.evaluateHandle.name, pageFunction);
|
||||
- return await this.handle.evaluateHandle(pageFunction, ...args);
|
||||
+ return await handle.handle.evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
@@ -371,7 +415,7 @@
|
||||
* @internal
|
||||
*/
|
||||
async dispose() {
|
||||
- await Promise.all([this.handle.dispose(), this.isolatedHandle?.dispose()]);
|
||||
+ await Promise.all([this.handle.dispose(), this.isolatedHandle?.dispose(), this.mainHandle?.dispose()]);
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
diff --git a/lib/puppeteer/api/ElementHandle.js b/lib/puppeteer/api/ElementHandle.js
|
||||
--- a/lib/puppeteer/api/ElementHandle.js
|
||||
+++ b/lib/puppeteer/api/ElementHandle.js
|
||||
@@ -599,15 +599,27 @@
|
||||
async $$eval(selector, pageFunction, ...args) {
|
||||
const env_2 = { stack: [], error: void 0, hasError: false };
|
||||
try {
|
||||
+ const mainWorld = shouldEvaluateInMainWorld(pageFunction);
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
|
||||
const results = await this.$$(selector);
|
||||
- const elements = __addDisposableResource(env_2, await this.evaluateHandle((_, ...elements) => {
|
||||
+ const realm = mainWorld ? this.frame.mainRealm() : this.frame.isolatedRealm();
|
||||
+ const adoptedResults = [];
|
||||
+ const handlesToDispose = [];
|
||||
+ for (const result of results) {
|
||||
+ handlesToDispose.push(result);
|
||||
+ const adopted = result.realm === realm ? result : await realm.adoptHandle(result);
|
||||
+ adoptedResults.push(adopted);
|
||||
+ if (adopted !== result) {
|
||||
+ handlesToDispose.push(adopted);
|
||||
+ }
|
||||
+ }
|
||||
+ const elements = __addDisposableResource(env_2, await realm.evaluateHandle((...elements) => {
|
||||
return elements;
|
||||
- }, ...results), false);
|
||||
+ }, ...adoptedResults), false);
|
||||
const [result] = await Promise.all([
|
||||
elements.evaluate(pageFunction, ...args),
|
||||
- ...results.map(results => {
|
||||
- return results.dispose();
|
||||
+ ...handlesToDispose.map(result => {
|
||||
+ return result.dispose();
|
||||
}),
|
||||
]);
|
||||
return result;
|
||||
Reference in New Issue
Block a user