An aiohttp/httpx-shaped async HTTP client backed by a single, persistent, stealth-patched Chromium instance via Playwright. get/post/put/patch/ delete/head/download use context.request (real Chrome TLS/HTTP2 fingerprint, no tab needed); render()/screenshot()/page() escalate to full JS-executing page rendering for targets that need a JS challenge solved. Supports named sessions for cookie persistence/warm-up, and `async with stealth as page` to claim a tab directly, task-safe for concurrent use on one shared instance. Context/browser recycling bounds cookie and on-disk cache growth in a long-lived process. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0116i7dYLNZTXNR7Lqf439bV
120 lines
5.1 KiB
Python
120 lines
5.1 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
"""Browser-side JS patches injected into every page/context before any page
|
|
script runs. Removes the automation tells that a stock headless Chromium
|
|
otherwise exposes (navigator.webdriver, missing window.chrome, empty
|
|
PluginArray, SwiftShader WebGL renderer, permissions-query mismatch,
|
|
iframe.contentWindow leaking the absence of window.chrome).
|
|
"""
|
|
|
|
STEALTH_INIT_JS = r"""
|
|
(() => {
|
|
const patchNativeToString = (fn, name) => {
|
|
const src = `function ${name}() { [native code] }`;
|
|
const proxied = new Proxy(fn, {
|
|
apply(target, thisArg, args) { return Reflect.apply(target, thisArg, args); },
|
|
});
|
|
Object.defineProperty(proxied, 'toString', {
|
|
value: () => src, configurable: true, enumerable: false, writable: false,
|
|
});
|
|
return proxied;
|
|
};
|
|
const define = (obj, prop, getter) => {
|
|
try {
|
|
Object.defineProperty(obj, prop, { get: patchNativeToString(getter, 'get ' + prop), configurable: true });
|
|
} catch (e) {}
|
|
};
|
|
|
|
define(Navigator.prototype, 'webdriver', () => undefined);
|
|
|
|
try {
|
|
if (!window.chrome || !window.chrome.runtime) {
|
|
window.chrome = {
|
|
runtime: {
|
|
connect: () => {}, sendMessage: () => {}, onMessage: { addListener: () => {} },
|
|
id: undefined,
|
|
},
|
|
loadTimes: function () {}, csi: function () {}, app: {
|
|
isInstalled: false,
|
|
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
|
|
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
|
|
},
|
|
};
|
|
}
|
|
} catch (e) {}
|
|
|
|
try {
|
|
const originalQuery = window.navigator.permissions && window.navigator.permissions.query;
|
|
if (originalQuery) {
|
|
window.navigator.permissions.query = patchNativeToString((parameters) => (
|
|
parameters && parameters.name === 'notifications'
|
|
? Promise.resolve({ state: Notification.permission, onchange: null })
|
|
: originalQuery(parameters)
|
|
), 'query');
|
|
}
|
|
} catch (e) {}
|
|
|
|
try {
|
|
const fakeMimeType = (type, description, suffixes) => ({ type, description, suffixes, enabledPlugin: null });
|
|
const fakePlugin = (name, description, filename, mimes) => {
|
|
const p = { name, description, filename, length: mimes.length };
|
|
mimes.forEach((m, i) => { p[i] = m; m.enabledPlugin = p; });
|
|
p.item = (i) => p[i] || null;
|
|
p.namedItem = (n) => mimes.find((m) => m.type === n) || null;
|
|
return p;
|
|
};
|
|
const pdfMime = fakeMimeType('application/pdf', 'Portable Document Format', 'pdf');
|
|
const plugins = [
|
|
fakePlugin('PDF Viewer', 'Portable Document Format', 'internal-pdf-viewer', [pdfMime]),
|
|
fakePlugin('Chrome PDF Viewer', 'Portable Document Format', 'internal-pdf-viewer', [pdfMime]),
|
|
fakePlugin('Chromium PDF Viewer', 'Portable Document Format', 'internal-pdf-viewer', [pdfMime]),
|
|
fakePlugin('Microsoft Edge PDF Viewer', 'Portable Document Format', 'internal-pdf-viewer', [pdfMime]),
|
|
fakePlugin('WebKit built-in PDF', 'Portable Document Format', 'internal-pdf-viewer', [pdfMime]),
|
|
];
|
|
const pluginArray = plugins;
|
|
pluginArray.item = (i) => pluginArray[i] || null;
|
|
pluginArray.namedItem = (n) => pluginArray.find((p) => p.name === n) || null;
|
|
pluginArray.refresh = () => {};
|
|
pluginArray.forEach((p) => {
|
|
try { Object.setPrototypeOf(p, Plugin.prototype); } catch (e) {}
|
|
Object.defineProperty(p, Symbol.toStringTag, { value: 'Plugin', configurable: true });
|
|
});
|
|
Object.defineProperty(pluginArray, Symbol.toStringTag, { value: 'PluginArray', configurable: true });
|
|
try { Object.setPrototypeOf(pluginArray, PluginArray.prototype); } catch (e) {}
|
|
define(Navigator.prototype, 'plugins', () => pluginArray);
|
|
define(Navigator.prototype, 'mimeTypes', () => {
|
|
const arr = [pdfMime];
|
|
Object.defineProperty(arr, Symbol.toStringTag, { value: 'MimeTypeArray', configurable: true });
|
|
try { Object.setPrototypeOf(arr, MimeTypeArray.prototype); } catch (e) {}
|
|
return arr;
|
|
});
|
|
} catch (e) {}
|
|
|
|
define(Navigator.prototype, 'deviceMemory', () => 8);
|
|
define(Navigator.prototype, 'hardwareConcurrency', () => 8);
|
|
|
|
try {
|
|
const spoofVendor = (ctxProto) => {
|
|
const original = ctxProto.getParameter;
|
|
ctxProto.getParameter = patchNativeToString(function (parameter) {
|
|
if (parameter === 37445) return 'Google Inc. (NVIDIA)';
|
|
if (parameter === 37446) return 'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)';
|
|
return original.apply(this, arguments);
|
|
}, 'getParameter');
|
|
};
|
|
spoofVendor(WebGLRenderingContext.prototype);
|
|
if (window.WebGL2RenderingContext) spoofVendor(WebGL2RenderingContext.prototype);
|
|
} catch (e) {}
|
|
|
|
try {
|
|
const contentWindowDesc = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow');
|
|
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
|
|
get: patchNativeToString(function () {
|
|
const win = contentWindowDesc.get.call(this);
|
|
try { if (win && !win.chrome) win.chrome = window.chrome; } catch (e) {}
|
|
return win;
|
|
}, 'get contentWindow'),
|
|
});
|
|
} catch (e) {}
|
|
})();
|
|
"""
|