use crate::page::escape_js_string;
use crate::stealth::fingerprint::Fingerprint;
use crate::StealthConfig;
pub const NATIVE_TOSTRING_EVASION: &str = r#"
const _eoka_spoofed = new WeakMap();
const _eoka_origToString = Function.prototype.toString;
const _eoka_mark = (fn, name) => {
if (typeof fn === 'function') {
_eoka_spoofed.set(fn, name || fn.name || '');
}
return fn;
};
const _eoka_toStringProxy = new Proxy(_eoka_origToString, {
apply(target, thisArg, args) {
if (typeof thisArg === 'function' && _eoka_spoofed.has(thisArg)) {
const name = _eoka_spoofed.get(thisArg);
return name ? 'function ' + name + '() { [native code] }'
: 'function () { [native code] }';
}
if (thisArg === _eoka_toStringProxy) {
return 'function toString() { [native code] }';
}
return Reflect.apply(target, thisArg, args);
}
});
Function.prototype.toString = _eoka_toStringProxy;
_eoka_mark(Function.prototype.toString, 'toString');
// Deterministic per-session PRNG (LCG), seeded from Rust.
const _eoka_lcg = (s) => (Math.imul(s >>> 0, 1664525) + 1013904223) >>> 0;
let _eoka_seed = (__EOKA_NOISE_SEED__ >>> 0) || 1;
const _eoka_rand = () => {
_eoka_seed = _eoka_lcg(_eoka_seed);
return _eoka_seed / 4294967296;
};
"#;
pub const WEBDRIVER_EVASION: &str = r#"
const _eoka_webdriverGet = _eoka_mark(() => false, 'get webdriver');
Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', {
get: _eoka_webdriverGet,
configurable: true,
enumerable: true
});
try {
Object.defineProperty(Navigator.prototype, 'webdriver', {
get: _eoka_webdriverGet,
configurable: true,
enumerable: true
});
} catch(e) {}
// Remove automation-related properties from window
const automationProps = [
'callPhantom', '_phantom', 'phantom', '__nightmare', 'domAutomation',
'domAutomationController', '_selenium', '_Selenium_IDE_Recorder',
'callSelenium', '__webdriver_script_fn', '__driver_evaluate',
'__webdriver_evaluate', '__fxdriver_evaluate', '__driver_unwrapped',
'__webdriver_unwrapped', '__fxdriver_unwrapped', '__selenium_unwrapped',
'_WEBDRIVER_ELEM_CACHE', 'ChromeDriverw', 'driver-evaluate',
'webdriver-evaluate', 'selenium-evaluate', 'webdriverCommand',
'webdriver-evaluate-response', '__webdriverFunc', '__lastWatirAlert',
'__lastWatirConfirm', '__lastWatirPrompt', '$chrome_asyncScriptInfo',
'__selenium_evaluate', '__webdriver_script_function'
];
automationProps.forEach(prop => {
try {
if (prop in window) delete window[prop];
} catch(e) {}
});
// Fix for Chrome's runtime.bindingsCalled check
if (window.chrome && window.chrome.runtime) {
try {
delete window.chrome.runtime.bindingsCalled;
} catch(e) {}
}
"#;
pub const CDP_EVASION: &str = r#"
// Pattern to match CDP/automation markers
const cdcPattern = /^cdc_|^\$cdc_|^__webdriver|^__selenium|^__driver|^\$chrome_|^\$wdc_/;
// Clean up CDP markers from window
const cleanupWindow = () => {
for (const prop of Object.getOwnPropertyNames(window)) {
if (cdcPattern.test(prop)) {
try { delete window[prop]; } catch(e) {}
}
}
};
// Clean up CDP markers from document
const cleanupDocument = () => {
for (const prop of Object.getOwnPropertyNames(document)) {
if (cdcPattern.test(prop)) {
try { delete document[prop]; } catch(e) {}
}
}
};
// Run cleanup immediately
cleanupWindow();
cleanupDocument();
// Override Object.getOwnPropertyNames to filter out CDP markers
const originalGetOwnPropertyNames = Object.getOwnPropertyNames;
Object.getOwnPropertyNames = _eoka_mark(function getOwnPropertyNames(obj) {
const names = originalGetOwnPropertyNames.call(this, obj);
if (obj === window || obj === document) {
return names.filter(name => !cdcPattern.test(name));
}
return names;
}, 'getOwnPropertyNames');
// Override Object.keys to filter out CDP markers
const originalKeys = Object.keys;
Object.keys = _eoka_mark(function keys(obj) {
const keys = originalKeys.call(this, obj);
if (obj === window || obj === document) {
return keys.filter(key => !cdcPattern.test(key));
}
return keys;
}, 'keys');
// Override hasOwnProperty to hide CDP markers
const originalHasOwnProperty = Object.prototype.hasOwnProperty;
Object.prototype.hasOwnProperty = _eoka_mark(function hasOwnProperty(prop) {
if ((this === window || this === document) && cdcPattern.test(prop)) {
return false;
}
return originalHasOwnProperty.call(this, prop);
}, 'hasOwnProperty');
// Override Object.getOwnPropertyDescriptor to hide CDP markers
const originalGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
Object.getOwnPropertyDescriptor = _eoka_mark(function getOwnPropertyDescriptor(obj, prop) {
if ((obj === window || obj === document) && cdcPattern.test(prop)) {
return undefined;
}
return originalGetOwnPropertyDescriptor.call(this, obj, prop);
}, 'getOwnPropertyDescriptor');
// Intercept direct property access on document using a Proxy (bound fns cached for stable identity).
const realDocument = document;
const _eoka_boundCache = new WeakMap();
try {
const documentProxy = new Proxy(realDocument, {
get(target, prop, receiver) {
if (typeof prop === 'string' && cdcPattern.test(prop)) {
return undefined;
}
const value = Reflect.get(target, prop, target);
if (typeof value === 'function') {
let cache = _eoka_boundCache.get(target);
if (!cache) { cache = new Map(); _eoka_boundCache.set(target, cache); }
if (!cache.has(prop)) cache.set(prop, value.bind(target));
return cache.get(prop);
}
return value;
},
has(target, prop) {
if (typeof prop === 'string' && cdcPattern.test(prop)) {
return false;
}
return Reflect.has(target, prop);
},
getOwnPropertyDescriptor(target, prop) {
if (typeof prop === 'string' && cdcPattern.test(prop)) {
return undefined;
}
return Reflect.getOwnPropertyDescriptor(target, prop);
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(key =>
typeof key !== 'string' || !cdcPattern.test(key)
);
}
});
Object.defineProperty(window, 'document', {
get: _eoka_mark(() => documentProxy, 'get document'),
configurable: true
});
} catch(e) {
let cleanupCount = 0;
const cleanupTimer = setInterval(() => {
cleanupWindow();
cleanupDocument();
if (++cleanupCount >= 10) clearInterval(cleanupTimer);
}, 200);
}
// Clean up document element attributes
const cleanupDocAttributes = () => {
const attrs = ['selenium', 'webdriver', 'driver'];
const docElement = document.documentElement;
if (docElement) {
attrs.forEach(attr => {
try {
if (docElement.hasAttribute(attr)) {
docElement.removeAttribute(attr);
}
} catch(e) {}
});
}
};
if (document.documentElement) {
cleanupDocAttributes();
}
document.addEventListener('DOMContentLoaded', cleanupDocAttributes);
"#;
pub const CHROME_RUNTIME_EVASION: &str = r#"
window.chrome = {
runtime: {
onConnect: {
addListener: function() {},
removeListener: function() {}
},
onMessage: {
addListener: function() {},
removeListener: function() {}
},
connect: function() {
return {
onMessage: { addListener: function() {} },
onDisconnect: { addListener: function() {} },
postMessage: function() {}
};
},
sendMessage: function() {},
id: undefined
},
loadTimes: function() {
return {
commitLoadTime: Date.now() / 1000 - _eoka_rand() * 2,
connectionInfo: "h2",
finishDocumentLoadTime: Date.now() / 1000 - _eoka_rand(),
finishLoadTime: Date.now() / 1000 - _eoka_rand() * 0.5,
firstPaintAfterLoadTime: 0,
firstPaintTime: Date.now() / 1000 - _eoka_rand() * 1.5,
navigationType: "Other",
npnNegotiatedProtocol: "h2",
requestTime: Date.now() / 1000 - _eoka_rand() * 3,
startLoadTime: Date.now() / 1000 - _eoka_rand() * 2.5,
wasAlternateProtocolAvailable: false,
wasFetchedViaSpdy: true,
wasNpnNegotiated: true
};
},
csi: function() {
return {
onloadT: Date.now(),
pageT: _eoka_rand() * 1000 + 500,
startE: Date.now() - _eoka_rand() * 3000,
tran: 15
};
},
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" }
}
};
"#;
pub const PERMISSIONS_EVASION: &str = r#"
// Fix Notification/Permissions API consistency
const originalPermissionsQuery = navigator.permissions.query.bind(navigator.permissions);
try {
Object.defineProperty(Notification, 'permission', {
get: _eoka_mark(() => 'default', 'get permission'),
configurable: true,
enumerable: true
});
} catch(e) {}
navigator.permissions.query = _eoka_mark(function query(parameters) {
const name = parameters.name;
if (name === 'notifications') {
return Promise.resolve({
state: 'prompt',
name: 'notifications',
onchange: null,
addEventListener: function() {},
removeEventListener: function() {},
dispatchEvent: function() { return true; }
});
}
if (name === 'clipboard-read' || name === 'clipboard-write') {
return Promise.resolve({
state: 'prompt',
name: name,
onchange: null,
addEventListener: function() {},
removeEventListener: function() {},
dispatchEvent: function() { return true; }
});
}
return originalPermissionsQuery(parameters).then(function(result) {
return {
state: result.state,
name: result.name || name,
onchange: result.onchange,
addEventListener: result.addEventListener?.bind(result) || function() {},
removeEventListener: result.removeEventListener?.bind(result) || function() {},
dispatchEvent: result.dispatchEvent?.bind(result) || function() { return true; }
};
}).catch(function() {
return {
state: 'prompt',
name: name,
onchange: null,
addEventListener: function() {},
removeEventListener: function() {},
dispatchEvent: function() { return true; }
};
});
}, 'query');
"#;
pub const PLUGINS_EVASION: &str = r#"
const _eoka_buildPlugins = () => {
const plugins = [
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Microsoft Edge PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'WebKit built-in PDF', filename: 'internal-pdf-viewer', description: 'Portable Document Format' }
];
const pluginArray = Object.create(PluginArray.prototype);
plugins.forEach((p, i) => {
const plugin = Object.create(Plugin.prototype);
Object.defineProperties(plugin, {
name: { value: p.name, enumerable: true },
filename: { value: p.filename, enumerable: true },
description: { value: p.description, enumerable: true },
length: { value: 1, enumerable: true }
});
pluginArray[i] = plugin;
pluginArray[p.name] = plugin;
});
Object.defineProperty(pluginArray, 'length', { value: plugins.length });
pluginArray.item = _eoka_mark((i) => pluginArray[i] || null, 'item');
pluginArray.namedItem = _eoka_mark((name) => pluginArray[name] || null, 'namedItem');
pluginArray.refresh = _eoka_mark(() => {}, 'refresh');
return pluginArray;
};
const _eoka_pluginArray = _eoka_buildPlugins();
Object.defineProperty(Navigator.prototype, 'plugins', {
get: _eoka_mark(() => _eoka_pluginArray, 'get plugins'),
configurable: true,
enumerable: true
});
"#;
pub const NAVIGATOR_PROPS_EVASION: &str = r#"
const navProps = {
languages: { get: () => Object.freeze(__EOKA_LANGUAGES__) },
language: { get: () => '__EOKA_LANGUAGE__' },
platform: { get: () => '__EOKA_PLATFORM__' },
vendor: { get: () => 'Google Inc.' },
hardwareConcurrency: { get: () => __EOKA_HW__ },
deviceMemory: { get: () => __EOKA_MEM__ },
maxTouchPoints: { get: () => 0 }
};
for (const [prop, desc] of Object.entries(navProps)) {
_eoka_mark(desc.get, 'get ' + prop);
Object.defineProperty(Navigator.prototype, prop, {
...desc,
configurable: true,
enumerable: true
});
}
"#;
pub const HEADLESS_EVASION: &str = r#"
// Fix screen properties
Object.defineProperty(screen, 'availWidth', { get: _eoka_mark(() => screen.width, 'get availWidth'), configurable: true });
Object.defineProperty(screen, 'availHeight', { get: _eoka_mark(() => screen.height - 40, 'get availHeight'), configurable: true });
// Mock window dimensions
Object.defineProperty(window, 'outerWidth', { get: _eoka_mark(() => window.innerWidth, 'get outerWidth'), configurable: true });
Object.defineProperty(window, 'outerHeight', { get: _eoka_mark(() => window.innerHeight + 85, 'get outerHeight'), configurable: true });
// Fix window.matchMedia
const originalMatchMedia = window.matchMedia;
if (originalMatchMedia) {
window.matchMedia = _eoka_mark(function matchMedia(query) {
const result = originalMatchMedia.call(window, query);
if (query.includes('prefers-reduced-motion')) {
return {
matches: false,
media: query,
onchange: null,
addListener: function() {},
removeListener: function() {},
addEventListener: function() {},
removeEventListener: function() {},
dispatchEvent: function() { return true; }
};
}
return result;
}, 'matchMedia');
}
// Ensure window.origin is correct
try {
if (!window.origin || window.origin === 'null') {
Object.defineProperty(window, 'origin', {
get: _eoka_mark(function() { return window.location.origin; }, 'get origin'),
configurable: true
});
}
} catch(e) {}
"#;
pub const BATTERY_EVASION: &str = r#"
const originalGetBattery = Navigator.prototype.getBattery;
if (originalGetBattery) {
Object.defineProperty(Navigator.prototype, 'getBattery', {
value: _eoka_mark(function getBattery() {
return originalGetBattery.call(this).then(function(battery) {
return new Proxy(battery, {
get(target, prop) {
switch (prop) {
case 'charging': return true;
case 'chargingTime': return 0;
case 'dischargingTime': return Infinity;
case 'level': return 1;
}
const v = target[prop];
return typeof v === 'function' ? v.bind(target) : v;
}
});
});
}, 'getBattery'),
configurable: true,
writable: true
});
}
"#;
pub const NAVIGATOR_EXTRA_EVASION: &str = r#"
if (navigator.userAgentData) {
const originalGetHighEntropyValues = navigator.userAgentData.getHighEntropyValues?.bind(navigator.userAgentData);
if (originalGetHighEntropyValues) {
navigator.userAgentData.getHighEntropyValues = _eoka_mark(function getHighEntropyValues(hints) {
return originalGetHighEntropyValues(hints).then(function(data) {
if (data.brands) {
data.brands = data.brands.filter(b =>
!b.brand.toLowerCase().includes('headless')
);
}
return data;
});
}, 'getHighEntropyValues');
}
}
// Fix navigator.connection
if (navigator.connection) {
try {
const connectionProps = {
downlink: 10,
effectiveType: '4g',
rtt: 50,
saveData: false
};
for (const [key, value] of Object.entries(connectionProps)) {
try {
Object.defineProperty(navigator.connection, key, {
get: _eoka_mark(() => value, 'get ' + key),
configurable: true,
enumerable: true
});
} catch(e) {}
}
} catch(e) {}
}
"#;
pub const FINGERPRINT_EVASION: &str = r#"
const _eoka_spoofWebGL = (proto) => {
const orig = proto.getParameter;
proto.getParameter = _eoka_mark(function getParameter(p) {
if (p === 37445) return '__EOKA_WEBGL_VENDOR__';
if (p === 37446) return '__EOKA_WEBGL_RENDERER__';
return orig.call(this, p);
}, 'getParameter');
};
_eoka_spoofWebGL(WebGLRenderingContext.prototype);
if (typeof WebGL2RenderingContext !== 'undefined') _eoka_spoofWebGL(WebGL2RenderingContext.prototype);
// Canvas noise — deterministic, applied on a temp copy.
const _eoka_noiseCanvas = (canvas) => {
const w = canvas.width | 0, h = canvas.height | 0;
if (w <= 0 || h <= 0) return canvas;
try {
const tmp = document.createElement('canvas');
tmp.width = w; tmp.height = h;
const tctx = tmp.getContext('2d');
tctx.drawImage(canvas, 0, 0);
const img = tctx.getImageData(0, 0, w, h);
const data = img.data;
let s = (__EOKA_NOISE_SEED__ >>> 0) || 1;
for (let i = 0; i < data.length; i += 4) {
s = _eoka_lcg(s);
data[i] = data[i] ^ (s & 1);
}
tctx.putImageData(img, 0, 0);
return tmp;
} catch(e) {
return canvas;
}
};
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = _eoka_mark(function toDataURL(...args) {
return origToDataURL.apply(_eoka_noiseCanvas(this), args);
}, 'toDataURL');
const origToBlob = HTMLCanvasElement.prototype.toBlob;
if (origToBlob) {
HTMLCanvasElement.prototype.toBlob = _eoka_mark(function toBlob(...args) {
return origToBlob.apply(_eoka_noiseCanvas(this), args);
}, 'toBlob');
}
// Audio noise — deterministic, applied once per channel buffer.
const origGetChannelData = AudioBuffer.prototype.getChannelData;
const _eoka_audioSeen = new WeakSet();
AudioBuffer.prototype.getChannelData = _eoka_mark(function getChannelData(ch) {
const d = origGetChannelData.call(this, ch);
if (!_eoka_audioSeen.has(d)) {
let s = ((__EOKA_NOISE_SEED__ >>> 0) ^ (ch >>> 0)) >>> 0 || 1;
for (let i = 0; i < d.length; i += 100) {
s = _eoka_lcg(s);
d[i] += (s / 4294967296 - 0.5) * 0.0001;
}
_eoka_audioSeen.add(d);
}
return d;
}, 'getChannelData');
"#;
pub const WEBRTC_EVASION: &str = r#"
if (typeof RTCPeerConnection !== 'undefined') {
const origRTCPeerConnection = RTCPeerConnection;
const _eoka_filterServers = (config) => {
if (config && config.iceServers) {
config.iceServers = config.iceServers.filter(server => {
const urls = Array.isArray(server.urls) ? server.urls : [server.urls];
return !urls.some(url =>
/^stuns?:/i.test(url) || /^turns?:/i.test(url)
);
});
}
return config;
};
window.RTCPeerConnection = _eoka_mark(function RTCPeerConnection(config, constraints) {
return new origRTCPeerConnection(_eoka_filterServers(config), constraints);
}, 'RTCPeerConnection');
window.RTCPeerConnection.prototype = origRTCPeerConnection.prototype;
Object.keys(origRTCPeerConnection).forEach(key => {
window.RTCPeerConnection[key] = origRTCPeerConnection[key];
});
}
if (typeof webkitRTCPeerConnection !== 'undefined') {
window.webkitRTCPeerConnection = window.RTCPeerConnection;
}
"#;
pub const SPEECH_EVASION: &str = r#"
if (typeof speechSynthesis !== 'undefined') {
const defaultVoices = [
{ name: 'Alex', lang: 'en-US', localService: true, default: true, voiceURI: 'Alex' },
{ name: 'Samantha', lang: 'en-US', localService: true, default: false, voiceURI: 'Samantha' },
{ name: 'Victoria', lang: 'en-US', localService: true, default: false, voiceURI: 'Victoria' },
{ name: 'Daniel', lang: 'en-GB', localService: true, default: false, voiceURI: 'Daniel' },
{ name: 'Google US English', lang: 'en-US', localService: false, default: false, voiceURI: 'Google US English' }
].map(v => {
const voice = Object.create(SpeechSynthesisVoice.prototype);
Object.defineProperties(voice, {
name: { value: v.name, enumerable: true },
lang: { value: v.lang, enumerable: true },
localService: { value: v.localService, enumerable: true },
default: { value: v.default, enumerable: true },
voiceURI: { value: v.voiceURI, enumerable: true }
});
return voice;
});
const origGetVoices = speechSynthesis.getVoices.bind(speechSynthesis);
speechSynthesis.getVoices = _eoka_mark(function getVoices() {
const voices = origGetVoices();
return voices.length > 0 ? voices : defaultVoices;
}, 'getVoices');
}
"#;
pub const MEDIA_DEVICES_EVASION: &str = r#"
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
const origEnumerateDevices = navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
navigator.mediaDevices.enumerateDevices = _eoka_mark(async function enumerateDevices() {
const devices = await origEnumerateDevices();
if (devices.length === 0) {
const fake = [
{ deviceId: 'default', groupId: '5f8b2c1e9a3d', kind: 'audioinput', label: '' },
{ deviceId: 'default', groupId: '5f8b2c1e9a3d', kind: 'audiooutput', label: '' },
{ deviceId: 'e2d4a6c8b0f1', groupId: '9c3e7a1d4b6f', kind: 'videoinput', label: '' }
];
return fake.map(d => {
const device = Object.create(MediaDeviceInfo.prototype);
Object.defineProperties(device, {
deviceId: { value: d.deviceId, enumerable: true },
groupId: { value: d.groupId, enumerable: true },
kind: { value: d.kind, enumerable: true },
label: { value: d.label, enumerable: true },
toJSON: { value: _eoka_mark(() => d, 'toJSON') }
});
return device;
});
}
return devices;
}, 'enumerateDevices');
}
"#;
pub const BLUETOOTH_EVASION: &str = r#"
if (!navigator.bluetooth) {
Object.defineProperty(Navigator.prototype, 'bluetooth', {
get: _eoka_mark(() => ({
getAvailability: () => Promise.resolve(false),
requestDevice: () => Promise.reject(new DOMException('User cancelled', 'NotFoundError')),
getDevices: () => Promise.resolve([]),
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => true
}), 'get bluetooth'),
configurable: true,
enumerable: true
});
}
"#;
pub const TIMEZONE_EVASION: &str = r#"
// Ensure Intl.DateTimeFormat returns consistent timezone
const targetTimezone = '__EOKA_TIMEZONE__';
const origDateTimeFormat = Intl.DateTimeFormat;
Intl.DateTimeFormat = _eoka_mark(function DateTimeFormat(locales, options) {
if (!options) options = {};
if (!options.timeZone) options.timeZone = targetTimezone;
return new origDateTimeFormat(locales, options);
}, 'DateTimeFormat');
Intl.DateTimeFormat.prototype = origDateTimeFormat.prototype;
Intl.DateTimeFormat.supportedLocalesOf = origDateTimeFormat.supportedLocalesOf;
// Override Date.prototype.getTimezoneOffset to match the target timezone.
// Precompute offsets for Jan and Jul to handle DST, avoiding per-call
// toLocaleString() which is ~10x slower than native and timing-detectable.
{
const _origGetTZO = Date.prototype.getTimezoneOffset;
function _calcOffset(date) {
const utcStr = date.toLocaleString('en-US', { timeZone: 'UTC' });
const tzStr = date.toLocaleString('en-US', { timeZone: targetTimezone });
return (new Date(utcStr) - new Date(tzStr)) / 60000;
}
const year = new Date().getFullYear();
const janOffset = _calcOffset(new Date(year, 0, 15));
const julOffset = _calcOffset(new Date(year, 6, 15));
// Standard offset is the larger value (more positive = further west)
const stdOffset = Math.max(janOffset, julOffset);
const dstOffset = Math.min(janOffset, julOffset);
// If offsets differ, this timezone observes DST
const hasDST = janOffset !== julOffset;
// Which months are DST depends on hemisphere: if Jul is the smaller
// offset, DST is northern-hemisphere (Mar-Nov); otherwise southern.
const dstIsJul = julOffset < janOffset;
Date.prototype.getTimezoneOffset = _eoka_mark(function getTimezoneOffset() {
if (!hasDST) return stdOffset;
// Quick month-based check — accurate for the vast majority of dates.
const m = this.getUTCMonth();
const isDST = dstIsJul ? (m >= 2 && m <= 10) : (m <= 2 || m >= 10);
return isDST ? dstOffset : stdOffset;
}, 'getTimezoneOffset');
}
"#;
pub(crate) const COMMON_TIMEZONES: &[&str] = &[
"America/New_York",
"America/Chicago",
"America/Los_Angeles",
"America/Sao_Paulo",
"Europe/London",
"Europe/Paris",
"Europe/Berlin",
"Europe/Moscow",
"Asia/Tokyo",
"Asia/Shanghai",
"Asia/Kolkata",
"Australia/Sydney",
];
pub fn build_evasion_script_for(config: &StealthConfig, fp: &Fingerprint) -> String {
let timezone = config
.timezone
.clone()
.unwrap_or_else(|| fp.timezone.clone());
let mut scripts = vec![
NATIVE_TOSTRING_EVASION, WEBDRIVER_EVASION,
CHROME_RUNTIME_EVASION,
PERMISSIONS_EVASION,
PLUGINS_EVASION,
NAVIGATOR_PROPS_EVASION,
HEADLESS_EVASION,
BATTERY_EVASION,
NAVIGATOR_EXTRA_EVASION,
WEBRTC_EVASION,
SPEECH_EVASION,
MEDIA_DEVICES_EVASION,
BLUETOOTH_EVASION,
TIMEZONE_EVASION,
];
if config.aggressive_cdp_evasion {
scripts.push(CDP_EVASION);
}
if config.webgl_spoof || config.canvas_spoof || config.audio_spoof {
scripts.push(FINGERPRINT_EVASION);
}
let languages_json = format!(
"[{}]",
fp.languages
.iter()
.map(|l| format!("'{}'", escape_js_string(l)))
.collect::<Vec<_>>()
.join(",")
);
let language = fp.languages.first().cloned().unwrap_or_default();
let noise_seed = fastrand::u32(1..=u32::MAX);
let script = format!("(function(){{{}}})();", scripts.join("\n"));
script
.replace("__EOKA_TIMEZONE__", &escape_js_string(&timezone))
.replace("__EOKA_PLATFORM__", &escape_js_string(fp.nav_platform()))
.replace("__EOKA_LANGUAGES__", &languages_json)
.replace("__EOKA_LANGUAGE__", &escape_js_string(&language))
.replace("__EOKA_HW__", &fp.hardware_concurrency.to_string())
.replace("__EOKA_MEM__", &fp.device_memory.to_string())
.replace("__EOKA_WEBGL_VENDOR__", &escape_js_string(&fp.webgl_vendor))
.replace(
"__EOKA_WEBGL_RENDERER__",
&escape_js_string(&fp.webgl_renderer),
)
.replace("__EOKA_NOISE_SEED__", &noise_seed.to_string())
}
pub fn build_evasion_script(config: &StealthConfig) -> String {
let fp = Fingerprint::resolve(config.user_agent.as_deref(), config.timezone.as_deref());
build_evasion_script_for(config, &fp)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_full_script() {
let config = StealthConfig::default();
let script = build_evasion_script(&config);
assert!(script.contains("webdriver"));
assert!(script.contains("WebGLRenderingContext"));
assert!(script.contains("HTMLCanvasElement"));
assert!(script.contains("AudioBuffer"));
assert!(!script.contains("const documentProxy"));
}
#[test]
fn test_aggressive_cdp_evasion_is_opt_in() {
let config = StealthConfig {
aggressive_cdp_evasion: true,
..StealthConfig::default()
};
assert!(build_evasion_script(&config).contains("const documentProxy"));
}
#[test]
fn test_placeholders_are_filled() {
let script = build_evasion_script(&StealthConfig::default());
assert!(
!script.contains("__EOKA_"),
"unresolved placeholder in script"
);
assert!(script.contains("_eoka_mark"));
}
#[test]
fn test_script_is_wrapped_in_iife() {
let config = StealthConfig::default();
let script = build_evasion_script(&config);
assert!(script.starts_with("(function()"));
assert!(script.ends_with("})();"));
}
#[test]
fn test_build_script_for_known_windows_fp() {
let fp = Fingerprint::resolve(
Some("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7312.86 Safari/537.36"),
None,
);
let config = StealthConfig::default();
let script = build_evasion_script_for(&config, &fp);
assert!(
script.contains("'Win32'"),
"script must contain the fingerprint nav_platform 'Win32'"
);
assert!(
!script.contains("'MacIntel'"),
"script must not contain the old hardcoded 'MacIntel'"
);
assert!(
script.contains(&fp.webgl_renderer),
"script must contain the fingerprint webgl_renderer: {}",
fp.webgl_renderer
);
assert!(
script.contains(&fp.hardware_concurrency.to_string()),
"script must contain hardware_concurrency"
);
assert!(
script.contains(&fp.device_memory.to_string()),
"script must contain device_memory"
);
assert!(script.contains("_eoka_mark"));
assert!(script.contains("[native code]"));
assert!(
!script.contains("__EOKA_"),
"script must not contain unresolved placeholders"
);
assert!(
!script.contains("__eoka_pending_requests"),
"removed request-beacon must not reappear"
);
}
#[test]
fn test_config_values_are_js_escaped() {
let config = StealthConfig {
timezone: Some("Foo'Bar".into()),
..StealthConfig::default()
};
let script = build_evasion_script(&config);
assert!(
script.contains(r"Foo\'Bar"),
"timezone single quote must be escaped via escape_js_string"
);
assert!(
!script.contains("'Foo'Bar'"),
"unescaped single quote must not appear"
);
}
}