ohmycaptcha / src /core /stealth.py
zzdccww's picture
πŸš€ Deploy ohmycaptcha via Automation Tool
8e61460 verified
"""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"
),
]