"""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" ), ]