@fanboynz/network-scanner 2.0.51 → 2.0.53
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/lib/fingerprint.js +510 -157
- package/nwss.js +62 -24
- package/package.json +1 -1
package/lib/fingerprint.js
CHANGED
|
@@ -54,9 +54,9 @@ const BUILT_IN_PROPERTIES = new Set([
|
|
|
54
54
|
|
|
55
55
|
// User agent collections with latest versions
|
|
56
56
|
const USER_AGENT_COLLECTIONS = Object.freeze(new Map([
|
|
57
|
-
['chrome', "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
58
|
-
['chrome_mac', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
59
|
-
['chrome_linux', "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
57
|
+
['chrome', "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"],
|
|
58
|
+
['chrome_mac', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"],
|
|
59
|
+
['chrome_linux', "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"],
|
|
60
60
|
['firefox', "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0"],
|
|
61
61
|
['firefox_mac', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0"],
|
|
62
62
|
['firefox_linux', "Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0"],
|
|
@@ -64,6 +64,50 @@ const USER_AGENT_COLLECTIONS = Object.freeze(new Map([
|
|
|
64
64
|
]));
|
|
65
65
|
|
|
66
66
|
// Timezone configuration with offsets
|
|
67
|
+
|
|
68
|
+
// GPU pool — realistic vendor/renderer combos per OS (used for WebGL spoofing)
|
|
69
|
+
const GPU_POOL = {
|
|
70
|
+
windows: [
|
|
71
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
72
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (Intel(R) UHD Graphics 770 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
73
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (Intel(R) Iris(R) Xe Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
74
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (NVIDIA GeForce GTX 1650 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
75
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (NVIDIA GeForce GTX 1060 6GB Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
76
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
77
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (NVIDIA GeForce RTX 4060 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
78
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (AMD Radeon RX 580 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
79
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (AMD Radeon(TM) Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
80
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (AMD Radeon RX 6600 XT Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
81
|
+
],
|
|
82
|
+
mac: [
|
|
83
|
+
{ vendor: 'Apple', renderer: 'Apple M1' },
|
|
84
|
+
{ vendor: 'Apple', renderer: 'Apple M1 Pro' },
|
|
85
|
+
{ vendor: 'Apple', renderer: 'Apple M2' },
|
|
86
|
+
{ vendor: 'Apple', renderer: 'Apple M3' },
|
|
87
|
+
{ vendor: 'Intel Inc.', renderer: 'Intel(R) UHD Graphics 630' },
|
|
88
|
+
{ vendor: 'Intel Inc.', renderer: 'Intel(R) Iris(TM) Plus Graphics 655' },
|
|
89
|
+
{ vendor: 'Intel Inc.', renderer: 'Intel(R) Iris(TM) Plus Graphics' },
|
|
90
|
+
],
|
|
91
|
+
linux: [
|
|
92
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (Intel(R) UHD Graphics 630, Mesa 23.2.1, OpenGL 4.6)' },
|
|
93
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (Intel(R) UHD Graphics 770, Mesa 24.0.3, OpenGL 4.6)' },
|
|
94
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (NVIDIA GeForce GTX 1080, NVIDIA 535.183.01, OpenGL 4.6.0)' },
|
|
95
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (NVIDIA GeForce RTX 3070, NVIDIA 545.29.06, OpenGL 4.6.0)' },
|
|
96
|
+
{ vendor: 'Google Inc. (ANGLE)', renderer: 'ANGLE (AMD Radeon RX 580, Mesa 23.2.1, OpenGL 4.6)' },
|
|
97
|
+
]
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Select a GPU from the pool based on user agent string.
|
|
102
|
+
* Called once per browser session so the GPU stays consistent across page loads.
|
|
103
|
+
*/
|
|
104
|
+
function selectGpuForUserAgent(userAgentString) {
|
|
105
|
+
let osKey = 'windows';
|
|
106
|
+
if (userAgentString && (userAgentString.includes('Macintosh') || userAgentString.includes('Mac OS X'))) osKey = 'mac';
|
|
107
|
+
else if (userAgentString && (userAgentString.includes('X11; Linux') || userAgentString.includes('Ubuntu'))) osKey = 'linux';
|
|
108
|
+
const pool = GPU_POOL[osKey];
|
|
109
|
+
return pool[Math.floor(Math.random() * pool.length)];
|
|
110
|
+
}
|
|
67
111
|
const TIMEZONE_CONFIG = {
|
|
68
112
|
'America/New_York': { offset: 300, abbr: 'EST' },
|
|
69
113
|
'America/Los_Angeles': { offset: 480, abbr: 'PST' },
|
|
@@ -267,7 +311,11 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
267
311
|
if (forceDebug) console.log(`[debug] Applying stealth protection for ${currentUrl}`);
|
|
268
312
|
|
|
269
313
|
try {
|
|
270
|
-
|
|
314
|
+
// Select GPU once per session — stays consistent across all page loads
|
|
315
|
+
const selectedGpu = selectGpuForUserAgent(ua);
|
|
316
|
+
if (forceDebug) console.log(`[debug] Selected GPU: ${selectedGpu.vendor} / ${selectedGpu.renderer}`);
|
|
317
|
+
|
|
318
|
+
await page.evaluateOnNewDocument((userAgent, debugEnabled, gpuConfig) => {
|
|
271
319
|
|
|
272
320
|
// Apply inline error suppression first
|
|
273
321
|
(function() {
|
|
@@ -394,7 +442,7 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
394
442
|
try {
|
|
395
443
|
delete navigator.webdriver;
|
|
396
444
|
} catch (e) {}
|
|
397
|
-
safeDefinePropertyLocal(navigator, 'webdriver', { get: () =>
|
|
445
|
+
safeDefinePropertyLocal(navigator, 'webdriver', { get: () => false });
|
|
398
446
|
}, 'webdriver removal');
|
|
399
447
|
|
|
400
448
|
// Remove automation properties
|
|
@@ -427,83 +475,105 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
427
475
|
//
|
|
428
476
|
safeExecute(() => {
|
|
429
477
|
if (!window.chrome) {
|
|
430
|
-
window.chrome = {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
get: (keys, callback) => callback && callback({}),
|
|
458
|
-
set: (items, callback) => callback && callback(),
|
|
459
|
-
remove: (keys, callback) => callback && callback(),
|
|
460
|
-
clear: (callback) => callback && callback()
|
|
461
|
-
},
|
|
462
|
-
sync: {
|
|
463
|
-
get: (keys, callback) => callback && callback({}),
|
|
464
|
-
set: (items, callback) => callback && callback(),
|
|
465
|
-
remove: (keys, callback) => callback && callback(),
|
|
466
|
-
clear: (callback) => callback && callback()
|
|
467
|
-
}
|
|
468
|
-
},
|
|
469
|
-
loadTimes: () => {
|
|
470
|
-
const now = performance.now();
|
|
471
|
-
return {
|
|
472
|
-
commitLoadTime: now - Math.random() * 1000,
|
|
473
|
-
connectionInfo: 'http/1.1',
|
|
474
|
-
finishDocumentLoadTime: now - Math.random() * 500,
|
|
475
|
-
finishLoadTime: now - Math.random() * 100,
|
|
476
|
-
navigationType: 'Navigation'
|
|
477
|
-
};
|
|
478
|
-
},
|
|
479
|
-
csi: () => ({
|
|
480
|
-
onloadT: Date.now(),
|
|
481
|
-
pageT: Math.random() * 1000,
|
|
482
|
-
startE: Date.now() - Math.random() * 2000
|
|
478
|
+
window.chrome = {};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Add runtime if missing — headless Chrome has chrome object but no runtime
|
|
482
|
+
if (!window.chrome.runtime) {
|
|
483
|
+
window.chrome.runtime = {
|
|
484
|
+
onConnect: { addListener: () => {}, removeListener: () => {} },
|
|
485
|
+
onMessage: { addListener: () => {}, removeListener: () => {} },
|
|
486
|
+
sendMessage: () => {},
|
|
487
|
+
connect: () => ({
|
|
488
|
+
onMessage: { addListener: () => {}, removeListener: () => {} },
|
|
489
|
+
postMessage: () => {},
|
|
490
|
+
disconnect: () => {}
|
|
491
|
+
}),
|
|
492
|
+
getManifest: () => ({
|
|
493
|
+
name: "Chrome",
|
|
494
|
+
version: "145.0.0.0",
|
|
495
|
+
manifest_version: 3,
|
|
496
|
+
description: "Chrome Browser"
|
|
497
|
+
}),
|
|
498
|
+
getURL: (path) => `chrome-extension://invalid/${path}`,
|
|
499
|
+
id: undefined,
|
|
500
|
+
getPlatformInfo: (callback) => callback({
|
|
501
|
+
os: navigator.platform.includes('Win') ? 'win' :
|
|
502
|
+
navigator.platform.includes('Mac') ? 'mac' : 'linux',
|
|
503
|
+
arch: 'x86-64',
|
|
504
|
+
nacl_arch: 'x86-64'
|
|
483
505
|
})
|
|
484
506
|
};
|
|
485
|
-
|
|
486
|
-
// Make chrome object non-enumerable to match real Chrome
|
|
487
|
-
Object.defineProperty(window, 'chrome', {
|
|
488
|
-
value: window.chrome,
|
|
489
|
-
writable: false,
|
|
490
|
-
enumerable: false,
|
|
491
|
-
configurable: true
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
// Add Chrome-specific globals that Cloudflare might check
|
|
495
|
-
if (!window.external) {
|
|
496
|
-
window.external = {
|
|
497
|
-
AddSearchProvider: () => {},
|
|
498
|
-
IsSearchProviderInstalled: () => 0
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Ensure chrome.runtime appears as a native object
|
|
503
507
|
Object.defineProperty(window.chrome.runtime, 'toString', {
|
|
504
508
|
value: () => '[object Object]'
|
|
505
509
|
});
|
|
506
510
|
}
|
|
511
|
+
|
|
512
|
+
// Add app if missing
|
|
513
|
+
if (!window.chrome.app) {
|
|
514
|
+
window.chrome.app = {
|
|
515
|
+
isInstalled: false,
|
|
516
|
+
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
|
|
517
|
+
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
|
|
518
|
+
getDetails: () => null,
|
|
519
|
+
getIsInstalled: () => false
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Add storage if missing
|
|
524
|
+
if (!window.chrome.storage) {
|
|
525
|
+
window.chrome.storage = {
|
|
526
|
+
local: {
|
|
527
|
+
get: (keys, callback) => callback && callback({}),
|
|
528
|
+
set: (items, callback) => callback && callback(),
|
|
529
|
+
remove: (keys, callback) => callback && callback(),
|
|
530
|
+
clear: (callback) => callback && callback()
|
|
531
|
+
},
|
|
532
|
+
sync: {
|
|
533
|
+
get: (keys, callback) => callback && callback({}),
|
|
534
|
+
set: (items, callback) => callback && callback(),
|
|
535
|
+
remove: (keys, callback) => callback && callback(),
|
|
536
|
+
clear: (callback) => callback && callback()
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Add loadTimes/csi if missing
|
|
542
|
+
if (!window.chrome.loadTimes) {
|
|
543
|
+
window.chrome.loadTimes = () => {
|
|
544
|
+
const now = performance.now();
|
|
545
|
+
return {
|
|
546
|
+
commitLoadTime: now - Math.random() * 1000,
|
|
547
|
+
connectionInfo: 'http/1.1',
|
|
548
|
+
finishDocumentLoadTime: now - Math.random() * 500,
|
|
549
|
+
finishLoadTime: now - Math.random() * 100,
|
|
550
|
+
navigationType: 'Navigation'
|
|
551
|
+
};
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
if (!window.chrome.csi) {
|
|
555
|
+
window.chrome.csi = () => ({
|
|
556
|
+
onloadT: Date.now(),
|
|
557
|
+
pageT: Math.random() * 1000,
|
|
558
|
+
startE: Date.now() - Math.random() * 2000
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Make chrome object non-enumerable to match real Chrome
|
|
563
|
+
Object.defineProperty(window, 'chrome', {
|
|
564
|
+
value: window.chrome,
|
|
565
|
+
writable: false,
|
|
566
|
+
enumerable: false,
|
|
567
|
+
configurable: true
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Add Chrome-specific globals that Cloudflare might check
|
|
571
|
+
if (!window.external) {
|
|
572
|
+
window.external = {
|
|
573
|
+
AddSearchProvider: () => {},
|
|
574
|
+
IsSearchProviderInstalled: () => 0
|
|
575
|
+
};
|
|
576
|
+
}
|
|
507
577
|
}, 'Chrome runtime simulation');
|
|
508
578
|
|
|
509
579
|
// Add realistic Chrome browser behavior
|
|
@@ -568,6 +638,7 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
568
638
|
plugins = [];
|
|
569
639
|
}
|
|
570
640
|
// Create proper array-like object with enumerable indices and length
|
|
641
|
+
// Create proper PluginArray-like object with required methods
|
|
571
642
|
const pluginsArray = {};
|
|
572
643
|
plugins.forEach((plugin, index) => {
|
|
573
644
|
pluginsArray[index] = plugin;
|
|
@@ -581,6 +652,13 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
581
652
|
configurable: false
|
|
582
653
|
});
|
|
583
654
|
|
|
655
|
+
// PluginArray methods that bot detectors check for
|
|
656
|
+
pluginsArray.item = function(i) { return plugins[i] || null; };
|
|
657
|
+
pluginsArray.namedItem = function(name) { return plugins.find(p => p.name === name) || null; };
|
|
658
|
+
pluginsArray.refresh = function() {};
|
|
659
|
+
pluginsArray[Symbol.iterator] = function*() { for (const p of plugins) yield p; };
|
|
660
|
+
pluginsArray[Symbol.toStringTag] = 'PluginArray';
|
|
661
|
+
|
|
584
662
|
safeDefinePropertyLocal(navigator, 'plugins', { get: () => pluginsArray });
|
|
585
663
|
}, 'plugins spoofing');
|
|
586
664
|
|
|
@@ -614,6 +692,71 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
614
692
|
spoofNavigatorProperties(navigator, vendorProps);
|
|
615
693
|
}, 'vendor/product spoofing');
|
|
616
694
|
|
|
695
|
+
// navigator.userAgentData — Chrome's Client Hints JS API
|
|
696
|
+
// Detection scripts check this; headless may have it missing or inconsistent
|
|
697
|
+
safeExecute(() => {
|
|
698
|
+
if (!userAgent.includes('Chrome/')) return; // Only for Chrome UAs
|
|
699
|
+
|
|
700
|
+
const chromeMatch = userAgent.match(/Chrome\/(\d+)/);
|
|
701
|
+
const majorVersion = chromeMatch ? chromeMatch[1] : '145';
|
|
702
|
+
|
|
703
|
+
let platform = 'Windows';
|
|
704
|
+
let platformVersion = '15.0.0';
|
|
705
|
+
let architecture = 'x86';
|
|
706
|
+
let model = '';
|
|
707
|
+
let bitness = '64';
|
|
708
|
+
if (userAgent.includes('Macintosh') || userAgent.includes('Mac OS X')) {
|
|
709
|
+
platform = 'macOS';
|
|
710
|
+
platformVersion = '13.5.0';
|
|
711
|
+
architecture = 'arm';
|
|
712
|
+
} else if (userAgent.includes('X11; Linux')) {
|
|
713
|
+
platform = 'Linux';
|
|
714
|
+
platformVersion = '6.5.0';
|
|
715
|
+
architecture = 'x86';
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const brands = [
|
|
719
|
+
{ brand: 'Not:A-Brand', version: '99' },
|
|
720
|
+
{ brand: 'Google Chrome', version: majorVersion },
|
|
721
|
+
{ brand: 'Chromium', version: majorVersion }
|
|
722
|
+
];
|
|
723
|
+
|
|
724
|
+
const uaData = {
|
|
725
|
+
brands: brands,
|
|
726
|
+
mobile: false,
|
|
727
|
+
platform: platform,
|
|
728
|
+
getHighEntropyValues: function(hints) {
|
|
729
|
+
const result = {
|
|
730
|
+
brands: brands,
|
|
731
|
+
mobile: false,
|
|
732
|
+
platform: platform,
|
|
733
|
+
architecture: architecture,
|
|
734
|
+
bitness: bitness,
|
|
735
|
+
model: model,
|
|
736
|
+
platformVersion: platformVersion,
|
|
737
|
+
fullVersionList: [
|
|
738
|
+
{ brand: 'Not:A-Brand', version: '99.0.0.0' },
|
|
739
|
+
{ brand: 'Google Chrome', version: majorVersion + '.0.7632.160' },
|
|
740
|
+
{ brand: 'Chromium', version: majorVersion + '.0.7632.160' }
|
|
741
|
+
]
|
|
742
|
+
};
|
|
743
|
+
// Only return requested hints
|
|
744
|
+
const filtered = {};
|
|
745
|
+
for (const hint of hints) {
|
|
746
|
+
if (result.hasOwnProperty(hint)) filtered[hint] = result[hint];
|
|
747
|
+
}
|
|
748
|
+
return Promise.resolve(filtered);
|
|
749
|
+
},
|
|
750
|
+
toJSON: function() {
|
|
751
|
+
return { brands: brands, mobile: false, platform: platform };
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
Object.defineProperty(navigator, 'userAgentData', {
|
|
755
|
+
get: () => uaData,
|
|
756
|
+
configurable: true
|
|
757
|
+
});
|
|
758
|
+
}, 'navigator.userAgentData spoofing');
|
|
759
|
+
|
|
617
760
|
// Enhanced OS fingerprinting protection based on actual user agent content
|
|
618
761
|
//
|
|
619
762
|
safeExecute(() => {
|
|
@@ -767,13 +910,18 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
767
910
|
};
|
|
768
911
|
}, 'fingerprinting mocks');
|
|
769
912
|
|
|
913
|
+
// GPU identity — selected once per browser session, passed from Node.js
|
|
914
|
+
const GPU_VENDOR = gpuConfig.vendor;
|
|
915
|
+
const GPU_RENDERER = gpuConfig.renderer;
|
|
916
|
+
if (debugEnabled) console.log(`[fingerprint] GPU: ${GPU_VENDOR} / ${GPU_RENDERER}`);
|
|
917
|
+
|
|
770
918
|
// WebGL spoofing
|
|
771
919
|
//
|
|
772
920
|
safeExecute(() => {
|
|
773
921
|
// Enhanced WebGL fingerprinting protection
|
|
774
922
|
const webglParams = {
|
|
775
|
-
37445:
|
|
776
|
-
37446:
|
|
923
|
+
37445: GPU_VENDOR, // VENDOR
|
|
924
|
+
37446: GPU_RENDERER, // RENDERER
|
|
777
925
|
7936: 'WebGL 1.0 (OpenGL ES 2.0 Chromium)', // VERSION
|
|
778
926
|
35724: 'WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.00 Chromium)', // SHADING_LANGUAGE_VERSION
|
|
779
927
|
34076: 16384, // MAX_TEXTURE_SIZE
|
|
@@ -798,6 +946,14 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
798
946
|
}
|
|
799
947
|
return getParameter.call(this, parameter);
|
|
800
948
|
};
|
|
949
|
+
// Intercept getExtension to control WEBGL_debug_renderer_info
|
|
950
|
+
const getExtension = WebGLRenderingContext.prototype.getExtension;
|
|
951
|
+
WebGLRenderingContext.prototype.getExtension = function(name) {
|
|
952
|
+
if (name === 'WEBGL_debug_renderer_info') {
|
|
953
|
+
return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
|
|
954
|
+
}
|
|
955
|
+
return getExtension.call(this, name);
|
|
956
|
+
};
|
|
801
957
|
// Spoof supported extensions
|
|
802
958
|
const getSupportedExtensions = WebGLRenderingContext.prototype.getSupportedExtensions;
|
|
803
959
|
WebGLRenderingContext.prototype.getSupportedExtensions = function() {
|
|
@@ -840,10 +996,72 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
840
996
|
}
|
|
841
997
|
return getParameter2.call(this, parameter);
|
|
842
998
|
};
|
|
999
|
+
const getExtension2 = WebGL2RenderingContext.prototype.getExtension;
|
|
1000
|
+
WebGL2RenderingContext.prototype.getExtension = function(name) {
|
|
1001
|
+
if (name === 'WEBGL_debug_renderer_info') {
|
|
1002
|
+
return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
|
|
1003
|
+
}
|
|
1004
|
+
return getExtension2.call(this, name);
|
|
1005
|
+
};
|
|
843
1006
|
}
|
|
844
1007
|
}, 'WebGL spoofing');
|
|
845
1008
|
|
|
846
|
-
//
|
|
1009
|
+
// WebGL context patching — Proxy wrapper for real contexts + null-context safety
|
|
1010
|
+
safeExecute(() => {
|
|
1011
|
+
const webglSpoofParams = {
|
|
1012
|
+
37445: GPU_VENDOR,
|
|
1013
|
+
37446: GPU_RENDERER
|
|
1014
|
+
};
|
|
1015
|
+
const debugRendererExt = { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
|
|
1016
|
+
|
|
1017
|
+
const originalGetContext = HTMLCanvasElement.prototype.getContext;
|
|
1018
|
+
HTMLCanvasElement.prototype.getContext = function(type, attrs) {
|
|
1019
|
+
const ctx = originalGetContext.call(this, type, attrs);
|
|
1020
|
+
if (type !== 'webgl' && type !== 'experimental-webgl' && type !== 'webgl2') return ctx;
|
|
1021
|
+
|
|
1022
|
+
const noop = () => {};
|
|
1023
|
+
|
|
1024
|
+
// Null context — return mock to prevent crashes
|
|
1025
|
+
if (ctx === null) {
|
|
1026
|
+
return new Proxy({}, {
|
|
1027
|
+
get(_, prop) {
|
|
1028
|
+
if (prop === 'getShaderPrecisionFormat') return () => ({ rangeMin: 127, rangeMax: 127, precision: 23 });
|
|
1029
|
+
if (prop === 'getParameter') return (p) => webglSpoofParams[p] || 0;
|
|
1030
|
+
if (prop === 'getSupportedExtensions') return () => [];
|
|
1031
|
+
if (prop === 'getExtension') return (name) => {
|
|
1032
|
+
if (name === 'WEBGL_debug_renderer_info') return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
|
|
1033
|
+
return null;
|
|
1034
|
+
};
|
|
1035
|
+
if (prop === 'canvas') return this;
|
|
1036
|
+
if (prop === 'drawingBufferWidth') return 1920;
|
|
1037
|
+
if (prop === 'drawingBufferHeight') return 1080;
|
|
1038
|
+
return noop;
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Real context — wrap in Proxy to intercept getParameter/getExtension
|
|
1044
|
+
// Direct property assignment fails silently on native WebGL objects
|
|
1045
|
+
return new Proxy(ctx, {
|
|
1046
|
+
get(target, prop, receiver) {
|
|
1047
|
+
if (prop === 'getParameter') {
|
|
1048
|
+
return function(param) {
|
|
1049
|
+
if (webglSpoofParams.hasOwnProperty(param)) return webglSpoofParams[param];
|
|
1050
|
+
return target.getParameter(param);
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
if (prop === 'getExtension') {
|
|
1054
|
+
return function(name) {
|
|
1055
|
+
if (name === 'WEBGL_debug_renderer_info') return debugRendererExt;
|
|
1056
|
+
return target.getExtension(name);
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
const val = Reflect.get(target, prop, receiver);
|
|
1060
|
+
return typeof val === 'function' ? val.bind(target) : val;
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
};
|
|
1064
|
+
}, 'WebGL context patching'); // Permissions API spoofing
|
|
847
1065
|
//
|
|
848
1066
|
safeExecute(() => {
|
|
849
1067
|
if (navigator.permissions?.query) {
|
|
@@ -895,6 +1113,118 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
895
1113
|
};
|
|
896
1114
|
}
|
|
897
1115
|
}, 'media device spoofing');
|
|
1116
|
+
|
|
1117
|
+
// Window dimensions — headless Chrome reports 0 for outer dimensions
|
|
1118
|
+
safeExecute(() => {
|
|
1119
|
+
if (!window.outerWidth || window.outerWidth === 0 || window.outerWidth === window.innerWidth) {
|
|
1120
|
+
Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth + 16, configurable: true });
|
|
1121
|
+
}
|
|
1122
|
+
if (!window.outerHeight || window.outerHeight === 0 || window.outerHeight === window.innerHeight) {
|
|
1123
|
+
Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight + 88, configurable: true });
|
|
1124
|
+
}
|
|
1125
|
+
if (window.screenX === 0 && window.screenY === 0) {
|
|
1126
|
+
const sX = Math.floor(Math.random() * 200);
|
|
1127
|
+
const sY = Math.floor(Math.random() * 50) + 20;
|
|
1128
|
+
Object.defineProperty(window, 'screenX', { get: () => sX });
|
|
1129
|
+
Object.defineProperty(window, 'screenY', { get: () => sY });
|
|
1130
|
+
}
|
|
1131
|
+
}, 'window dimension spoofing');
|
|
1132
|
+
|
|
1133
|
+
// navigator.connection — missing or incomplete in headless
|
|
1134
|
+
safeExecute(() => {
|
|
1135
|
+
if (!navigator.connection) {
|
|
1136
|
+
Object.defineProperty(navigator, 'connection', {
|
|
1137
|
+
get: () => ({
|
|
1138
|
+
effectiveType: '4g',
|
|
1139
|
+
rtt: 50,
|
|
1140
|
+
downlink: 10,
|
|
1141
|
+
saveData: false,
|
|
1142
|
+
type: 'wifi',
|
|
1143
|
+
addEventListener: () => {},
|
|
1144
|
+
removeEventListener: () => {}
|
|
1145
|
+
})
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
}, 'connection API spoofing');
|
|
1149
|
+
|
|
1150
|
+
// navigator.pdfViewerEnabled — missing in headless, true in real Chrome
|
|
1151
|
+
safeExecute(() => {
|
|
1152
|
+
if (navigator.pdfViewerEnabled === undefined) {
|
|
1153
|
+
Object.defineProperty(navigator, 'pdfViewerEnabled', {
|
|
1154
|
+
get: () => true, configurable: true
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}, 'pdfViewerEnabled spoofing');
|
|
1158
|
+
|
|
1159
|
+
// speechSynthesis — headless returns empty voices array
|
|
1160
|
+
safeExecute(() => {
|
|
1161
|
+
if (window.speechSynthesis) {
|
|
1162
|
+
const origGetVoices = speechSynthesis.getVoices.bind(speechSynthesis);
|
|
1163
|
+
speechSynthesis.getVoices = function() {
|
|
1164
|
+
const voices = origGetVoices();
|
|
1165
|
+
if (voices.length === 0) {
|
|
1166
|
+
return [{
|
|
1167
|
+
default: true, lang: 'en-US', localService: true,
|
|
1168
|
+
name: 'Microsoft David - English (United States)', voiceURI: 'Microsoft David - English (United States)'
|
|
1169
|
+
}, {
|
|
1170
|
+
default: false, lang: 'en-US', localService: true,
|
|
1171
|
+
name: 'Microsoft Zira - English (United States)', voiceURI: 'Microsoft Zira - English (United States)'
|
|
1172
|
+
}];
|
|
1173
|
+
}
|
|
1174
|
+
return voices;
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
}, 'speechSynthesis spoofing');
|
|
1178
|
+
|
|
1179
|
+
// AudioContext — headless has distinct audio processing fingerprint
|
|
1180
|
+
safeExecute(() => {
|
|
1181
|
+
if (window.AudioContext || window.webkitAudioContext) {
|
|
1182
|
+
const OrigAudioContext = window.AudioContext || window.webkitAudioContext;
|
|
1183
|
+
const origCreateOscillator = OrigAudioContext.prototype.createOscillator;
|
|
1184
|
+
const origCreateDynamicsCompressor = OrigAudioContext.prototype.createDynamicsCompressor;
|
|
1185
|
+
|
|
1186
|
+
// Inject deterministic noise into audio output — consistent per session
|
|
1187
|
+
const audioNoiseSeed = Math.random() * 0.01 - 0.005;
|
|
1188
|
+
const compNoiseSeed = Math.random() * 0.1 - 0.05;
|
|
1189
|
+
|
|
1190
|
+
OrigAudioContext.prototype.createOscillator = function() {
|
|
1191
|
+
const osc = origCreateOscillator.call(this);
|
|
1192
|
+
const origFreq = osc.frequency.value;
|
|
1193
|
+
osc.frequency.value = origFreq + audioNoiseSeed;
|
|
1194
|
+
return osc;
|
|
1195
|
+
};
|
|
1196
|
+
OrigAudioContext.prototype.createDynamicsCompressor = function() {
|
|
1197
|
+
const comp = origCreateDynamicsCompressor.call(this);
|
|
1198
|
+
const origThreshold = comp.threshold.value;
|
|
1199
|
+
comp.threshold.value = origThreshold + compNoiseSeed;
|
|
1200
|
+
return comp;
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
}, 'AudioContext fingerprint spoofing');
|
|
1204
|
+
|
|
1205
|
+
// Broken image dimensions — headless returns 0x0, real browsers show broken icon
|
|
1206
|
+
safeExecute(() => {
|
|
1207
|
+
// Patch Image constructor to intercept broken image detection
|
|
1208
|
+
const OrigImage = window.Image;
|
|
1209
|
+
window.Image = function(w, h) {
|
|
1210
|
+
const img = arguments.length ? new OrigImage(w, h) : new OrigImage();
|
|
1211
|
+
// After load attempt, if broken, fake dimensions
|
|
1212
|
+
const origWidthDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'width');
|
|
1213
|
+
const origHeightDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'height');
|
|
1214
|
+
|
|
1215
|
+
// Monitor for broken state
|
|
1216
|
+
img.addEventListener('error', function() {
|
|
1217
|
+
// Force non-zero dimensions for broken images
|
|
1218
|
+
Object.defineProperty(this, 'width', { get: () => 24, configurable: true });
|
|
1219
|
+
Object.defineProperty(this, 'height', { get: () => 24, configurable: true });
|
|
1220
|
+
Object.defineProperty(this, 'naturalWidth', { get: () => 24, configurable: true });
|
|
1221
|
+
Object.defineProperty(this, 'naturalHeight', { get: () => 24, configurable: true });
|
|
1222
|
+
});
|
|
1223
|
+
return img;
|
|
1224
|
+
};
|
|
1225
|
+
window.Image.prototype = OrigImage.prototype;
|
|
1226
|
+
Object.defineProperty(window, 'Image', { writable: true, configurable: true });
|
|
1227
|
+
}, 'broken image dimension spoofing');
|
|
898
1228
|
|
|
899
1229
|
// Fetch Request Headers Normalization
|
|
900
1230
|
//
|
|
@@ -938,21 +1268,9 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
938
1268
|
|
|
939
1269
|
// CSS Media Query Spoofing
|
|
940
1270
|
//
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const result = originalMatchMedia.call(this, query);
|
|
945
|
-
// Add slight randomization to avoid fingerprinting for device queries
|
|
946
|
-
if (query.includes('device-width') || query.includes('device-height') ||
|
|
947
|
-
query.includes('aspect-ratio') || query.includes('color-gamut')) {
|
|
948
|
-
Object.defineProperty(result, 'matches', {
|
|
949
|
-
get: () => Math.random() > 0.1 ? originalMatchMedia.call(window, query).matches : !originalMatchMedia.call(window, query).matches,
|
|
950
|
-
configurable: true
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
|
-
return result;
|
|
954
|
-
};
|
|
955
|
-
}, 'CSS media query spoofing');
|
|
1271
|
+
// CSS media query — pass through normally. Screen dimensions are already spoofed
|
|
1272
|
+
// so matchMedia results will naturally reflect the spoofed values.
|
|
1273
|
+
// No modification needed here.
|
|
956
1274
|
|
|
957
1275
|
// Enhanced WebRTC Spoofing
|
|
958
1276
|
//
|
|
@@ -961,13 +1279,41 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
961
1279
|
const OriginalRTC = window.RTCPeerConnection;
|
|
962
1280
|
window.RTCPeerConnection = function(...args) {
|
|
963
1281
|
const pc = new OriginalRTC(...args);
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1282
|
+
|
|
1283
|
+
// Intercept onicecandidate to strip local IP addresses
|
|
1284
|
+
const origAddEventListener = pc.addEventListener.bind(pc);
|
|
1285
|
+
pc.addEventListener = function(type, listener, ...rest) {
|
|
1286
|
+
if (type === 'icecandidate') {
|
|
1287
|
+
const wrappedListener = function(event) {
|
|
1288
|
+
if (event.candidate && event.candidate.candidate) {
|
|
1289
|
+
// Strip candidates containing local/private IPs
|
|
1290
|
+
const c = event.candidate.candidate;
|
|
1291
|
+
if (c.includes('.local') || /(?:10|172\.(?:1[6-9]|2\d|3[01])|192\.168)\./.test(c)) {
|
|
1292
|
+
return; // suppress local IP candidates
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
listener.call(this, event);
|
|
1296
|
+
};
|
|
1297
|
+
return origAddEventListener(type, wrappedListener, ...rest);
|
|
1298
|
+
}
|
|
1299
|
+
return origAddEventListener(type, listener, ...rest);
|
|
967
1300
|
};
|
|
1301
|
+
|
|
1302
|
+
// Also intercept the property-based handler
|
|
1303
|
+
let _onicecandidateHandler = null;
|
|
1304
|
+
Object.defineProperty(pc, 'onicecandidate', {
|
|
1305
|
+
get: () => _onicecandidateHandler,
|
|
1306
|
+
set: (handler) => {
|
|
1307
|
+
_onicecandidateHandler = handler;
|
|
1308
|
+
// No-op — the addEventListener wrapper above handles filtering
|
|
1309
|
+
},
|
|
1310
|
+
configurable: true
|
|
1311
|
+
});
|
|
1312
|
+
|
|
968
1313
|
return pc;
|
|
969
1314
|
};
|
|
970
1315
|
Object.setPrototypeOf(window.RTCPeerConnection, OriginalRTC);
|
|
1316
|
+
window.RTCPeerConnection.prototype = OriginalRTC.prototype;
|
|
971
1317
|
}
|
|
972
1318
|
}, 'WebRTC spoofing');
|
|
973
1319
|
|
|
@@ -1039,62 +1385,15 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1039
1385
|
|
|
1040
1386
|
CanvasRenderingContext2D.prototype.measureText = function(text) {
|
|
1041
1387
|
const result = originalMeasureText.call(this, text);
|
|
1042
|
-
//
|
|
1388
|
+
// Deterministic slight noise to text measurements
|
|
1043
1389
|
const originalWidth = result.width;
|
|
1390
|
+
const hash = text.length * 7 + (text.charCodeAt(0) || 0);
|
|
1391
|
+
const noise = ((hash * 2654435761 >>> 0) % 100 - 50) / 500; // -0.1 to +0.1, deterministic per text
|
|
1044
1392
|
return Object.create(result, {
|
|
1045
|
-
width: { get: () => originalWidth +
|
|
1393
|
+
width: { get: () => originalWidth + noise }
|
|
1046
1394
|
});
|
|
1047
1395
|
};
|
|
1048
1396
|
|
|
1049
|
-
// Comprehensive canvas fingerprinting protection
|
|
1050
|
-
//
|
|
1051
|
-
const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
|
1052
|
-
CanvasRenderingContext2D.prototype.getImageData = function(sx, sy, sw, sh) {
|
|
1053
|
-
const imageData = originalGetImageData.call(this, sx, sy, sw, sh);
|
|
1054
|
-
// Skip noise on large canvases (>500K pixels) � too expensive for minimal fingerprint benefit
|
|
1055
|
-
if (imageData.data.length > 2000000) {
|
|
1056
|
-
return imageData;
|
|
1057
|
-
}
|
|
1058
|
-
// Add subtle noise to pixel data
|
|
1059
|
-
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
1060
|
-
if (Math.random() < 0.1) { // 10% chance to modify each pixel
|
|
1061
|
-
imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + Math.floor(Math.random() * 3) - 1));
|
|
1062
|
-
imageData.data[i + 1] = Math.max(0, Math.min(255, imageData.data[i + 1] + Math.floor(Math.random() * 3) - 1));
|
|
1063
|
-
imageData.data[i + 2] = Math.max(0, Math.min(255, imageData.data[i + 2] + Math.floor(Math.random() * 3) - 1));
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
return imageData;
|
|
1067
|
-
};
|
|
1068
|
-
|
|
1069
|
-
// WebGL canvas context fingerprinting
|
|
1070
|
-
const originalGetContext = HTMLCanvasElement.prototype.getContext;
|
|
1071
|
-
HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) {
|
|
1072
|
-
const context = originalGetContext.call(this, contextType, contextAttributes);
|
|
1073
|
-
|
|
1074
|
-
if (contextType === 'webgl' || contextType === 'webgl2' || contextType === 'experimental-webgl') {
|
|
1075
|
-
// Override WebGL-specific fingerprinting methods
|
|
1076
|
-
const originalGetShaderPrecisionFormat = context.getShaderPrecisionFormat;
|
|
1077
|
-
context.getShaderPrecisionFormat = function(shaderType, precisionType) {
|
|
1078
|
-
return {
|
|
1079
|
-
rangeMin: 127,
|
|
1080
|
-
rangeMax: 127,
|
|
1081
|
-
precision: 23
|
|
1082
|
-
};
|
|
1083
|
-
};
|
|
1084
|
-
|
|
1085
|
-
const originalGetExtension = context.getExtension;
|
|
1086
|
-
context.getExtension = function(name) {
|
|
1087
|
-
// Block access to fingerprinting-sensitive extensions
|
|
1088
|
-
if (name === 'WEBGL_debug_renderer_info') {
|
|
1089
|
-
return null;
|
|
1090
|
-
}
|
|
1091
|
-
return originalGetExtension.call(this, name);
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
return context;
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1098
1397
|
// Override font detection methods
|
|
1099
1398
|
//
|
|
1100
1399
|
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth');
|
|
@@ -1104,7 +1403,10 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1104
1403
|
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
|
1105
1404
|
get: function() {
|
|
1106
1405
|
if (this.style && this.style.fontFamily) {
|
|
1107
|
-
|
|
1406
|
+
const w = originalOffsetWidth.get.call(this);
|
|
1407
|
+
// Deterministic per font family — same font always gets same offset
|
|
1408
|
+
const fontHash = this.style.fontFamily.split('').reduce((a, c) => (a * 31 + c.charCodeAt(0)) & 0xffff, 0);
|
|
1409
|
+
return Math.floor(w + ((fontHash % 3) - 1));
|
|
1108
1410
|
}
|
|
1109
1411
|
return originalOffsetWidth.get.call(this);
|
|
1110
1412
|
},
|
|
@@ -1125,24 +1427,75 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1125
1427
|
// Canvas fingerprinting protection
|
|
1126
1428
|
//
|
|
1127
1429
|
safeExecute(() => {
|
|
1430
|
+
// Per-session seed — consistent within session, varies between sessions
|
|
1431
|
+
const canvasSeed = Math.floor(Math.random() * 2147483647);
|
|
1432
|
+
|
|
1433
|
+
// Simple seeded PRNG — deterministic for same input
|
|
1434
|
+
function seededNoise(seed) {
|
|
1435
|
+
let s = seed ^ 0x5DEECE66D;
|
|
1436
|
+
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
|
1437
|
+
return s;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
|
1441
|
+
CanvasRenderingContext2D.prototype.getImageData = function(sx, sy, sw, sh) {
|
|
1442
|
+
const imageData = originalGetImageData.call(this, sx, sy, sw, sh);
|
|
1443
|
+
if (imageData.data.length > 2000000) return imageData;
|
|
1444
|
+
// Deterministic noise — same canvas content + position = same noise every call
|
|
1445
|
+
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
1446
|
+
const n = seededNoise(canvasSeed ^ (i >> 2));
|
|
1447
|
+
if ((n & 0xf) < 2) { // ~12.5% of pixels
|
|
1448
|
+
const shift = (n >> 4 & 3) - 1; // -1, 0, or +1
|
|
1449
|
+
imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + shift));
|
|
1450
|
+
imageData.data[i + 1] = Math.max(0, Math.min(255, imageData.data[i + 1] + shift));
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return imageData;
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1128
1456
|
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
1129
1457
|
HTMLCanvasElement.prototype.toDataURL = function(...args) {
|
|
1130
|
-
//
|
|
1458
|
+
// Apply same deterministic noise by reading through spoofed getImageData
|
|
1459
|
+
try {
|
|
1460
|
+
const ctx = this.getContext('2d');
|
|
1461
|
+
if (ctx && this.width > 0 && this.height > 0 && this.width * this.height < 500000) {
|
|
1462
|
+
const imageData = ctx.getImageData(0, 0, this.width, this.height);
|
|
1463
|
+
ctx.putImageData(imageData, 0, 0);
|
|
1464
|
+
}
|
|
1465
|
+
} catch (e) {} // WebGL or other context — skip
|
|
1131
1466
|
return originalToDataURL.apply(this, args);
|
|
1132
1467
|
};
|
|
1468
|
+
|
|
1469
|
+
const originalToBlob = HTMLCanvasElement.prototype.toBlob;
|
|
1470
|
+
if (originalToBlob) {
|
|
1471
|
+
HTMLCanvasElement.prototype.toBlob = function(callback, ...args) {
|
|
1472
|
+
try {
|
|
1473
|
+
const ctx = this.getContext('2d');
|
|
1474
|
+
if (ctx && this.width > 0 && this.height > 0 && this.width * this.height < 500000) {
|
|
1475
|
+
const imageData = ctx.getImageData(0, 0, this.width, this.height);
|
|
1476
|
+
ctx.putImageData(imageData, 0, 0);
|
|
1477
|
+
}
|
|
1478
|
+
} catch (e) {}
|
|
1479
|
+
return originalToBlob.call(this, callback, ...args);
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1133
1482
|
}, 'canvas fingerprinting protection');
|
|
1134
1483
|
|
|
1135
1484
|
// Battery API spoofing
|
|
1136
1485
|
//
|
|
1137
1486
|
safeExecute(() => {
|
|
1138
1487
|
if (navigator.getBattery) {
|
|
1488
|
+
const batteryState = {
|
|
1489
|
+
charging: Math.random() > 0.5,
|
|
1490
|
+
chargingTime: Math.random() > 0.5 ? Infinity : Math.floor(Math.random() * 3600),
|
|
1491
|
+
dischargingTime: Math.floor(Math.random() * 7200),
|
|
1492
|
+
level: Math.round((Math.random() * 0.7 + 0.25) * 100) / 100,
|
|
1493
|
+
addEventListener: () => {},
|
|
1494
|
+
removeEventListener: () => {},
|
|
1495
|
+
dispatchEvent: () => true
|
|
1496
|
+
};
|
|
1139
1497
|
navigator.getBattery = function() {
|
|
1140
|
-
return Promise.resolve(
|
|
1141
|
-
charging: Math.random() > 0.5,
|
|
1142
|
-
chargingTime: Math.random() > 0.5 ? Infinity : Math.random() * 3600,
|
|
1143
|
-
dischargingTime: Math.random() * 7200,
|
|
1144
|
-
level: Math.random() * 0.99 + 0.01
|
|
1145
|
-
});
|
|
1498
|
+
return Promise.resolve(batteryState);
|
|
1146
1499
|
};
|
|
1147
1500
|
}
|
|
1148
1501
|
}, 'battery API spoofing');
|
|
@@ -1265,7 +1618,7 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1265
1618
|
});
|
|
1266
1619
|
}, 'location URL masking');
|
|
1267
1620
|
|
|
1268
|
-
}, ua, forceDebug);
|
|
1621
|
+
}, ua, forceDebug, selectedGpu);
|
|
1269
1622
|
} catch (stealthErr) {
|
|
1270
1623
|
if (stealthErr.message.includes('Session closed') ||
|
|
1271
1624
|
stealthErr.message.includes('addScriptToEvaluateOnNewDocument timed out') ||
|
package/nwss.js
CHANGED
|
@@ -99,9 +99,9 @@ const CONCURRENCY_LIMITS = Object.freeze({
|
|
|
99
99
|
|
|
100
100
|
// V8 Optimization: Use Map for user agent lookups instead of object
|
|
101
101
|
const USER_AGENTS = Object.freeze(new Map([
|
|
102
|
-
['chrome', "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
103
|
-
['chrome_mac', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
104
|
-
['chrome_linux', "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
102
|
+
['chrome', "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"],
|
|
103
|
+
['chrome_mac', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"],
|
|
104
|
+
['chrome_linux', "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"],
|
|
105
105
|
['firefox', "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0"],
|
|
106
106
|
['firefox_mac', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0"],
|
|
107
107
|
['firefox_linux', "Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0"],
|
|
@@ -619,7 +619,7 @@ Redirect Handling Options:
|
|
|
619
619
|
source: true/false Save page source HTML after load
|
|
620
620
|
firstParty: true/false Allow first-party matches (default: false)
|
|
621
621
|
thirdParty: true/false Allow third-party matches (default: true)
|
|
622
|
-
screenshot: true/false
|
|
622
|
+
screenshot: true/false/\"force\" Capture screenshot (true=on failure, \"force\"=always)
|
|
623
623
|
headful: true/false Launch browser with GUI for this site
|
|
624
624
|
fingerprint_protection: true/false/"random" Enable fingerprint spoofing: true/false/"random"
|
|
625
625
|
adblock_rules: true/false Generate adblock filter rules with resource types for this site
|
|
@@ -1367,9 +1367,17 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1367
1367
|
if (launchHeadless) {
|
|
1368
1368
|
const puppeteerInfo = detectPuppeteerVersion();
|
|
1369
1369
|
|
|
1370
|
+
// Check if any site needs fingerprint protection — use stealth-friendly headless mode
|
|
1371
|
+
const needsStealth = sites.some(site => site.fingerprint_protection);
|
|
1372
|
+
|
|
1370
1373
|
if (puppeteerInfo.useShellMode) {
|
|
1371
|
-
|
|
1372
|
-
|
|
1374
|
+
if (needsStealth) {
|
|
1375
|
+
headlessMode = 'new'; // Full Chrome in headless — harder to detect than chrome-headless-shell
|
|
1376
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Using headless=new for stealth (fingerprint_protection detected)`));
|
|
1377
|
+
} else {
|
|
1378
|
+
headlessMode = 'shell'; // Use fast chrome-headless-shell for 22.x+
|
|
1379
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Using chrome-headless-shell (Puppeteer ${puppeteerInfo.version || 'v' + puppeteerInfo.majorVersion + '.x'})`));
|
|
1380
|
+
}
|
|
1373
1381
|
} else {
|
|
1374
1382
|
headlessMode = true; // Use regular headless for older versions
|
|
1375
1383
|
if (forceDebug) console.log(formatLogMessage('debug', 'Could not detect Puppeteer version, using regular headless mode'));
|
|
@@ -1439,7 +1447,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1439
1447
|
'--disable-features=SafeBrowsing',
|
|
1440
1448
|
'--disable-dev-shm-usage',
|
|
1441
1449
|
'--disable-sync',
|
|
1442
|
-
'--
|
|
1450
|
+
'--use-gl=swiftshader', // Software WebGL — prevents ad script crashes in headless
|
|
1443
1451
|
'--mute-audio',
|
|
1444
1452
|
'--disable-translate',
|
|
1445
1453
|
'--window-size=1920,1080',
|
|
@@ -1494,7 +1502,9 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1494
1502
|
|
|
1495
1503
|
// Log which headless mode is being used
|
|
1496
1504
|
if (forceDebug && launchHeadless) {
|
|
1497
|
-
|
|
1505
|
+
const needsStealth = sites.some(site => site.fingerprint_protection);
|
|
1506
|
+
const modeLabel = needsStealth ? 'headless=new (stealth mode)' : 'chrome-headless-shell (performance mode)';
|
|
1507
|
+
console.log(formatLogMessage('debug', `Using ${modeLabel}`));
|
|
1498
1508
|
}
|
|
1499
1509
|
|
|
1500
1510
|
// Initial cleanup of any existing Chrome temp files - always comprehensive on startup
|
|
@@ -1652,6 +1662,11 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1652
1662
|
let cdpSessionManager = null;
|
|
1653
1663
|
// Use Map to track domains and their resource types for --adblock-rules or --dry-run
|
|
1654
1664
|
const matchedDomains = (adblockRulesMode || siteConfig.adblock_rules || dryRunMode) ? new Map() : new Set();
|
|
1665
|
+
|
|
1666
|
+
// Local domain dedup scoped to THIS processUrl call only
|
|
1667
|
+
// Prevents cross-config contamination from the global domain cache
|
|
1668
|
+
const localDetectedDomains = new Set();
|
|
1669
|
+
const isLocallyDetected = (domain) => localDetectedDomains.has(domain);
|
|
1655
1670
|
|
|
1656
1671
|
// Initialize dry run matches collection
|
|
1657
1672
|
if (dryRunMode) {
|
|
@@ -2159,14 +2174,14 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2159
2174
|
}
|
|
2160
2175
|
|
|
2161
2176
|
await page.setExtraHTTPHeaders({
|
|
2162
|
-
'Sec-CH-UA': '"
|
|
2177
|
+
'Sec-CH-UA': '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"',
|
|
2163
2178
|
'Sec-CH-UA-Platform': `"${platform}"`,
|
|
2164
2179
|
'Sec-CH-UA-Platform-Version': `"${platformVersion}"`,
|
|
2165
2180
|
'Sec-CH-UA-Mobile': '?0',
|
|
2166
2181
|
'Sec-CH-UA-Arch': `"${arch}"`,
|
|
2167
2182
|
'Sec-CH-UA-Bitness': '"64"',
|
|
2168
|
-
'Sec-CH-UA-Full-Version': '"
|
|
2169
|
-
'Sec-CH-UA-Full-Version-List': '"
|
|
2183
|
+
'Sec-CH-UA-Full-Version': '"145.0.7632.160"',
|
|
2184
|
+
'Sec-CH-UA-Full-Version-List': '"Not:A-Brand";v="99.0.0.0", "Google Chrome";v="145.0.7632.160", "Chromium";v="145.0.7632.160"'
|
|
2170
2185
|
});
|
|
2171
2186
|
}
|
|
2172
2187
|
} catch (fingerprintErr) {
|
|
@@ -2430,6 +2445,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2430
2445
|
|
|
2431
2446
|
// Mark full subdomain as detected for future reference
|
|
2432
2447
|
markDomainAsDetected(cacheKey);
|
|
2448
|
+
localDetectedDomains.add(cacheKey);
|
|
2433
2449
|
|
|
2434
2450
|
// Also mark in smart cache with context (if cache is enabled)
|
|
2435
2451
|
if (smartCache) {
|
|
@@ -2534,7 +2550,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2534
2550
|
if (forceDebug) {
|
|
2535
2551
|
console.log(formatLogMessage('debug', `${messageColors.blocked('[adblock]')} ${checkedUrl} (${result.reason})`));
|
|
2536
2552
|
}
|
|
2537
|
-
request.abort();
|
|
2553
|
+
request.abort('blockedbyclient');
|
|
2538
2554
|
return;
|
|
2539
2555
|
}
|
|
2540
2556
|
adblockStats.allowed++;
|
|
@@ -2614,7 +2630,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2614
2630
|
}
|
|
2615
2631
|
}
|
|
2616
2632
|
|
|
2617
|
-
request.abort();
|
|
2633
|
+
request.abort('blockedbyclient');
|
|
2618
2634
|
return;
|
|
2619
2635
|
}
|
|
2620
2636
|
|
|
@@ -2724,7 +2740,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2724
2740
|
dryRunCallback: dryRunMode ? createEnhancedDryRunCallback(matchedDomains, forceDebug) : null,
|
|
2725
2741
|
matchedDomains,
|
|
2726
2742
|
addMatchedDomain,
|
|
2727
|
-
isDomainAlreadyDetected,
|
|
2743
|
+
isDomainAlreadyDetected: isLocallyDetected,
|
|
2728
2744
|
onWhoisResult: smartCache ? (domain, result) => smartCache.cacheNetTools(domain, 'whois', result) : undefined,
|
|
2729
2745
|
onDigResult: smartCache ? (domain, result, recordType) => smartCache.cacheNetTools(domain, 'dig', result, recordType) : undefined,
|
|
2730
2746
|
cachedWhois: smartCache ? smartCache.getCachedNetTools(reqDomain, 'whois') : null,
|
|
@@ -2773,8 +2789,8 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2773
2789
|
}
|
|
2774
2790
|
} else if (hasNetTools && !hasSearchString && !hasSearchStringAnd) {
|
|
2775
2791
|
// If nettools are configured (whois/dig), perform checks on the domain
|
|
2776
|
-
// Skip nettools check if full subdomain was already detected
|
|
2777
|
-
if (
|
|
2792
|
+
// Skip nettools check if full subdomain was already detected in THIS scan
|
|
2793
|
+
if (localDetectedDomains.has(fullSubdomain)) {
|
|
2778
2794
|
if (forceDebug) {
|
|
2779
2795
|
console.log(formatLogMessage('debug', `Skipping nettools check for already detected subdomain: ${fullSubdomain}`));
|
|
2780
2796
|
}
|
|
@@ -2833,7 +2849,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2833
2849
|
dryRunCallback: dryRunMode ? createEnhancedDryRunCallback(matchedDomains, forceDebug) : null,
|
|
2834
2850
|
matchedDomains,
|
|
2835
2851
|
addMatchedDomain,
|
|
2836
|
-
isDomainAlreadyDetected,
|
|
2852
|
+
isDomainAlreadyDetected: isLocallyDetected,
|
|
2837
2853
|
// Add cache callbacks if smart cache is available and caching is enabled
|
|
2838
2854
|
onWhoisResult: smartCache ? (domain, result) => {
|
|
2839
2855
|
smartCache.cacheNetTools(domain, 'whois', result);
|
|
@@ -2863,8 +2879,8 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2863
2879
|
}
|
|
2864
2880
|
} else {
|
|
2865
2881
|
// If searchstring or searchstring_and IS defined (with or without nettools), queue for content checking
|
|
2866
|
-
// Skip searchstring check if full subdomain was already detected
|
|
2867
|
-
if (
|
|
2882
|
+
// Skip searchstring check if full subdomain was already detected in THIS scan
|
|
2883
|
+
if (localDetectedDomains.has(fullSubdomain)) {
|
|
2868
2884
|
if (forceDebug) {
|
|
2869
2885
|
console.log(formatLogMessage('debug', `Skipping searchstring check for already detected subdomain: ${fullSubdomain}`));
|
|
2870
2886
|
}
|
|
@@ -2920,7 +2936,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2920
2936
|
searchStringsAnd,
|
|
2921
2937
|
matchedDomains,
|
|
2922
2938
|
addMatchedDomain, // Pass the helper function
|
|
2923
|
-
isDomainAlreadyDetected,
|
|
2939
|
+
isDomainAlreadyDetected: isLocallyDetected,
|
|
2924
2940
|
onContentFetched: smartCache && !ignoreCache ? (url, content) => {
|
|
2925
2941
|
// Only cache if not bypassing cache
|
|
2926
2942
|
if (!shouldBypassCacheForUrl(url, siteConfig)) {
|
|
@@ -2956,7 +2972,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2956
2972
|
regexes,
|
|
2957
2973
|
matchedDomains,
|
|
2958
2974
|
addMatchedDomain,
|
|
2959
|
-
isDomainAlreadyDetected,
|
|
2975
|
+
isDomainAlreadyDetected: isLocallyDetected,
|
|
2960
2976
|
onContentFetched: smartCache && !ignoreCache ? (url, content) => {
|
|
2961
2977
|
// Only cache if not bypassing cache
|
|
2962
2978
|
if (!shouldBypassCacheForUrl(url, siteConfig)) {
|
|
@@ -3027,7 +3043,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3027
3043
|
matchedDomains,
|
|
3028
3044
|
addMatchedDomain, // Pass the helper function
|
|
3029
3045
|
bypassCache: (url) => shouldBypassCacheForUrl(url, siteConfig),
|
|
3030
|
-
isDomainAlreadyDetected,
|
|
3046
|
+
isDomainAlreadyDetected: isLocallyDetected,
|
|
3031
3047
|
onContentFetched: smartCache && !ignoreCache ? (url, content) => {
|
|
3032
3048
|
// Only cache if not bypassing cache
|
|
3033
3049
|
if (!shouldBypassCacheForUrl(url, siteConfig)) {
|
|
@@ -3374,8 +3390,16 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3374
3390
|
|
|
3375
3391
|
// Mark page as processing during interactions
|
|
3376
3392
|
updatePageUsage(page, true);
|
|
3377
|
-
// Use enhanced interaction module
|
|
3378
|
-
|
|
3393
|
+
// Use enhanced interaction module with hard abort timeout
|
|
3394
|
+
const INTERACTION_HARD_TIMEOUT = 15000;
|
|
3395
|
+
try {
|
|
3396
|
+
await Promise.race([
|
|
3397
|
+
performPageInteraction(page, currentUrl, interactionConfig, forceDebug),
|
|
3398
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('interaction hard timeout')), INTERACTION_HARD_TIMEOUT))
|
|
3399
|
+
]);
|
|
3400
|
+
} catch (interactTimeoutErr) {
|
|
3401
|
+
if (forceDebug) console.log(formatLogMessage('debug', `[interaction] Aborted after ${INTERACTION_HARD_TIMEOUT}ms: ${interactTimeoutErr.message}`));
|
|
3402
|
+
}
|
|
3379
3403
|
}
|
|
3380
3404
|
|
|
3381
3405
|
const delayMs = DEFAULT_DELAY;
|
|
@@ -3799,6 +3823,20 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3799
3823
|
});
|
|
3800
3824
|
} catch (gcErr) { /* ignore */ }
|
|
3801
3825
|
|
|
3826
|
+
// Force screenshot — always capture regardless of success/failure
|
|
3827
|
+
if (siteConfig.screenshot === 'force' && page && !page.isClosed()) {
|
|
3828
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
3829
|
+
const safeUrl = currentUrl.replace(/https?:\/\//, '').replace(/[^a-zA-Z0-9]/g, '_').substring(0, 80);
|
|
3830
|
+
const filename = `screenshots/${safeUrl}-${timestamp}.png`;
|
|
3831
|
+
try {
|
|
3832
|
+
if (!fs.existsSync('screenshots')) fs.mkdirSync('screenshots', { recursive: true });
|
|
3833
|
+
await page.screenshot({ path: filename, type: 'png', fullPage: true });
|
|
3834
|
+
console.log(formatLogMessage('info', `Screenshot saved: ${filename}`));
|
|
3835
|
+
} catch (screenshotErr) {
|
|
3836
|
+
console.warn(messageColors.warn(`[screenshot failed] ${currentUrl}: ${screenshotErr.message}`));
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3802
3840
|
try {
|
|
3803
3841
|
await page.close();
|
|
3804
3842
|
if (forceDebug) console.log(formatLogMessage('debug', `Page closed for ${currentUrl}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanboynz/network-scanner",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.53",
|
|
4
4
|
"description": "A Puppeteer-based network scanner for analyzing web traffic, generating adblock filter rules, and identifying third-party requests. Features include fingerprint spoofing, Cloudflare bypass, content analysis with curl/grep, and multiple output formats.",
|
|
5
5
|
"main": "nwss.js",
|
|
6
6
|
"scripts": {
|