Spaces:
Paused
Paused
| """Comprehensive anti-detection (stealth) scripts for Playwright browser contexts. | |
| This module centralises all anti-fingerprinting init scripts used by the | |
| various captcha solvers. Every solver should import from here instead of | |
| maintaining its own copy. | |
| Key techniques: | |
| - Remove / mask navigator.webdriver | |
| - Realistic navigator.plugins (not a plain array) | |
| - Full window.chrome shim | |
| - Permissions API override | |
| - navigator.connection simulation | |
| - Console.debug masking (reCAPTCHA detection vector) | |
| - getBoundingClientRect consistency patch | |
| - iframe contentWindow protection | |
| """ | |
| from __future__ import annotations | |
| # ββ Core stealth script ββββββββββββββββββββββββββββββββββββββββββββββ | |
| STEALTH_JS = """ | |
| // ββ 1. navigator.webdriver βββββββββββββββββββββββββββββββββββββββββ | |
| // Delete the property from the prototype so `webdriver in navigator` is false, | |
| // not just returning undefined from a getter. | |
| try { | |
| Object.defineProperty(navigator, 'webdriver', { | |
| get: () => undefined, | |
| configurable: true | |
| }); | |
| delete Object.getPrototypeOf(navigator).__proto__.webdriver; | |
| } catch (_) {} | |
| // ββ 2. navigator.plugins β realistic mock βββββββββββββββββββββββββββ | |
| // Google's bot detection checks that plugins is a PluginArray, not a plain | |
| // array. We construct a minimal but plausible PluginArray-like object. | |
| try { | |
| const _makePlugin = (name, filename, description, mimes) => { | |
| const m = mimes.map(t => Object.create(MimeType.prototype, { | |
| type: { value: t.type, enumerable: true }, | |
| suffixes: { value: t.suffixes, enumerable: true }, | |
| description: { value: t.description, enumerable: true } | |
| })); | |
| const p = Object.create(Plugin.prototype, { | |
| name: { value: name, enumerable: true }, | |
| filename: { value: filename, enumerable: true }, | |
| description: { value: description, enumerable: true }, | |
| length: { value: m.length, enumerable: true } | |
| }); | |
| m.forEach((mime, i) => { p[i] = mime; }); | |
| return p; | |
| }; | |
| const _plugins = [ | |
| _makePlugin('PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format', [ | |
| { type: 'application/pdf', suffixes: 'pdf', description: '' } | |
| ]), | |
| _makePlugin('Chrome PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format', [ | |
| { type: 'application/pdf', suffixes: 'pdf', description: '' } | |
| ]), | |
| _makePlugin('Chromium PDF Plugin', 'internal-pdf-viewer', 'Portable Document Format', [ | |
| { type: 'application/pdf', suffixes: 'pdf', description: '' } | |
| ]), | |
| ]; | |
| const pluginArray = Object.create(PluginArray.prototype); | |
| _plugins.forEach((p, i) => { | |
| Object.defineProperty(pluginArray, i, { value: p, enumerable: true }); | |
| Object.defineProperty(pluginArray, p.name, { value: p, enumerable: true }); | |
| }); | |
| Object.defineProperty(pluginArray, 'length', { value: _plugins.length, enumerable: true }); | |
| Object.defineProperty(navigator, 'plugins', { | |
| get: () => pluginArray, | |
| configurable: true | |
| }); | |
| } catch (_) {} | |
| // ββ 3. window.chrome shim βββββββββββββββββββββββββββββββββββββββββββ | |
| // A real Chrome browser always exposes a `chrome` global with several | |
| // well-known sub-properties. | |
| try { | |
| if (!window.chrome) { | |
| window.chrome = {}; | |
| } | |
| if (!window.chrome.runtime) { | |
| window.chrome.runtime = { | |
| OnInstalledReason: { | |
| CHROME_UPDATE: 'chrome_update', | |
| INSTALL: 'install', | |
| SHARED_MODULE_UPDATE: 'shared_module_update', | |
| UPDATE: 'update' | |
| }, | |
| OnRestartRequiredReason: { | |
| APP_UPDATE: 'app_update', | |
| OS_UPDATE: 'os_update', | |
| PERIODIC: 'periodic' | |
| }, | |
| PlatformArch: { | |
| ARM: 'arm', | |
| ARM64: 'arm64', | |
| MIPS: 'mips', | |
| MIPS64: 'mips64', | |
| X86_32: 'x86-32', | |
| X86_64: 'x86-64' | |
| }, | |
| PlatformNaclArch: { | |
| ARM: 'arm', | |
| MIPS: 'mips', | |
| MIPS64: 'mips64', | |
| X86_32: 'x86-32', | |
| X86_64: 'x86-64' | |
| }, | |
| PlatformOs: { | |
| ANDROID: 'android', | |
| CROS: 'cros', | |
| LINUX: 'linux', | |
| MAC: 'mac', | |
| OPENBSD: 'openbsd', | |
| WIN: 'win' | |
| }, | |
| RequestUpdateCheckStatus: { | |
| NO_UPDATE: 'no_update', | |
| THROTTLED: 'throttled', | |
| UPDATE_AVAILABLE: 'update_available' | |
| }, | |
| connect: function() {}, | |
| sendMessage: function() {} | |
| }; | |
| } | |
| if (!window.chrome.csi) { | |
| window.chrome.csi = function() { | |
| return { | |
| onloadT: Date.now(), | |
| startE: Date.now(), | |
| pageT: Math.random() * 500 + 100, | |
| tran: 15 | |
| }; | |
| }; | |
| } | |
| if (!window.chrome.loadTimes) { | |
| window.chrome.loadTimes = function() { | |
| const now = Date.now() / 1000; | |
| return { | |
| commitLoadTime: now, | |
| connectionInfo: 'h2', | |
| finishDocumentLoadTime: now + 0.1, | |
| finishLoadTime: now + 0.2, | |
| firstPaintAfterLoadTime: now + 0.3, | |
| firstPaintTime: now + 0.05, | |
| navigationType: 'Other', | |
| npnNegotiatedProtocol: 'h2', | |
| requestTime: now - 0.5, | |
| startLoadTime: now - 0.4, | |
| wasAlternateProtocolAvailable: false, | |
| wasFetchedViaSpdy: true, | |
| wasNpnNegotiated: true | |
| }; | |
| }; | |
| } | |
| } catch (_) {} | |
| // ββ 4. navigator.languages ββββββββββββββββββββββββββββββββββββββββββ | |
| try { | |
| Object.defineProperty(navigator, 'languages', { | |
| get: () => ['en-US', 'en'], | |
| configurable: true | |
| }); | |
| } catch (_) {} | |
| // ββ 5. Permissions API override βββββββββββββββββββββββββββββββββββββ | |
| // reCAPTCHA calls navigator.permissions.query({name:'notifications'}) and | |
| // expects a typical result. Headless Chromium returns a different state. | |
| try { | |
| const _origQuery = window.Permissions.prototype.query; | |
| window.Permissions.prototype.query = function(parameters) { | |
| if (parameters.name === 'notifications') { | |
| return Promise.resolve({ | |
| state: Notification.permission === 'denied' ? 'denied' : 'prompt', | |
| onchange: null | |
| }); | |
| } | |
| return _origQuery.call(this, parameters); | |
| }; | |
| } catch (_) {} | |
| // ββ 6. navigator.connection ββββββββββββββββββββββββββββββββββββββββββ | |
| try { | |
| if (!navigator.connection) { | |
| Object.defineProperty(navigator, 'connection', { | |
| get: () => ({ | |
| effectiveType: '4g', | |
| rtt: 50, | |
| downlink: 10, | |
| saveData: false, | |
| onchange: null, | |
| addEventListener: function() {}, | |
| removeEventListener: function() {} | |
| }), | |
| configurable: true | |
| }); | |
| } | |
| } catch (_) {} | |
| // ββ 7. Console.debug masking ββββββββββββββββββββββββββββββββββββββββ | |
| // Some captcha scripts inject a debug probe via console.debug. | |
| try { | |
| const _origDebug = console.debug; | |
| console.debug = function() { | |
| if (arguments.length && typeof arguments[0] === 'string' | |
| && arguments[0].includes('Captcha')) { | |
| return; | |
| } | |
| return _origDebug.apply(console, arguments); | |
| }; | |
| } catch (_) {} | |
| // ββ 8. getBoundingClientRect consistency βββββββββββββββββββββββββββββ | |
| // Headless Chromium's element rects can subtly differ; ensure getClientRects | |
| // returns results when getBoundingClientRect does. | |
| try { | |
| const _origGetClientRects = Element.prototype.getClientRects; | |
| Element.prototype.getClientRects = function() { | |
| const rects = _origGetClientRects.call(this); | |
| if (rects.length === 0) { | |
| const br = this.getBoundingClientRect(); | |
| if (br.width > 0 && br.height > 0) { | |
| return [br]; | |
| } | |
| } | |
| return rects; | |
| }; | |
| } catch (_) {} | |
| // ββ 9. WebGL vendor / renderer override βββββββββββββββββββββββββββββ | |
| // Some detections read the unmasked WebGL renderer to spot headless. | |
| try { | |
| const _origGetParam = WebGLRenderingContext.prototype.getParameter; | |
| WebGLRenderingContext.prototype.getParameter = function(param) { | |
| if (param === 37445) return 'Intel Inc.'; // UNMASKED_VENDOR_WEBGL | |
| if (param === 37446) return 'Intel Iris OpenGL Engine'; // UNMASKED_RENDERER_WEBGL | |
| return _origGetParam.call(this, param); | |
| }; | |
| if (typeof WebGL2RenderingContext !== 'undefined') { | |
| const _origGetParam2 = WebGL2RenderingContext.prototype.getParameter; | |
| WebGL2RenderingContext.prototype.getParameter = function(param) { | |
| if (param === 37445) return 'Intel Inc.'; | |
| if (param === 37446) return 'Intel Iris OpenGL Engine'; | |
| return _origGetParam2.call(this, param); | |
| }; | |
| } | |
| } catch (_) {} | |
| // ββ 10. iframe contentWindow protection βββββββββββββββββββββββββββββ | |
| // Ensure cross-origin iframes expose a .contentWindow (headless sometimes | |
| // returns null). | |
| try { | |
| const _origDesc = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow'); | |
| if (!_origDesc || !_origDesc.get) { | |
| Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', { | |
| get: function() { | |
| return this._contentWindow || window; | |
| }, | |
| configurable: true | |
| }); | |
| } | |
| } catch (_) {} | |
| """ | |
| # ββ Browser launch arguments βββββββββββββββββββββββββββββββββββββββββ | |
| STEALTH_LAUNCH_ARGS = [ | |
| "--disable-blink-features=AutomationControlled", | |
| "--no-sandbox", | |
| "--disable-dev-shm-usage", | |
| "--disable-gpu", | |
| # Additional anti-detection flags | |
| "--disable-infobars", | |
| "--window-size=1920,1080", | |
| "--disable-extensions", | |
| "--disable-component-extensions-with-background-pages", | |
| "--disable-default-apps", | |
| "--disable-background-timer-throttling", | |
| "--disable-backgrounding-occluded-windows", | |
| "--disable-renderer-backgrounding", | |
| "--disable-background-networking", | |
| "--disable-breakpad", | |
| "--disable-component-update", | |
| "--disable-domain-reliability", | |
| "--disable-sync", | |
| "--disable-features=TranslateUI", | |
| "--no-first-run", | |
| "--no-default-browser-check", | |
| ] | |
| # ββ Realistic User-Agent strings (rotated per context) ββββββββββββββ | |
| USER_AGENTS = [ | |
| ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/131.0.0.0 Safari/537.36" | |
| ), | |
| ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/130.0.0.0 Safari/537.36" | |
| ), | |
| ( | |
| "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/131.0.0.0 Safari/537.36" | |
| ), | |
| ( | |
| "Mozilla/5.0 (X11; Linux x86_64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/131.0.0.0 Safari/537.36" | |
| ), | |
| ] |