@fanboynz/network-scanner 2.0.66 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +140 -11
- package/CHANGELOG.md +164 -0
- package/CLAUDE.md +40 -7
- package/README.md +29 -4
- package/lib/adblock-rust.js +23 -18
- package/lib/adblock.js +127 -82
- package/lib/browserexit.js +213 -203
- package/lib/browserhealth.js +85 -61
- package/lib/cdp.js +103 -81
- package/lib/clear_sitedata.js +61 -159
- package/lib/cloudflare.js +579 -409
- package/lib/colorize.js +29 -12
- package/lib/compare.js +16 -8
- package/lib/compress.js +2 -1
- package/lib/curl.js +287 -220
- package/lib/domain-cache.js +87 -40
- package/lib/dry-run.js +137 -194
- package/lib/fingerprint.js +341 -176
- package/lib/flowproxy.js +391 -188
- package/lib/ghost-cursor.js +8 -7
- package/lib/grep.js +248 -171
- package/lib/ignore_similar.js +70 -124
- package/lib/interaction.js +132 -235
- package/lib/nettools.js +309 -87
- package/lib/openvpn_vpn.js +12 -11
- package/lib/output.js +92 -59
- package/lib/post-processing.js +216 -162
- package/lib/proxy.js +9 -2
- package/lib/redirect.js +47 -31
- package/lib/referrer.js +158 -165
- package/lib/searchstring.js +290 -381
- package/lib/smart-cache.js +141 -91
- package/lib/socks-relay.js +21 -7
- package/lib/spawn-async.js +137 -0
- package/lib/validate_rules.js +188 -176
- package/lib/wireguard_vpn.js +111 -117
- package/nwss.js +743 -159
- package/package.json +4 -4
- package/scripts/test-stealth.js +281 -0
package/lib/fingerprint.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// and comprehensive bot detection evasion techniques.
|
|
4
4
|
//const { applyErrorSuppression } = require('./error-suppression');
|
|
5
5
|
|
|
6
|
+
const { formatLogMessage } = require('./colorize');
|
|
7
|
+
|
|
6
8
|
// Default values for fingerprint spoofing if not set to 'random'
|
|
7
9
|
const DEFAULT_PLATFORM = 'Win32';
|
|
8
10
|
const DEFAULT_TIMEZONE = 'America/New_York';
|
|
@@ -24,13 +26,6 @@ function seededRandom(seed) {
|
|
|
24
26
|
const _fingerprintCache = new Map();
|
|
25
27
|
const FINGERPRINT_CACHE_MAX = 500;
|
|
26
28
|
|
|
27
|
-
// Type-specific property spoofing functions for monomorphic optimization
|
|
28
|
-
// Built-in properties that should not be modified
|
|
29
|
-
const BUILT_IN_PROPERTIES = new Set([
|
|
30
|
-
'href', 'origin', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash',
|
|
31
|
-
'constructor', 'prototype', '__proto__', 'toString', 'valueOf', 'assign', 'reload', 'replace'
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
29
|
// User agent collections with latest versions
|
|
35
30
|
const USER_AGENT_COLLECTIONS = Object.freeze(new Map([
|
|
36
31
|
['chrome', "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"],
|
|
@@ -75,15 +70,53 @@ const GPU_POOL = {
|
|
|
75
70
|
};
|
|
76
71
|
|
|
77
72
|
/**
|
|
78
|
-
* Select a GPU from the pool based on user agent string.
|
|
79
|
-
*
|
|
73
|
+
* Select a GPU from the pool based on user agent string. When `domain` is
|
|
74
|
+
* provided, selection is deterministic per-domain (seeded) -- a tracker
|
|
75
|
+
* logging UNMASKED_RENDERER_WEBGL sees the SAME machine across reloads of
|
|
76
|
+
* the same site. Without `domain`, falls back to Math.random for ad-hoc
|
|
77
|
+
* callers. Matches the same per-domain consistency that
|
|
78
|
+
* generateRealisticFingerprint uses for the rest of the spoof set.
|
|
80
79
|
*/
|
|
81
|
-
function selectGpuForUserAgent(userAgentString) {
|
|
80
|
+
function selectGpuForUserAgent(userAgentString, domain = '') {
|
|
82
81
|
let osKey = 'windows';
|
|
83
82
|
if (userAgentString && (userAgentString.includes('Macintosh') || userAgentString.includes('Mac OS X'))) osKey = 'mac';
|
|
84
83
|
else if (userAgentString && (userAgentString.includes('X11; Linux') || userAgentString.includes('Ubuntu'))) osKey = 'linux';
|
|
85
84
|
const pool = GPU_POOL[osKey];
|
|
86
|
-
|
|
85
|
+
// Distinct seed suffix so the GPU pick doesn't collide with the
|
|
86
|
+
// fingerprint generator's advance sequence for the same domain.
|
|
87
|
+
const rand = domain ? seededRandom(domain + ':gpu') : Math.random;
|
|
88
|
+
return pool[Math.floor(rand() * pool.length)];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* One-shot "is this browser dead?" guard. Three of the four spoof entry
|
|
93
|
+
* points (applyUserAgentSpoofing, applyBraveSpoofing,
|
|
94
|
+
* applyFingerprintProtection) had this exact try/catch block inline:
|
|
95
|
+
*
|
|
96
|
+
* try {
|
|
97
|
+
* if (!page.browser().connected || page.isClosed()) return;
|
|
98
|
+
* if (page.browser().process()?.killed) return;
|
|
99
|
+
* } catch { return; }
|
|
100
|
+
*
|
|
101
|
+
* Three copies meant any Puppeteer API change (like the v25
|
|
102
|
+
* isConnected -> connected swap) needed three fixes. Now one.
|
|
103
|
+
*
|
|
104
|
+
* simulateHumanBehavior is NOT migrated here -- it has different debug
|
|
105
|
+
* log messages per failure mode ("page closed" vs "browser
|
|
106
|
+
* disconnected") that this helper doesn't preserve. Could plumb a
|
|
107
|
+
* callback for the log but the existing inline form is fine for one
|
|
108
|
+
* caller.
|
|
109
|
+
*/
|
|
110
|
+
function isBrowserDead(page) {
|
|
111
|
+
try {
|
|
112
|
+
if (!page || page.isClosed()) return true;
|
|
113
|
+
const browser = page.browser();
|
|
114
|
+
if (!browser.connected) return true;
|
|
115
|
+
if (browser.process()?.killed) return true;
|
|
116
|
+
return false;
|
|
117
|
+
} catch {
|
|
118
|
+
return true; // any failure reading browser state -> treat as dead
|
|
119
|
+
}
|
|
87
120
|
}
|
|
88
121
|
|
|
89
122
|
/**
|
|
@@ -101,67 +134,21 @@ function isSessionClosedError(err) {
|
|
|
101
134
|
}
|
|
102
135
|
|
|
103
136
|
/**
|
|
104
|
-
*
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const existing = Object.getOwnPropertyDescriptor(target, property);
|
|
114
|
-
if (existing?.configurable === false) {
|
|
115
|
-
if (options.debug) console.log(`[fingerprint] Cannot modify non-configurable: ${property}`);
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
Object.defineProperty(target, property, descriptor);
|
|
120
|
-
return true;
|
|
121
|
-
} catch (err) {
|
|
122
|
-
if (options.debug) console.log(`[fingerprint] Failed to define ${property}: ${err.message}`);
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Safely executes spoofing operations with error handling
|
|
129
|
-
*/
|
|
130
|
-
function safeSpoofingExecution(spoofFunction, description, options = {}) {
|
|
131
|
-
try {
|
|
132
|
-
spoofFunction();
|
|
133
|
-
return true;
|
|
134
|
-
} catch (err) {
|
|
135
|
-
if (options.debug) console.log(`[fingerprint] ${description} failed: ${err.message}`);
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Generates realistic screen resolutions based on common monitor sizes
|
|
142
|
-
*/
|
|
143
|
-
function getRealisticScreenResolution() {
|
|
144
|
-
const commonResolutions = [
|
|
145
|
-
{ width: 1920, height: 1080 },
|
|
146
|
-
{ width: 1366, height: 768 },
|
|
147
|
-
{ width: 1440, height: 900 },
|
|
148
|
-
{ width: 1536, height: 864 },
|
|
149
|
-
{ width: 1600, height: 900 },
|
|
150
|
-
{ width: 2560, height: 1440 },
|
|
151
|
-
{ width: 1280, height: 720 },
|
|
152
|
-
{ width: 3440, height: 1440 }
|
|
153
|
-
];
|
|
154
|
-
return commonResolutions[Math.floor(Math.random() * commonResolutions.length)];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Generates randomized but realistic browser fingerprint values
|
|
159
|
-
* When domain is provided, values are deterministic per-domain (consistent across reloads)
|
|
137
|
+
* Generates randomized but realistic browser fingerprint values.
|
|
138
|
+
* When domain is provided, values are deterministic per-(domain, userAgent)
|
|
139
|
+
* tuple -- consistent across reloads, but distinct across UA rotation so
|
|
140
|
+
* a re-scan with `userAgent: 'firefox'` doesn't reuse a previous
|
|
141
|
+
* `userAgent: 'chrome'` cache entry (which would ship Mac/Win-platform
|
|
142
|
+
* values under a Firefox UA, etc.).
|
|
160
143
|
*/
|
|
161
144
|
function generateRealisticFingerprint(userAgent, domain = '') {
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
145
|
+
// Cache key includes UA so a domain scanned twice with different UAs
|
|
146
|
+
// doesn't get the first UA's OS-mismatched fingerprint reused for the
|
|
147
|
+
// second. The `|` separator can't appear in a hostname (RFC 952/RFC
|
|
148
|
+
// 1123 allow only LDH) so it's collision-safe.
|
|
149
|
+
const cacheKey = domain ? `${domain}|${userAgent}` : '';
|
|
150
|
+
if (cacheKey) {
|
|
151
|
+
const cached = _fingerprintCache.get(cacheKey);
|
|
165
152
|
if (cached) return cached;
|
|
166
153
|
}
|
|
167
154
|
|
|
@@ -237,12 +224,12 @@ function generateRealisticFingerprint(userAgent, domain = '') {
|
|
|
237
224
|
doNotTrack: null // Most users don't enable DNT
|
|
238
225
|
};
|
|
239
226
|
|
|
240
|
-
// Cache for this domain
|
|
241
|
-
if (
|
|
227
|
+
// Cache for this (domain, userAgent) tuple. Same key as the lookup above.
|
|
228
|
+
if (cacheKey) {
|
|
242
229
|
if (_fingerprintCache.size >= FINGERPRINT_CACHE_MAX) {
|
|
243
230
|
_fingerprintCache.delete(_fingerprintCache.keys().next().value);
|
|
244
231
|
}
|
|
245
|
-
_fingerprintCache.set(
|
|
232
|
+
_fingerprintCache.set(cacheKey, fingerprint);
|
|
246
233
|
}
|
|
247
234
|
|
|
248
235
|
return fingerprint;
|
|
@@ -255,8 +242,8 @@ async function validatePageForInjection(page, currentUrl, forceDebug) {
|
|
|
255
242
|
try {
|
|
256
243
|
if (!page || page.isClosed()) return false;
|
|
257
244
|
|
|
258
|
-
if (!page.browser().
|
|
259
|
-
if (forceDebug) console.log(
|
|
245
|
+
if (!page.browser().connected) {
|
|
246
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Page validation failed - browser disconnected: ${currentUrl}`));
|
|
260
247
|
return false;
|
|
261
248
|
}
|
|
262
249
|
await Promise.race([
|
|
@@ -265,7 +252,7 @@ async function validatePageForInjection(page, currentUrl, forceDebug) {
|
|
|
265
252
|
]);
|
|
266
253
|
return true;
|
|
267
254
|
} catch (validationErr) {
|
|
268
|
-
if (forceDebug) console.log(
|
|
255
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Page validation failed - ${validationErr.message}: ${currentUrl}`));
|
|
269
256
|
return false;
|
|
270
257
|
}
|
|
271
258
|
}
|
|
@@ -275,14 +262,18 @@ async function validatePageForInjection(page, currentUrl, forceDebug) {
|
|
|
275
262
|
*/
|
|
276
263
|
async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl) {
|
|
277
264
|
if (!siteConfig.userAgent) return;
|
|
265
|
+
// Type guard: callers (including config-driven paths) might pass non-string
|
|
266
|
+
// values by accident (e.g. an array, an object). Without this guard, the
|
|
267
|
+
// .toLowerCase() call below would throw and crash the whole spoof
|
|
268
|
+
// pipeline for this URL with no actionable error.
|
|
269
|
+
if (typeof siteConfig.userAgent !== 'string') {
|
|
270
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Invalid userAgent type for ${currentUrl}: expected string, got ${typeof siteConfig.userAgent}`));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
278
273
|
|
|
279
|
-
if (forceDebug) console.log(
|
|
274
|
+
if (forceDebug) console.log(formatLogMessage('debug', `User agent spoofing: ${siteConfig.userAgent}`));
|
|
280
275
|
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
if (!page.browser().isConnected() || page.isClosed()) return;
|
|
284
|
-
if (page.browser().process()?.killed) return;
|
|
285
|
-
} catch { return; }
|
|
276
|
+
if (isBrowserDead(page)) return;
|
|
286
277
|
|
|
287
278
|
// Validate page state before injection
|
|
288
279
|
if (!(await validatePageForInjection(page, currentUrl, forceDebug))) return;
|
|
@@ -294,18 +285,34 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
294
285
|
try {
|
|
295
286
|
await page.setUserAgent(ua);
|
|
296
287
|
} catch (uaErr) {
|
|
297
|
-
if (forceDebug) console.log(
|
|
288
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Could not set user agent - page closed: ${currentUrl}`));
|
|
298
289
|
return;
|
|
299
290
|
}
|
|
300
291
|
|
|
301
|
-
if (forceDebug) console.log(
|
|
292
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Applying stealth protection for ${currentUrl}`));
|
|
302
293
|
|
|
303
294
|
try {
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
295
|
+
// Derive site domain once -- used to seed the per-domain GPU pick
|
|
296
|
+
// (so a tracker logging UNMASKED_RENDERER_WEBGL sees one machine per
|
|
297
|
+
// site) AND to pre-compute hardware-concurrency from the SAME
|
|
298
|
+
// realistic-fingerprint generator that applyFingerprintProtection
|
|
299
|
+
// uses. That kills the order-dependent visible-value bug: previously
|
|
300
|
+
// hardwareConcurrency was random in the UA block (line 819) and
|
|
301
|
+
// domain-seeded in applyFingerprintProtection -- whichever evaluate
|
|
302
|
+
// ran second won. Now both paths produce the same value.
|
|
303
|
+
let siteDomain = '';
|
|
304
|
+
try { siteDomain = new URL(currentUrl).hostname; } catch (_) {}
|
|
305
|
+
|
|
306
|
+
const selectedGpu = selectGpuForUserAgent(ua, siteDomain);
|
|
307
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Selected GPU: ${selectedGpu.vendor} / ${selectedGpu.renderer}`));
|
|
308
|
+
|
|
309
|
+
// Pre-compute hardware-concurrency from the (cached, domain-seeded)
|
|
310
|
+
// realistic fingerprint so the UA-spoof block uses the same value
|
|
311
|
+
// applyFingerprintProtection will later -- no order dependency,
|
|
312
|
+
// same value regardless of which evaluate runs first.
|
|
313
|
+
const realistic = generateRealisticFingerprint(ua, siteDomain);
|
|
314
|
+
|
|
315
|
+
await page.evaluateOnNewDocument((userAgent, debugEnabled, gpuConfig, seededCores) => {
|
|
309
316
|
|
|
310
317
|
// Apply inline error suppression first
|
|
311
318
|
(function() {
|
|
@@ -313,35 +320,53 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
313
320
|
const originalWindowError = window.onerror;
|
|
314
321
|
|
|
315
322
|
function shouldSuppressFingerprintError(message) {
|
|
323
|
+
// Suppression list, ordered specific -> general.
|
|
324
|
+
// .some() stops at first match, so a broken specific just means
|
|
325
|
+
// the general catchers below do the work instead.
|
|
326
|
+
//
|
|
327
|
+
// Fixed (was using `\\.` / `\\d` / `\\(` / `\\$` -- DOUBLE
|
|
328
|
+
// backslashes in source). Each `\\X` parses as
|
|
329
|
+
// literal-backslash + wildcard-X, requiring a literal `\` in
|
|
330
|
+
// the error text -- which never appears. So those entries
|
|
331
|
+
// silently never matched anything; only the general catchers
|
|
332
|
+
// below were actually suppressing those error families.
|
|
333
|
+
// Switched to single-backslash form (literal-X escapes) so
|
|
334
|
+
// each specific entry now matches its intended error class
|
|
335
|
+
// for accurate debug logging of WHICH pattern matched.
|
|
336
|
+
//
|
|
337
|
+
// Also dropped:
|
|
338
|
+
// - /Failed to load resource.*40[34]/i -- fully subsumed by
|
|
339
|
+
// the broader /[45]\d{2}/i below.
|
|
316
340
|
const patterns = [
|
|
317
341
|
/\.closest is not a function/i,
|
|
318
342
|
/\.querySelector is not a function/i,
|
|
319
343
|
/\.addEventListener is not a function/i,
|
|
320
|
-
/Cannot read propert(y|ies) of null
|
|
321
|
-
/Cannot read propert(y|ies) of undefined
|
|
344
|
+
/Cannot read propert(y|ies) of null \(reading 'fp'\)/i,
|
|
345
|
+
/Cannot read propert(y|ies) of undefined \(reading 'fp'\)/i,
|
|
322
346
|
/Cannot redefine property: href/i,
|
|
323
347
|
/Cannot redefine property: __webdriver_script_func/i,
|
|
324
348
|
/Cannot redefine property: webdriver/i,
|
|
325
|
-
/Cannot read propert(y|ies) of undefined
|
|
326
|
-
|
|
349
|
+
/Cannot read propert(y|ies) of undefined \(reading 'toLowerCase'\)/i,
|
|
350
|
+
/\.toLowerCase is not a function/i,
|
|
327
351
|
/fp is not defined/i,
|
|
328
352
|
/fingerprint is not defined/i,
|
|
329
353
|
/FingerprintJS is not defined/i,
|
|
330
|
-
|
|
354
|
+
/\$ is not defined/i,
|
|
331
355
|
/jQuery is not defined/i,
|
|
332
356
|
/_ is not defined/i,
|
|
333
|
-
/Failed to load resource.*server responded with a status of [45]
|
|
357
|
+
/Failed to load resource.*server responded with a status of [45]\d{2}/i,
|
|
334
358
|
/Failed to fetch/i,
|
|
335
359
|
/(webdriver|callPhantom|_phantom|__nightmare|_selenium) is not defined/i,
|
|
336
360
|
/Failed to execute 'observe' on 'IntersectionObserver'.*parameter 1 is not of type 'Element'/i,
|
|
337
361
|
/tz check/i,
|
|
338
|
-
/new window
|
|
339
|
-
/Failed to load resource.*server responded with a status of 40[34]/i,
|
|
362
|
+
/new window\.Error.*<anonymous>/i,
|
|
340
363
|
/Blocked script execution in 'about:blank'.*sandboxed.*allow-scripts/i,
|
|
341
364
|
/Page JavaScript error:/i,
|
|
342
365
|
/^[a-zA-Z0-9_$]+\[.*\]\s+is not a function/i,
|
|
343
366
|
/^[a-zA-Z0-9_$]+\(.*\)\s+is not a function/i,
|
|
344
367
|
/^[a-zA-Z0-9_$]+\.[a-zA-Z0-9_$]+.*is not a function/i,
|
|
368
|
+
// General catchers — kept last so the specific entries
|
|
369
|
+
// above can match first for accurate debug attribution.
|
|
345
370
|
/Failed to load resource/i,
|
|
346
371
|
/is not defined/i,
|
|
347
372
|
/is not a function/i
|
|
@@ -381,6 +406,30 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
381
406
|
}
|
|
382
407
|
return fn;
|
|
383
408
|
}
|
|
409
|
+
|
|
410
|
+
// Wrapper-constructor identity preserver. Function.name and
|
|
411
|
+
// Function.length are own data properties of every function
|
|
412
|
+
// object, NOT inherited via prototype chain -- so even after
|
|
413
|
+
// Object.setPrototypeOf(wrapper, OriginalCtor), the wrapper
|
|
414
|
+
// still has its own name (empty for anonymous expressions) and
|
|
415
|
+
// length (count of leading formal params). A bot detector that
|
|
416
|
+
// checks `Error.name === 'Error'` would see '' on our spoof.
|
|
417
|
+
//
|
|
418
|
+
// Real Chrome's Function.name has descriptor
|
|
419
|
+
// {value: ..., writable: false, enumerable: false, configurable: true}
|
|
420
|
+
// -- match that shape so getOwnPropertyDescriptor checks pass too.
|
|
421
|
+
function preserveCtorIdentity(wrapper, original) {
|
|
422
|
+
try {
|
|
423
|
+
Object.defineProperty(wrapper, 'name', {
|
|
424
|
+
value: original.name, writable: false, enumerable: false, configurable: true
|
|
425
|
+
});
|
|
426
|
+
Object.defineProperty(wrapper, 'length', {
|
|
427
|
+
value: original.length, writable: false, enumerable: false, configurable: true
|
|
428
|
+
});
|
|
429
|
+
} catch (e) {
|
|
430
|
+
if (debugEnabled) console.log(`[fingerprint] preserveCtorIdentity failed: ${e.message}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
384
433
|
|
|
385
434
|
Function.prototype.toString = function() {
|
|
386
435
|
if (nativeFunctionStore.has(this)) {
|
|
@@ -391,10 +440,14 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
391
440
|
// Protect the toString override itself
|
|
392
441
|
nativeFunctionStore.set(Function.prototype.toString, 'toString');
|
|
393
442
|
|
|
394
|
-
// Create safe property definition helper
|
|
443
|
+
// Create safe property definition helper.
|
|
444
|
+
// configurable: true is a DEFAULT (not a force) -- if a caller
|
|
445
|
+
// wants to lock a property (configurable: false) the spread won't
|
|
446
|
+
// override their explicit value. Previously `{ ...descriptor,
|
|
447
|
+
// configurable: true }` silently overrode any caller intent.
|
|
395
448
|
function safeDefinePropertyLocal(target, property, descriptor) {
|
|
396
449
|
const builtInProps = new Set(['href', 'origin', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash', 'constructor', 'prototype', '__proto__', 'toString', 'valueOf', 'assign', 'reload', 'replace']);
|
|
397
|
-
|
|
450
|
+
|
|
398
451
|
if (builtInProps.has(property)) {
|
|
399
452
|
if (debugEnabled) console.log(`[fingerprint] Skipping built-in property: ${property}`);
|
|
400
453
|
return false;
|
|
@@ -408,8 +461,8 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
408
461
|
}
|
|
409
462
|
|
|
410
463
|
Object.defineProperty(target, property, {
|
|
411
|
-
|
|
412
|
-
|
|
464
|
+
configurable: true,
|
|
465
|
+
...descriptor
|
|
413
466
|
});
|
|
414
467
|
return true;
|
|
415
468
|
} catch (err) {
|
|
@@ -471,13 +524,21 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
471
524
|
'$cdc_asdjflasutopfhvcZLmcfl_', '$chrome_asyncScriptInfo', '__$webdriverAsyncExecutor'
|
|
472
525
|
];
|
|
473
526
|
|
|
527
|
+
// Just delete -- DO NOT re-add as undefined-returning getters.
|
|
528
|
+
// The previous version did:
|
|
529
|
+
// delete window[prop];
|
|
530
|
+
// safeDefinePropertyLocal(window, prop, { get: () => undefined });
|
|
531
|
+
// which made `window.callPhantom` return undefined (good) but ALSO
|
|
532
|
+
// made `'callPhantom' in window` return TRUE (bad) -- the property
|
|
533
|
+
// exists on the object even if the getter returns undefined. Real
|
|
534
|
+
// Chrome doesn't have these props at all, so `in`-operator and
|
|
535
|
+
// Object.getOwnPropertyNames probes saw a present property and
|
|
536
|
+
// flagged us as a bot. This own-goal was caught by
|
|
537
|
+
// scripts/test-stealth.js sannysoft (PHANTOM_PROPERTIES and
|
|
538
|
+
// SELENIUM_DRIVER cells went red because of it).
|
|
474
539
|
automationProps.forEach(prop => {
|
|
475
|
-
try {
|
|
476
|
-
|
|
477
|
-
delete navigator[prop];
|
|
478
|
-
safeDefinePropertyLocal(window, prop, { get: () => undefined });
|
|
479
|
-
safeDefinePropertyLocal(navigator, prop, { get: () => undefined });
|
|
480
|
-
} catch (e) {}
|
|
540
|
+
try { delete window[prop]; } catch (e) {}
|
|
541
|
+
try { delete navigator[prop]; } catch (e) {}
|
|
481
542
|
});
|
|
482
543
|
}, 'automation properties removal');
|
|
483
544
|
|
|
@@ -499,12 +560,21 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
499
560
|
postMessage: () => {},
|
|
500
561
|
disconnect: () => {}
|
|
501
562
|
}),
|
|
502
|
-
getManifest: () =>
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
563
|
+
getManifest: () => {
|
|
564
|
+
// Derive Chrome version from the spoofed UA so a tracker
|
|
565
|
+
// cross-checking navigator.userAgent's Chrome version
|
|
566
|
+
// against chrome.runtime.getManifest().version sees a
|
|
567
|
+
// consistent number. Was hardcoded "146.0.0.0" which lied
|
|
568
|
+
// any time the UA was rotated to a different Chrome major.
|
|
569
|
+
const m = userAgent.match(/Chrome\/(\d+)/);
|
|
570
|
+
const major = m ? m[1] : '146';
|
|
571
|
+
return {
|
|
572
|
+
name: "Chrome",
|
|
573
|
+
version: `${major}.0.0.0`,
|
|
574
|
+
manifest_version: 3,
|
|
575
|
+
description: "Chrome Browser"
|
|
576
|
+
};
|
|
577
|
+
},
|
|
508
578
|
getURL: (path) => `chrome-extension://invalid/${path}`,
|
|
509
579
|
id: undefined,
|
|
510
580
|
getPlatformInfo: (callback) => callback({
|
|
@@ -569,11 +639,22 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
569
639
|
});
|
|
570
640
|
}
|
|
571
641
|
|
|
572
|
-
//
|
|
642
|
+
// The old comment claimed "non-enumerable to match real Chrome" --
|
|
643
|
+
// but real Chrome's window.chrome property descriptor is
|
|
644
|
+
// {value: ..., writable: true, enumerable: true, configurable: true}.
|
|
645
|
+
// The previous writable:false + enumerable:false were themselves
|
|
646
|
+
// fingerprintable tells: a bot detector reading
|
|
647
|
+
// Object.getOwnPropertyDescriptor(window, 'chrome')
|
|
648
|
+
// would see {writable:false, enumerable:false} and know this
|
|
649
|
+
// isn't real Chrome. `window.chrome = 'x'` would also silently
|
|
650
|
+
// fail (vs succeed on real Chrome). Match real Chrome's
|
|
651
|
+
// descriptor instead. The defineProperty is kept (rather than
|
|
652
|
+
// removed entirely) so re-injection on reload doesn't lose the
|
|
653
|
+
// descriptor shape if anything earlier tightened it.
|
|
573
654
|
Object.defineProperty(window, 'chrome', {
|
|
574
655
|
value: window.chrome,
|
|
575
|
-
writable:
|
|
576
|
-
enumerable:
|
|
656
|
+
writable: true,
|
|
657
|
+
enumerable: true,
|
|
577
658
|
configurable: true
|
|
578
659
|
});
|
|
579
660
|
|
|
@@ -647,6 +728,30 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
647
728
|
// Safari typically has no plugins in modern versions
|
|
648
729
|
plugins = [];
|
|
649
730
|
}
|
|
731
|
+
// Each plugin object needs to identify as a Plugin instance so
|
|
732
|
+
// `navigator.plugins[i].toString() === '[object Plugin]'` passes.
|
|
733
|
+
// (Sannysoft's actual PluginArray check tests this -- found via
|
|
734
|
+
// scripts/test-stealth.js sannysoft.) Resolve Plugin.prototype the
|
|
735
|
+
// same way we resolve PluginArray.prototype below: prefer the
|
|
736
|
+
// global, fall back to navigator.plugins[0]'s prototype if a real
|
|
737
|
+
// plugin exists, fall back to just Symbol.toStringTag if neither.
|
|
738
|
+
let pluginProto = null;
|
|
739
|
+
try {
|
|
740
|
+
if (typeof Plugin !== 'undefined' && Plugin.prototype) {
|
|
741
|
+
pluginProto = Plugin.prototype;
|
|
742
|
+
} else if (navigator.plugins && navigator.plugins[0]) {
|
|
743
|
+
pluginProto = Object.getPrototypeOf(navigator.plugins[0]);
|
|
744
|
+
}
|
|
745
|
+
} catch (e) {}
|
|
746
|
+
plugins = plugins.map(p => {
|
|
747
|
+
const wrapped = Object.assign(Object.create(pluginProto || Object.prototype), p);
|
|
748
|
+
if (!pluginProto) {
|
|
749
|
+
// Last-ditch: at least toString() returns "[object Plugin]"
|
|
750
|
+
wrapped[Symbol.toStringTag] = 'Plugin';
|
|
751
|
+
}
|
|
752
|
+
return wrapped;
|
|
753
|
+
});
|
|
754
|
+
|
|
650
755
|
// Create proper array-like object with enumerable indices and length
|
|
651
756
|
// Create proper PluginArray-like object with required methods
|
|
652
757
|
const pluginsArray = {};
|
|
@@ -669,6 +774,27 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
669
774
|
pluginsArray[Symbol.iterator] = function*() { for (const p of plugins) yield p; };
|
|
670
775
|
pluginsArray[Symbol.toStringTag] = 'PluginArray';
|
|
671
776
|
|
|
777
|
+
// Make `navigator.plugins instanceof PluginArray` evaluate true.
|
|
778
|
+
// Multiple ways to get PluginArray.prototype, in order of
|
|
779
|
+
// preference -- evaluateOnNewDocument can fire before all globals
|
|
780
|
+
// are bound, so we don't assume `PluginArray` is in scope. Falls
|
|
781
|
+
// back to inheriting from the existing navigator.plugins's own
|
|
782
|
+
// prototype, which is ALWAYS a real PluginArray.prototype on
|
|
783
|
+
// any DOM-bearing context.
|
|
784
|
+
try {
|
|
785
|
+
let pluginArrayProto = null;
|
|
786
|
+
if (typeof PluginArray !== 'undefined' && PluginArray.prototype) {
|
|
787
|
+
pluginArrayProto = PluginArray.prototype;
|
|
788
|
+
} else if (navigator.plugins) {
|
|
789
|
+
pluginArrayProto = Object.getPrototypeOf(navigator.plugins);
|
|
790
|
+
}
|
|
791
|
+
if (pluginArrayProto && pluginArrayProto !== Object.prototype) {
|
|
792
|
+
Object.setPrototypeOf(pluginsArray, pluginArrayProto);
|
|
793
|
+
}
|
|
794
|
+
} catch (e) {
|
|
795
|
+
if (debugEnabled) console.log(`[fingerprint] PluginArray prototype setup failed: ${e.message}`);
|
|
796
|
+
}
|
|
797
|
+
|
|
672
798
|
safeDefinePropertyLocal(navigator, 'plugins', { get: () => pluginsArray });
|
|
673
799
|
}, 'plugins spoofing');
|
|
674
800
|
|
|
@@ -811,12 +937,17 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
811
937
|
}
|
|
812
938
|
}, 'enhanced OS fingerprinting protection');
|
|
813
939
|
|
|
814
|
-
// Hardware concurrency spoofing (universal coverage)
|
|
815
|
-
//
|
|
940
|
+
// Hardware concurrency spoofing (universal coverage).
|
|
941
|
+
// Uses the domain-seeded value passed in from the Node-side
|
|
942
|
+
// generateRealisticFingerprint call -- previously did its own
|
|
943
|
+
// Math.random() pick from [4,6,8,12], which conflicted with the
|
|
944
|
+
// domain-seeded value set later by applyFingerprintProtection.
|
|
945
|
+
// Whichever evaluate ran second won, producing an order-dependent
|
|
946
|
+
// visible value. Now both paths use the same seeded value, so
|
|
947
|
+
// double-spoof becomes idempotent.
|
|
816
948
|
safeExecute(() => {
|
|
817
|
-
const spoofedCores = [4, 6, 8, 12][Math.floor(Math.random() * 4)];
|
|
818
949
|
const hardwareProps = {
|
|
819
|
-
hardwareConcurrency: { get: () =>
|
|
950
|
+
hardwareConcurrency: { get: () => seededCores }
|
|
820
951
|
};
|
|
821
952
|
spoofNavigatorProperties(navigator, hardwareProps);
|
|
822
953
|
}, 'hardware concurrency spoofing');
|
|
@@ -887,12 +1018,24 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
887
1018
|
|
|
888
1019
|
window.Error.prototype = OriginalError.prototype;
|
|
889
1020
|
Object.setPrototypeOf(window.Error, OriginalError);
|
|
1021
|
+
preserveCtorIdentity(window.Error, OriginalError);
|
|
890
1022
|
|
|
891
|
-
//
|
|
1023
|
+
// Forward static properties via getter/setter pairs instead of
|
|
1024
|
+
// value-copy, so live mutations to OriginalError (e.g. page
|
|
1025
|
+
// code doing `Error.stackTraceLimit = 100`) propagate through
|
|
1026
|
+
// the wrapper. Previously the value-copy froze a snapshot at
|
|
1027
|
+
// injection time; a tracker that mutated stackTraceLimit and
|
|
1028
|
+
// then read it back from the wrapped Error would see the old
|
|
1029
|
+
// value -- a real fingerprint tell.
|
|
892
1030
|
['captureStackTrace', 'stackTraceLimit', 'prepareStackTrace'].forEach(prop => {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1031
|
+
try {
|
|
1032
|
+
Object.defineProperty(window.Error, prop, {
|
|
1033
|
+
get: () => OriginalError[prop],
|
|
1034
|
+
set: (v) => { OriginalError[prop] = v; },
|
|
1035
|
+
configurable: true,
|
|
1036
|
+
enumerable: false
|
|
1037
|
+
});
|
|
1038
|
+
} catch (e) {}
|
|
896
1039
|
});
|
|
897
1040
|
}, 'Error stack protection');
|
|
898
1041
|
|
|
@@ -1159,19 +1302,25 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1159
1302
|
}
|
|
1160
1303
|
}, 'window dimension spoofing');
|
|
1161
1304
|
|
|
1162
|
-
// navigator.connection — missing or incomplete in headless
|
|
1305
|
+
// navigator.connection — missing or incomplete in headless.
|
|
1306
|
+
// Object literal hoisted out of the getter so identity is stable
|
|
1307
|
+
// across reads. Real Chrome's NetworkInformation instance has
|
|
1308
|
+
// `navigator.connection === navigator.connection`; previously
|
|
1309
|
+
// the getter returned a new object on every read, which a
|
|
1310
|
+
// tracker could detect by comparing references.
|
|
1163
1311
|
safeExecute(() => {
|
|
1164
1312
|
if (!navigator.connection) {
|
|
1313
|
+
const connectionInfoStable = {
|
|
1314
|
+
effectiveType: '4g',
|
|
1315
|
+
rtt: 50,
|
|
1316
|
+
downlink: 10,
|
|
1317
|
+
saveData: false,
|
|
1318
|
+
type: 'wifi',
|
|
1319
|
+
addEventListener: () => {},
|
|
1320
|
+
removeEventListener: () => {}
|
|
1321
|
+
};
|
|
1165
1322
|
Object.defineProperty(navigator, 'connection', {
|
|
1166
|
-
get: () =>
|
|
1167
|
-
effectiveType: '4g',
|
|
1168
|
-
rtt: 50,
|
|
1169
|
-
downlink: 10,
|
|
1170
|
-
saveData: false,
|
|
1171
|
-
type: 'wifi',
|
|
1172
|
-
addEventListener: () => {},
|
|
1173
|
-
removeEventListener: () => {}
|
|
1174
|
-
})
|
|
1323
|
+
get: () => connectionInfoStable
|
|
1175
1324
|
});
|
|
1176
1325
|
}
|
|
1177
1326
|
}, 'connection API spoofing');
|
|
@@ -1252,7 +1401,10 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1252
1401
|
return img;
|
|
1253
1402
|
};
|
|
1254
1403
|
window.Image.prototype = OrigImage.prototype;
|
|
1255
|
-
|
|
1404
|
+
preserveCtorIdentity(window.Image, OrigImage);
|
|
1405
|
+
// (Note: the prior Object.defineProperty(window, 'Image', ...) here
|
|
1406
|
+
// was a no-op -- changed descriptor flags without setting value.
|
|
1407
|
+
// Removed; the wrapper is already assigned to window.Image above.)
|
|
1256
1408
|
}, 'broken image dimension spoofing');
|
|
1257
1409
|
|
|
1258
1410
|
// Fetch Request Headers Normalization
|
|
@@ -1343,6 +1495,7 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1343
1495
|
};
|
|
1344
1496
|
Object.setPrototypeOf(window.RTCPeerConnection, OriginalRTC);
|
|
1345
1497
|
window.RTCPeerConnection.prototype = OriginalRTC.prototype;
|
|
1498
|
+
preserveCtorIdentity(window.RTCPeerConnection, OriginalRTC);
|
|
1346
1499
|
}
|
|
1347
1500
|
}, 'WebRTC spoofing');
|
|
1348
1501
|
|
|
@@ -1585,8 +1738,9 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1585
1738
|
return new OriginalPointerEvent(type, enhancedDict);
|
|
1586
1739
|
};
|
|
1587
1740
|
Object.setPrototypeOf(window.PointerEvent, OriginalPointerEvent);
|
|
1741
|
+
preserveCtorIdentity(window.PointerEvent, OriginalPointerEvent);
|
|
1588
1742
|
}
|
|
1589
|
-
|
|
1743
|
+
|
|
1590
1744
|
// Spoof touch capabilities for mobile detection evasion
|
|
1591
1745
|
if (!window.TouchEvent && Math.random() > 0.8) {
|
|
1592
1746
|
// 20% chance to add touch support to confuse fingerprinters
|
|
@@ -1610,6 +1764,8 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1610
1764
|
};
|
|
1611
1765
|
return new originalWheelEvent(type, enhancedDict);
|
|
1612
1766
|
};
|
|
1767
|
+
Object.setPrototypeOf(window.WheelEvent, originalWheelEvent);
|
|
1768
|
+
preserveCtorIdentity(window.WheelEvent, originalWheelEvent);
|
|
1613
1769
|
}
|
|
1614
1770
|
|
|
1615
1771
|
}, 'enhanced mouse/pointer spoofing');
|
|
@@ -1742,10 +1898,10 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1742
1898
|
}
|
|
1743
1899
|
}, 'interaction-gated script trigger');
|
|
1744
1900
|
|
|
1745
|
-
}, ua, forceDebug, selectedGpu);
|
|
1901
|
+
}, ua, forceDebug, selectedGpu, realistic.hardwareConcurrency);
|
|
1746
1902
|
} catch (stealthErr) {
|
|
1747
1903
|
if (isSessionClosedError(stealthErr)) {
|
|
1748
|
-
if (forceDebug) console.log(
|
|
1904
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Page closed during stealth injection: ${currentUrl}`));
|
|
1749
1905
|
return;
|
|
1750
1906
|
}
|
|
1751
1907
|
console.warn(`[stealth protection failed] ${currentUrl}: ${stealthErr.message}`);
|
|
@@ -1759,13 +1915,9 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1759
1915
|
async function applyBraveSpoofing(page, siteConfig, forceDebug, currentUrl) {
|
|
1760
1916
|
if (!siteConfig.isBrave) return;
|
|
1761
1917
|
|
|
1762
|
-
if (forceDebug) console.log(
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
try {
|
|
1766
|
-
if (!page.browser().isConnected() || page.isClosed()) return;
|
|
1767
|
-
if (page.browser().process()?.killed) return;
|
|
1768
|
-
} catch { return; }
|
|
1918
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Brave spoofing enabled for ${currentUrl}`));
|
|
1919
|
+
|
|
1920
|
+
if (isBrowserDead(page)) return;
|
|
1769
1921
|
|
|
1770
1922
|
// Validate page state before injection
|
|
1771
1923
|
if (!(await validatePageForInjection(page, currentUrl, forceDebug))) return;
|
|
@@ -1797,10 +1949,10 @@ async function applyBraveSpoofing(page, siteConfig, forceDebug, currentUrl) {
|
|
|
1797
1949
|
}, forceDebug);
|
|
1798
1950
|
} catch (braveErr) {
|
|
1799
1951
|
if (isSessionClosedError(braveErr)) {
|
|
1800
|
-
if (forceDebug) console.log(
|
|
1952
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Page closed during Brave injection: ${currentUrl}`));
|
|
1801
1953
|
return;
|
|
1802
1954
|
}
|
|
1803
|
-
if (forceDebug) console.log(
|
|
1955
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Brave spoofing failed: ${currentUrl} - ${braveErr.message}`));
|
|
1804
1956
|
}
|
|
1805
1957
|
}
|
|
1806
1958
|
|
|
@@ -1811,13 +1963,9 @@ async function applyFingerprintProtection(page, siteConfig, forceDebug, currentU
|
|
|
1811
1963
|
const fingerprintSetting = siteConfig.fingerprint_protection;
|
|
1812
1964
|
if (!fingerprintSetting) return;
|
|
1813
1965
|
|
|
1814
|
-
if (forceDebug) console.log(
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
try {
|
|
1818
|
-
if (!page.browser().isConnected() || page.isClosed()) return;
|
|
1819
|
-
if (page.browser().process()?.killed) return;
|
|
1820
|
-
} catch { return; }
|
|
1966
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Fingerprint protection enabled for ${currentUrl}`));
|
|
1967
|
+
|
|
1968
|
+
if (isBrowserDead(page)) return;
|
|
1821
1969
|
|
|
1822
1970
|
// Validate page state before injection
|
|
1823
1971
|
if (!(await validatePageForInjection(page, currentUrl, forceDebug))) return;
|
|
@@ -1827,7 +1975,7 @@ async function applyFingerprintProtection(page, siteConfig, forceDebug, currentU
|
|
|
1827
1975
|
try {
|
|
1828
1976
|
currentUserAgent = await page.evaluate(() => navigator.userAgent);
|
|
1829
1977
|
} catch (evalErr) {
|
|
1830
|
-
if (forceDebug) console.log(
|
|
1978
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Could not get user agent - page closed: ${currentUrl}`));
|
|
1831
1979
|
return;
|
|
1832
1980
|
}
|
|
1833
1981
|
|
|
@@ -1862,14 +2010,30 @@ async function applyFingerprintProtection(page, siteConfig, forceDebug, currentU
|
|
|
1862
2010
|
}
|
|
1863
2011
|
}
|
|
1864
2012
|
|
|
2013
|
+
// Mirror of the helper defined inside applyUserAgentSpoofing's
|
|
2014
|
+
// evaluate -- both behave identically: skip built-ins, refuse to
|
|
2015
|
+
// touch non-configurable, default (not force) configurable:true so
|
|
2016
|
+
// explicit caller intent is preserved. Previously this copy
|
|
2017
|
+
// diverged: it had no built-in check AND it force-overrode
|
|
2018
|
+
// configurable:true regardless of caller. Now consistent.
|
|
1865
2019
|
function safeDefinePropertyLocal(target, property, descriptor) {
|
|
2020
|
+
const builtInProps = new Set(['href', 'origin', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash', 'constructor', 'prototype', '__proto__', 'toString', 'valueOf', 'assign', 'reload', 'replace']);
|
|
2021
|
+
|
|
2022
|
+
if (builtInProps.has(property)) {
|
|
2023
|
+
if (debugEnabled) console.log(`[fingerprint] Skipping built-in property: ${property}`);
|
|
2024
|
+
return false;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1866
2027
|
try {
|
|
1867
2028
|
const existing = Object.getOwnPropertyDescriptor(target, property);
|
|
1868
|
-
if (existing?.configurable === false)
|
|
1869
|
-
|
|
2029
|
+
if (existing?.configurable === false) {
|
|
2030
|
+
if (debugEnabled) console.log(`[fingerprint] Cannot modify non-configurable: ${property}`);
|
|
2031
|
+
return false;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
1870
2034
|
Object.defineProperty(target, property, {
|
|
1871
|
-
|
|
1872
|
-
|
|
2035
|
+
configurable: true,
|
|
2036
|
+
...descriptor
|
|
1873
2037
|
});
|
|
1874
2038
|
return true;
|
|
1875
2039
|
} catch (err) {
|
|
@@ -1960,7 +2124,7 @@ async function applyFingerprintProtection(page, siteConfig, forceDebug, currentU
|
|
|
1960
2124
|
}, { spoof, debugEnabled: forceDebug });
|
|
1961
2125
|
} catch (err) {
|
|
1962
2126
|
if (isSessionClosedError(err)) {
|
|
1963
|
-
if (forceDebug) console.log(
|
|
2127
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Page closed during fingerprint injection: ${currentUrl}`));
|
|
1964
2128
|
return;
|
|
1965
2129
|
}
|
|
1966
2130
|
console.warn(`[fingerprint protection failed] ${currentUrl}: ${err.message}`);
|
|
@@ -1975,13 +2139,13 @@ async function simulateHumanBehavior(page, forceDebug) {
|
|
|
1975
2139
|
try {
|
|
1976
2140
|
// Validate page state before injection
|
|
1977
2141
|
if (!page || page.isClosed()) {
|
|
1978
|
-
if (forceDebug) console.log(
|
|
2142
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Human behavior simulation skipped - page closed`));
|
|
1979
2143
|
return;
|
|
1980
2144
|
}
|
|
1981
2145
|
|
|
1982
2146
|
// Check if browser is still connected
|
|
1983
|
-
if (!page.browser().
|
|
1984
|
-
if (forceDebug) console.log(
|
|
2147
|
+
if (!page.browser().connected) {
|
|
2148
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Human behavior simulation skipped - browser disconnected`));
|
|
1985
2149
|
return;
|
|
1986
2150
|
}
|
|
1987
2151
|
|
|
@@ -2093,7 +2257,7 @@ async function simulateHumanBehavior(page, forceDebug) {
|
|
|
2093
2257
|
|
|
2094
2258
|
}, forceDebug);
|
|
2095
2259
|
} catch (err) {
|
|
2096
|
-
if (forceDebug) console.log(
|
|
2260
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Human behavior simulation setup failed: ${err.message}`));
|
|
2097
2261
|
}
|
|
2098
2262
|
}
|
|
2099
2263
|
|
|
@@ -2112,7 +2276,7 @@ async function applyAllFingerprintSpoofing(page, siteConfig, forceDebug, current
|
|
|
2112
2276
|
try {
|
|
2113
2277
|
await fn(page, siteConfig, forceDebug, currentUrl);
|
|
2114
2278
|
} catch (err) {
|
|
2115
|
-
if (forceDebug) console.log(
|
|
2279
|
+
if (forceDebug) console.log(formatLogMessage('debug', `${name} failed for ${currentUrl}: ${err.message}`));
|
|
2116
2280
|
}
|
|
2117
2281
|
}
|
|
2118
2282
|
|
|
@@ -2121,21 +2285,22 @@ async function applyAllFingerprintSpoofing(page, siteConfig, forceDebug, current
|
|
|
2121
2285
|
try {
|
|
2122
2286
|
await simulateHumanBehavior(page, forceDebug);
|
|
2123
2287
|
} catch (behaviorErr) {
|
|
2124
|
-
if (forceDebug) console.log(
|
|
2288
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Human behavior simulation failed for ${currentUrl}: ${behaviorErr.message}`));
|
|
2125
2289
|
}
|
|
2126
2290
|
}
|
|
2127
2291
|
}
|
|
2128
2292
|
|
|
2293
|
+
// Public surface kept narrow on purpose -- only what nwss.js actually
|
|
2294
|
+
// imports. Internal helpers (generateRealisticFingerprint,
|
|
2295
|
+
// applyUserAgentSpoofing, applyBraveSpoofing, applyFingerprintProtection,
|
|
2296
|
+
// simulateHumanBehavior, selectGpuForUserAgent, isBrowserDead,
|
|
2297
|
+
// isSessionClosedError, validatePageForInjection, seededRandom,
|
|
2298
|
+
// DEFAULT_PLATFORM, DEFAULT_TIMEZONE) stay as module-local; move back to
|
|
2299
|
+
// module.exports only if a new external consumer appears.
|
|
2129
2300
|
module.exports = {
|
|
2130
|
-
generateRealisticFingerprint,
|
|
2131
|
-
getRealisticScreenResolution,
|
|
2132
|
-
applyUserAgentSpoofing,
|
|
2133
|
-
applyBraveSpoofing,
|
|
2134
|
-
applyFingerprintProtection,
|
|
2135
2301
|
applyAllFingerprintSpoofing,
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
DEFAULT_TIMEZONE
|
|
2302
|
+
// Exposed for scripts/test-stealth.js so the harness can validate --ua=
|
|
2303
|
+
// against the canonical UA list (instead of duplicating the keys here).
|
|
2304
|
+
// The Map itself is frozen; consumers cannot mutate the spoof source.
|
|
2305
|
+
USER_AGENT_COLLECTIONS
|
|
2141
2306
|
};
|