@fanboynz/network-scanner 2.0.52 → 2.0.54
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 +519 -170
- package/lib/nettools.js +68 -61
- package/nwss.js +24 -14
- 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,31 +996,89 @@ 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
|
-
// WebGL
|
|
1009
|
+
// WebGL context patching — Proxy wrapper for real contexts + null-context safety
|
|
847
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
|
+
|
|
848
1017
|
const originalGetContext = HTMLCanvasElement.prototype.getContext;
|
|
849
1018
|
HTMLCanvasElement.prototype.getContext = function(type, attrs) {
|
|
850
1019
|
const ctx = originalGetContext.call(this, type, attrs);
|
|
851
|
-
if (
|
|
852
|
-
|
|
1020
|
+
if (type !== 'webgl' && type !== 'experimental-webgl' && type !== 'webgl2') return ctx;
|
|
1021
|
+
|
|
853
1022
|
const noop = () => {};
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1023
|
+
|
|
1024
|
+
// Null context — return mock to prevent crashes
|
|
1025
|
+
if (ctx === null) {
|
|
1026
|
+
const canvasEl = this; // capture the actual canvas element
|
|
1027
|
+
const mock = new Proxy({}, {
|
|
1028
|
+
get(target, prop) {
|
|
1029
|
+
if (prop === 'getShaderPrecisionFormat') return () => ({ rangeMin: 127, rangeMax: 127, precision: 23 });
|
|
1030
|
+
if (prop === 'getParameter') return (p) => webglSpoofParams[p] || 0;
|
|
1031
|
+
if (prop === 'getSupportedExtensions') return () => [];
|
|
1032
|
+
if (prop === 'getExtension') return (name) => {
|
|
1033
|
+
if (name === 'WEBGL_debug_renderer_info') return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
|
|
1034
|
+
return null;
|
|
1035
|
+
};
|
|
1036
|
+
if (prop === 'getContextAttributes') return () => ({
|
|
1037
|
+
alpha: true, antialias: true, depth: true, failIfMajorPerformanceCaveat: false,
|
|
1038
|
+
desynchronized: false, premultipliedAlpha: true, preserveDrawingBuffer: false,
|
|
1039
|
+
powerPreference: 'default', stencil: false, xrCompatible: false
|
|
1040
|
+
});
|
|
1041
|
+
if (prop === 'isContextLost') return () => false;
|
|
1042
|
+
if (prop === 'canvas') return canvasEl;
|
|
1043
|
+
if (prop === 'drawingBufferWidth') return canvasEl.width || 1920;
|
|
1044
|
+
if (prop === 'drawingBufferHeight') return canvasEl.height || 1080;
|
|
1045
|
+
if (prop === 'drawingBufferColorSpace') return 'srgb';
|
|
1046
|
+
// Identity — let prototype chain handle constructor/toString/Symbol.toStringTag
|
|
1047
|
+
if (prop === 'constructor') return WebGLRenderingContext;
|
|
1048
|
+
if (prop === Symbol.toStringTag) return 'WebGLRenderingContext';
|
|
1049
|
+
// Common draw/state methods — return noop to prevent crashes
|
|
1050
|
+
return noop;
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
// Make mock pass instanceof checks
|
|
1054
|
+
if (window.WebGLRenderingContext) {
|
|
1055
|
+
Object.setPrototypeOf(mock, WebGLRenderingContext.prototype);
|
|
1056
|
+
}
|
|
1057
|
+
return mock;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Real context — wrap in Proxy to intercept getParameter/getExtension
|
|
1061
|
+
// Direct property assignment fails silently on native WebGL objects
|
|
1062
|
+
return new Proxy(ctx, {
|
|
1063
|
+
get(target, prop, receiver) {
|
|
1064
|
+
if (prop === 'getParameter') {
|
|
1065
|
+
return function(param) {
|
|
1066
|
+
if (webglSpoofParams.hasOwnProperty(param)) return webglSpoofParams[param];
|
|
1067
|
+
return target.getParameter(param);
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
if (prop === 'getExtension') {
|
|
1071
|
+
return function(name) {
|
|
1072
|
+
if (name === 'WEBGL_debug_renderer_info') return debugRendererExt;
|
|
1073
|
+
return target.getExtension(name);
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
const val = Reflect.get(target, prop, receiver);
|
|
1077
|
+
return typeof val === 'function' ? val.bind(target) : val;
|
|
864
1078
|
}
|
|
865
1079
|
});
|
|
866
1080
|
};
|
|
867
|
-
}, 'WebGL
|
|
1081
|
+
}, 'WebGL context patching'); // Permissions API spoofing
|
|
868
1082
|
//
|
|
869
1083
|
safeExecute(() => {
|
|
870
1084
|
if (navigator.permissions?.query) {
|
|
@@ -916,6 +1130,118 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
916
1130
|
};
|
|
917
1131
|
}
|
|
918
1132
|
}, 'media device spoofing');
|
|
1133
|
+
|
|
1134
|
+
// Window dimensions — headless Chrome reports 0 for outer dimensions
|
|
1135
|
+
safeExecute(() => {
|
|
1136
|
+
if (!window.outerWidth || window.outerWidth === 0 || window.outerWidth === window.innerWidth) {
|
|
1137
|
+
Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth + 16, configurable: true });
|
|
1138
|
+
}
|
|
1139
|
+
if (!window.outerHeight || window.outerHeight === 0 || window.outerHeight === window.innerHeight) {
|
|
1140
|
+
Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight + 88, configurable: true });
|
|
1141
|
+
}
|
|
1142
|
+
if (window.screenX === 0 && window.screenY === 0) {
|
|
1143
|
+
const sX = Math.floor(Math.random() * 200);
|
|
1144
|
+
const sY = Math.floor(Math.random() * 50) + 20;
|
|
1145
|
+
Object.defineProperty(window, 'screenX', { get: () => sX });
|
|
1146
|
+
Object.defineProperty(window, 'screenY', { get: () => sY });
|
|
1147
|
+
}
|
|
1148
|
+
}, 'window dimension spoofing');
|
|
1149
|
+
|
|
1150
|
+
// navigator.connection — missing or incomplete in headless
|
|
1151
|
+
safeExecute(() => {
|
|
1152
|
+
if (!navigator.connection) {
|
|
1153
|
+
Object.defineProperty(navigator, 'connection', {
|
|
1154
|
+
get: () => ({
|
|
1155
|
+
effectiveType: '4g',
|
|
1156
|
+
rtt: 50,
|
|
1157
|
+
downlink: 10,
|
|
1158
|
+
saveData: false,
|
|
1159
|
+
type: 'wifi',
|
|
1160
|
+
addEventListener: () => {},
|
|
1161
|
+
removeEventListener: () => {}
|
|
1162
|
+
})
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
}, 'connection API spoofing');
|
|
1166
|
+
|
|
1167
|
+
// navigator.pdfViewerEnabled — missing in headless, true in real Chrome
|
|
1168
|
+
safeExecute(() => {
|
|
1169
|
+
if (navigator.pdfViewerEnabled === undefined) {
|
|
1170
|
+
Object.defineProperty(navigator, 'pdfViewerEnabled', {
|
|
1171
|
+
get: () => true, configurable: true
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
}, 'pdfViewerEnabled spoofing');
|
|
1175
|
+
|
|
1176
|
+
// speechSynthesis — headless returns empty voices array
|
|
1177
|
+
safeExecute(() => {
|
|
1178
|
+
if (window.speechSynthesis) {
|
|
1179
|
+
const origGetVoices = speechSynthesis.getVoices.bind(speechSynthesis);
|
|
1180
|
+
speechSynthesis.getVoices = function() {
|
|
1181
|
+
const voices = origGetVoices();
|
|
1182
|
+
if (voices.length === 0) {
|
|
1183
|
+
return [{
|
|
1184
|
+
default: true, lang: 'en-US', localService: true,
|
|
1185
|
+
name: 'Microsoft David - English (United States)', voiceURI: 'Microsoft David - English (United States)'
|
|
1186
|
+
}, {
|
|
1187
|
+
default: false, lang: 'en-US', localService: true,
|
|
1188
|
+
name: 'Microsoft Zira - English (United States)', voiceURI: 'Microsoft Zira - English (United States)'
|
|
1189
|
+
}];
|
|
1190
|
+
}
|
|
1191
|
+
return voices;
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
}, 'speechSynthesis spoofing');
|
|
1195
|
+
|
|
1196
|
+
// AudioContext — headless has distinct audio processing fingerprint
|
|
1197
|
+
safeExecute(() => {
|
|
1198
|
+
if (window.AudioContext || window.webkitAudioContext) {
|
|
1199
|
+
const OrigAudioContext = window.AudioContext || window.webkitAudioContext;
|
|
1200
|
+
const origCreateOscillator = OrigAudioContext.prototype.createOscillator;
|
|
1201
|
+
const origCreateDynamicsCompressor = OrigAudioContext.prototype.createDynamicsCompressor;
|
|
1202
|
+
|
|
1203
|
+
// Inject deterministic noise into audio output — consistent per session
|
|
1204
|
+
const audioNoiseSeed = Math.random() * 0.01 - 0.005;
|
|
1205
|
+
const compNoiseSeed = Math.random() * 0.1 - 0.05;
|
|
1206
|
+
|
|
1207
|
+
OrigAudioContext.prototype.createOscillator = function() {
|
|
1208
|
+
const osc = origCreateOscillator.call(this);
|
|
1209
|
+
const origFreq = osc.frequency.value;
|
|
1210
|
+
osc.frequency.value = origFreq + audioNoiseSeed;
|
|
1211
|
+
return osc;
|
|
1212
|
+
};
|
|
1213
|
+
OrigAudioContext.prototype.createDynamicsCompressor = function() {
|
|
1214
|
+
const comp = origCreateDynamicsCompressor.call(this);
|
|
1215
|
+
const origThreshold = comp.threshold.value;
|
|
1216
|
+
comp.threshold.value = origThreshold + compNoiseSeed;
|
|
1217
|
+
return comp;
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
}, 'AudioContext fingerprint spoofing');
|
|
1221
|
+
|
|
1222
|
+
// Broken image dimensions — headless returns 0x0, real browsers show broken icon
|
|
1223
|
+
safeExecute(() => {
|
|
1224
|
+
// Patch Image constructor to intercept broken image detection
|
|
1225
|
+
const OrigImage = window.Image;
|
|
1226
|
+
window.Image = function(w, h) {
|
|
1227
|
+
const img = arguments.length ? new OrigImage(w, h) : new OrigImage();
|
|
1228
|
+
// After load attempt, if broken, fake dimensions
|
|
1229
|
+
const origWidthDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'width');
|
|
1230
|
+
const origHeightDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'height');
|
|
1231
|
+
|
|
1232
|
+
// Monitor for broken state
|
|
1233
|
+
img.addEventListener('error', function() {
|
|
1234
|
+
// Force non-zero dimensions for broken images
|
|
1235
|
+
Object.defineProperty(this, 'width', { get: () => 24, configurable: true });
|
|
1236
|
+
Object.defineProperty(this, 'height', { get: () => 24, configurable: true });
|
|
1237
|
+
Object.defineProperty(this, 'naturalWidth', { get: () => 24, configurable: true });
|
|
1238
|
+
Object.defineProperty(this, 'naturalHeight', { get: () => 24, configurable: true });
|
|
1239
|
+
});
|
|
1240
|
+
return img;
|
|
1241
|
+
};
|
|
1242
|
+
window.Image.prototype = OrigImage.prototype;
|
|
1243
|
+
Object.defineProperty(window, 'Image', { writable: true, configurable: true });
|
|
1244
|
+
}, 'broken image dimension spoofing');
|
|
919
1245
|
|
|
920
1246
|
// Fetch Request Headers Normalization
|
|
921
1247
|
//
|
|
@@ -959,21 +1285,9 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
959
1285
|
|
|
960
1286
|
// CSS Media Query Spoofing
|
|
961
1287
|
//
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
const result = originalMatchMedia.call(this, query);
|
|
966
|
-
// Add slight randomization to avoid fingerprinting for device queries
|
|
967
|
-
if (query.includes('device-width') || query.includes('device-height') ||
|
|
968
|
-
query.includes('aspect-ratio') || query.includes('color-gamut')) {
|
|
969
|
-
Object.defineProperty(result, 'matches', {
|
|
970
|
-
get: () => Math.random() > 0.1 ? originalMatchMedia.call(window, query).matches : !originalMatchMedia.call(window, query).matches,
|
|
971
|
-
configurable: true
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
return result;
|
|
975
|
-
};
|
|
976
|
-
}, 'CSS media query spoofing');
|
|
1288
|
+
// CSS media query — pass through normally. Screen dimensions are already spoofed
|
|
1289
|
+
// so matchMedia results will naturally reflect the spoofed values.
|
|
1290
|
+
// No modification needed here.
|
|
977
1291
|
|
|
978
1292
|
// Enhanced WebRTC Spoofing
|
|
979
1293
|
//
|
|
@@ -982,13 +1296,41 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
982
1296
|
const OriginalRTC = window.RTCPeerConnection;
|
|
983
1297
|
window.RTCPeerConnection = function(...args) {
|
|
984
1298
|
const pc = new OriginalRTC(...args);
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1299
|
+
|
|
1300
|
+
// Intercept onicecandidate to strip local IP addresses
|
|
1301
|
+
const origAddEventListener = pc.addEventListener.bind(pc);
|
|
1302
|
+
pc.addEventListener = function(type, listener, ...rest) {
|
|
1303
|
+
if (type === 'icecandidate') {
|
|
1304
|
+
const wrappedListener = function(event) {
|
|
1305
|
+
if (event.candidate && event.candidate.candidate) {
|
|
1306
|
+
// Strip candidates containing local/private IPs
|
|
1307
|
+
const c = event.candidate.candidate;
|
|
1308
|
+
if (c.includes('.local') || /(?:10|172\.(?:1[6-9]|2\d|3[01])|192\.168)\./.test(c)) {
|
|
1309
|
+
return; // suppress local IP candidates
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
listener.call(this, event);
|
|
1313
|
+
};
|
|
1314
|
+
return origAddEventListener(type, wrappedListener, ...rest);
|
|
1315
|
+
}
|
|
1316
|
+
return origAddEventListener(type, listener, ...rest);
|
|
988
1317
|
};
|
|
1318
|
+
|
|
1319
|
+
// Also intercept the property-based handler
|
|
1320
|
+
let _onicecandidateHandler = null;
|
|
1321
|
+
Object.defineProperty(pc, 'onicecandidate', {
|
|
1322
|
+
get: () => _onicecandidateHandler,
|
|
1323
|
+
set: (handler) => {
|
|
1324
|
+
_onicecandidateHandler = handler;
|
|
1325
|
+
// No-op — the addEventListener wrapper above handles filtering
|
|
1326
|
+
},
|
|
1327
|
+
configurable: true
|
|
1328
|
+
});
|
|
1329
|
+
|
|
989
1330
|
return pc;
|
|
990
1331
|
};
|
|
991
1332
|
Object.setPrototypeOf(window.RTCPeerConnection, OriginalRTC);
|
|
1333
|
+
window.RTCPeerConnection.prototype = OriginalRTC.prototype;
|
|
992
1334
|
}
|
|
993
1335
|
}, 'WebRTC spoofing');
|
|
994
1336
|
|
|
@@ -1060,62 +1402,15 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1060
1402
|
|
|
1061
1403
|
CanvasRenderingContext2D.prototype.measureText = function(text) {
|
|
1062
1404
|
const result = originalMeasureText.call(this, text);
|
|
1063
|
-
//
|
|
1405
|
+
// Deterministic slight noise to text measurements
|
|
1064
1406
|
const originalWidth = result.width;
|
|
1407
|
+
const hash = text.length * 7 + (text.charCodeAt(0) || 0);
|
|
1408
|
+
const noise = ((hash * 2654435761 >>> 0) % 100 - 50) / 500; // -0.1 to +0.1, deterministic per text
|
|
1065
1409
|
return Object.create(result, {
|
|
1066
|
-
width: { get: () => originalWidth +
|
|
1410
|
+
width: { get: () => originalWidth + noise }
|
|
1067
1411
|
});
|
|
1068
1412
|
};
|
|
1069
1413
|
|
|
1070
|
-
// Comprehensive canvas fingerprinting protection
|
|
1071
|
-
//
|
|
1072
|
-
const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
|
1073
|
-
CanvasRenderingContext2D.prototype.getImageData = function(sx, sy, sw, sh) {
|
|
1074
|
-
const imageData = originalGetImageData.call(this, sx, sy, sw, sh);
|
|
1075
|
-
// Skip noise on large canvases (>500K pixels) � too expensive for minimal fingerprint benefit
|
|
1076
|
-
if (imageData.data.length > 2000000) {
|
|
1077
|
-
return imageData;
|
|
1078
|
-
}
|
|
1079
|
-
// Add subtle noise to pixel data
|
|
1080
|
-
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
1081
|
-
if (Math.random() < 0.1) { // 10% chance to modify each pixel
|
|
1082
|
-
imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + Math.floor(Math.random() * 3) - 1));
|
|
1083
|
-
imageData.data[i + 1] = Math.max(0, Math.min(255, imageData.data[i + 1] + Math.floor(Math.random() * 3) - 1));
|
|
1084
|
-
imageData.data[i + 2] = Math.max(0, Math.min(255, imageData.data[i + 2] + Math.floor(Math.random() * 3) - 1));
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
return imageData;
|
|
1088
|
-
};
|
|
1089
|
-
|
|
1090
|
-
// WebGL canvas context fingerprinting
|
|
1091
|
-
const originalGetContext = HTMLCanvasElement.prototype.getContext;
|
|
1092
|
-
HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) {
|
|
1093
|
-
const context = originalGetContext.call(this, contextType, contextAttributes);
|
|
1094
|
-
|
|
1095
|
-
if (contextType === 'webgl' || contextType === 'webgl2' || contextType === 'experimental-webgl') {
|
|
1096
|
-
// Override WebGL-specific fingerprinting methods
|
|
1097
|
-
const originalGetShaderPrecisionFormat = context.getShaderPrecisionFormat;
|
|
1098
|
-
context.getShaderPrecisionFormat = function(shaderType, precisionType) {
|
|
1099
|
-
return {
|
|
1100
|
-
rangeMin: 127,
|
|
1101
|
-
rangeMax: 127,
|
|
1102
|
-
precision: 23
|
|
1103
|
-
};
|
|
1104
|
-
};
|
|
1105
|
-
|
|
1106
|
-
const originalGetExtension = context.getExtension;
|
|
1107
|
-
context.getExtension = function(name) {
|
|
1108
|
-
// Block access to fingerprinting-sensitive extensions
|
|
1109
|
-
if (name === 'WEBGL_debug_renderer_info') {
|
|
1110
|
-
return null;
|
|
1111
|
-
}
|
|
1112
|
-
return originalGetExtension.call(this, name);
|
|
1113
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
return context;
|
|
1117
|
-
};
|
|
1118
|
-
|
|
1119
1414
|
// Override font detection methods
|
|
1120
1415
|
//
|
|
1121
1416
|
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth');
|
|
@@ -1125,7 +1420,10 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1125
1420
|
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
|
1126
1421
|
get: function() {
|
|
1127
1422
|
if (this.style && this.style.fontFamily) {
|
|
1128
|
-
|
|
1423
|
+
const w = originalOffsetWidth.get.call(this);
|
|
1424
|
+
// Deterministic per font family — same font always gets same offset
|
|
1425
|
+
const fontHash = this.style.fontFamily.split('').reduce((a, c) => (a * 31 + c.charCodeAt(0)) & 0xffff, 0);
|
|
1426
|
+
return Math.floor(w + ((fontHash % 3) - 1));
|
|
1129
1427
|
}
|
|
1130
1428
|
return originalOffsetWidth.get.call(this);
|
|
1131
1429
|
},
|
|
@@ -1146,24 +1444,75 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1146
1444
|
// Canvas fingerprinting protection
|
|
1147
1445
|
//
|
|
1148
1446
|
safeExecute(() => {
|
|
1447
|
+
// Per-session seed — consistent within session, varies between sessions
|
|
1448
|
+
const canvasSeed = Math.floor(Math.random() * 2147483647);
|
|
1449
|
+
|
|
1450
|
+
// Simple seeded PRNG — deterministic for same input
|
|
1451
|
+
function seededNoise(seed) {
|
|
1452
|
+
let s = seed ^ 0x5DEECE66D;
|
|
1453
|
+
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
|
1454
|
+
return s;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
|
1458
|
+
CanvasRenderingContext2D.prototype.getImageData = function(sx, sy, sw, sh) {
|
|
1459
|
+
const imageData = originalGetImageData.call(this, sx, sy, sw, sh);
|
|
1460
|
+
if (imageData.data.length > 2000000) return imageData;
|
|
1461
|
+
// Deterministic noise — same canvas content + position = same noise every call
|
|
1462
|
+
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
1463
|
+
const n = seededNoise(canvasSeed ^ (i >> 2));
|
|
1464
|
+
if ((n & 0xf) < 2) { // ~12.5% of pixels
|
|
1465
|
+
const shift = (n >> 4 & 3) - 1; // -1, 0, or +1
|
|
1466
|
+
imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + shift));
|
|
1467
|
+
imageData.data[i + 1] = Math.max(0, Math.min(255, imageData.data[i + 1] + shift));
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return imageData;
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1149
1473
|
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
1150
1474
|
HTMLCanvasElement.prototype.toDataURL = function(...args) {
|
|
1151
|
-
//
|
|
1475
|
+
// Apply same deterministic noise by reading through spoofed getImageData
|
|
1476
|
+
try {
|
|
1477
|
+
const ctx = this.getContext('2d');
|
|
1478
|
+
if (ctx && this.width > 0 && this.height > 0 && this.width * this.height < 500000) {
|
|
1479
|
+
const imageData = ctx.getImageData(0, 0, this.width, this.height);
|
|
1480
|
+
ctx.putImageData(imageData, 0, 0);
|
|
1481
|
+
}
|
|
1482
|
+
} catch (e) {} // WebGL or other context — skip
|
|
1152
1483
|
return originalToDataURL.apply(this, args);
|
|
1153
1484
|
};
|
|
1485
|
+
|
|
1486
|
+
const originalToBlob = HTMLCanvasElement.prototype.toBlob;
|
|
1487
|
+
if (originalToBlob) {
|
|
1488
|
+
HTMLCanvasElement.prototype.toBlob = function(callback, ...args) {
|
|
1489
|
+
try {
|
|
1490
|
+
const ctx = this.getContext('2d');
|
|
1491
|
+
if (ctx && this.width > 0 && this.height > 0 && this.width * this.height < 500000) {
|
|
1492
|
+
const imageData = ctx.getImageData(0, 0, this.width, this.height);
|
|
1493
|
+
ctx.putImageData(imageData, 0, 0);
|
|
1494
|
+
}
|
|
1495
|
+
} catch (e) {}
|
|
1496
|
+
return originalToBlob.call(this, callback, ...args);
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1154
1499
|
}, 'canvas fingerprinting protection');
|
|
1155
1500
|
|
|
1156
1501
|
// Battery API spoofing
|
|
1157
1502
|
//
|
|
1158
1503
|
safeExecute(() => {
|
|
1159
1504
|
if (navigator.getBattery) {
|
|
1505
|
+
const batteryState = {
|
|
1506
|
+
charging: Math.random() > 0.5,
|
|
1507
|
+
chargingTime: Math.random() > 0.5 ? Infinity : Math.floor(Math.random() * 3600),
|
|
1508
|
+
dischargingTime: Math.floor(Math.random() * 7200),
|
|
1509
|
+
level: Math.round((Math.random() * 0.7 + 0.25) * 100) / 100,
|
|
1510
|
+
addEventListener: () => {},
|
|
1511
|
+
removeEventListener: () => {},
|
|
1512
|
+
dispatchEvent: () => true
|
|
1513
|
+
};
|
|
1160
1514
|
navigator.getBattery = function() {
|
|
1161
|
-
return Promise.resolve(
|
|
1162
|
-
charging: Math.random() > 0.5,
|
|
1163
|
-
chargingTime: Math.random() > 0.5 ? Infinity : Math.random() * 3600,
|
|
1164
|
-
dischargingTime: Math.random() * 7200,
|
|
1165
|
-
level: Math.random() * 0.99 + 0.01
|
|
1166
|
-
});
|
|
1515
|
+
return Promise.resolve(batteryState);
|
|
1167
1516
|
};
|
|
1168
1517
|
}
|
|
1169
1518
|
}, 'battery API spoofing');
|
|
@@ -1286,7 +1635,7 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1286
1635
|
});
|
|
1287
1636
|
}, 'location URL masking');
|
|
1288
1637
|
|
|
1289
|
-
}, ua, forceDebug);
|
|
1638
|
+
}, ua, forceDebug, selectedGpu);
|
|
1290
1639
|
} catch (stealthErr) {
|
|
1291
1640
|
if (stealthErr.message.includes('Session closed') ||
|
|
1292
1641
|
stealthErr.message.includes('addScriptToEvaluateOnNewDocument timed out') ||
|
package/lib/nettools.js
CHANGED
|
@@ -11,6 +11,20 @@ const execPromise = util.promisify(exec);
|
|
|
11
11
|
// Cycling index for whois server rotation
|
|
12
12
|
let whoisServerCycleIndex = 0;
|
|
13
13
|
|
|
14
|
+
// Global dig result cache — shared across ALL handler instances and processUrl calls
|
|
15
|
+
// Key: `${domain}-${recordType}`, Value: { result, timestamp }
|
|
16
|
+
// DNS records don't change based on what terms you're searching for,
|
|
17
|
+
// so we cache the raw dig output and let each handler check its own terms against it
|
|
18
|
+
const globalDigResultCache = new Map();
|
|
19
|
+
const GLOBAL_DIG_CACHE_TTL = 300000; // 5 minutes
|
|
20
|
+
const GLOBAL_DIG_CACHE_MAX = 500;
|
|
21
|
+
|
|
22
|
+
// Global whois result cache — shared across ALL handler instances and processUrl calls
|
|
23
|
+
// Whois data is per root domain and doesn't change based on search terms
|
|
24
|
+
const globalWhoisResultCache = new Map();
|
|
25
|
+
const GLOBAL_WHOIS_CACHE_TTL = 900000; // 15 minutes (whois data changes less frequently)
|
|
26
|
+
const GLOBAL_WHOIS_CACHE_MAX = 500;
|
|
27
|
+
|
|
14
28
|
/**
|
|
15
29
|
* Strips ANSI color codes from a string for clean file logging
|
|
16
30
|
* @param {string} text - Text that may contain ANSI codes
|
|
@@ -26,27 +40,28 @@ function stripAnsiColors(text) {
|
|
|
26
40
|
* @returns {Object} Object with isAvailable boolean and version/error info
|
|
27
41
|
*/
|
|
28
42
|
function validateWhoisAvailability() {
|
|
43
|
+
if (validateWhoisAvailability._cached) return validateWhoisAvailability._cached;
|
|
29
44
|
try {
|
|
30
45
|
const result = execSync('whois --version 2>&1', { encoding: 'utf8' });
|
|
31
|
-
|
|
46
|
+
validateWhoisAvailability._cached = {
|
|
32
47
|
isAvailable: true,
|
|
33
48
|
version: result.trim()
|
|
34
49
|
};
|
|
35
50
|
} catch (error) {
|
|
36
|
-
// Some systems don't have --version, try just whois
|
|
37
51
|
try {
|
|
38
52
|
execSync('which whois', { encoding: 'utf8' });
|
|
39
|
-
|
|
53
|
+
validateWhoisAvailability._cached = {
|
|
40
54
|
isAvailable: true,
|
|
41
55
|
version: 'whois (version unknown)'
|
|
42
56
|
};
|
|
43
57
|
} catch (e) {
|
|
44
|
-
|
|
58
|
+
validateWhoisAvailability._cached = {
|
|
45
59
|
isAvailable: false,
|
|
46
60
|
error: 'whois command not found'
|
|
47
61
|
};
|
|
48
62
|
}
|
|
49
63
|
}
|
|
64
|
+
return validateWhoisAvailability._cached;
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
/**
|
|
@@ -54,18 +69,20 @@ function validateWhoisAvailability() {
|
|
|
54
69
|
* @returns {Object} Object with isAvailable boolean and version/error info
|
|
55
70
|
*/
|
|
56
71
|
function validateDigAvailability() {
|
|
72
|
+
if (validateDigAvailability._cached) return validateDigAvailability._cached;
|
|
57
73
|
try {
|
|
58
74
|
const result = execSync('dig -v 2>&1', { encoding: 'utf8' });
|
|
59
|
-
|
|
75
|
+
validateDigAvailability._cached = {
|
|
60
76
|
isAvailable: true,
|
|
61
77
|
version: result.split('\n')[0].trim()
|
|
62
78
|
};
|
|
63
79
|
} catch (error) {
|
|
64
|
-
|
|
80
|
+
validateDigAvailability._cached = {
|
|
65
81
|
isAvailable: false,
|
|
66
82
|
error: 'dig command not found'
|
|
67
83
|
};
|
|
68
84
|
}
|
|
85
|
+
return validateDigAvailability._cached;
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
/**
|
|
@@ -635,8 +652,8 @@ async function digLookup(domain = '', recordType = 'A', timeout = 5000) {
|
|
|
635
652
|
// Clean domain
|
|
636
653
|
const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/\/.*$/, '').replace(/:\d+$/, '');
|
|
637
654
|
|
|
638
|
-
//
|
|
639
|
-
const { stdout, stderr } = await execWithTimeout(`dig
|
|
655
|
+
// Single dig command — full output contains everything including the short answers
|
|
656
|
+
const { stdout: fullOutput, stderr } = await execWithTimeout(`dig "${cleanDomain}" ${recordType}`, timeout);
|
|
640
657
|
|
|
641
658
|
if (stderr && stderr.trim()) {
|
|
642
659
|
return {
|
|
@@ -647,13 +664,21 @@ async function digLookup(domain = '', recordType = 'A', timeout = 5000) {
|
|
|
647
664
|
};
|
|
648
665
|
}
|
|
649
666
|
|
|
650
|
-
//
|
|
651
|
-
const
|
|
667
|
+
// Extract short output from ANSWER SECTION of full dig output
|
|
668
|
+
const answerMatch = fullOutput.match(/;; ANSWER SECTION:\n([\s\S]*?)(?:\n;;|\n*$)/);
|
|
669
|
+
let shortOutput = '';
|
|
670
|
+
if (answerMatch) {
|
|
671
|
+
shortOutput = answerMatch[1]
|
|
672
|
+
.split('\n')
|
|
673
|
+
.map(line => line.split(/\s+/).pop())
|
|
674
|
+
.filter(Boolean)
|
|
675
|
+
.join('\n');
|
|
676
|
+
}
|
|
652
677
|
|
|
653
678
|
return {
|
|
654
679
|
success: true,
|
|
655
680
|
output: fullOutput,
|
|
656
|
-
shortOutput
|
|
681
|
+
shortOutput,
|
|
657
682
|
domain: cleanDomain,
|
|
658
683
|
recordType
|
|
659
684
|
};
|
|
@@ -766,7 +791,6 @@ function createNetToolsHandler(config) {
|
|
|
766
791
|
whoisDelay = 4000,
|
|
767
792
|
whoisServer,
|
|
768
793
|
whoisServerMode = 'random',
|
|
769
|
-
bufferedLogWrite = null,
|
|
770
794
|
debugLogFile = null,
|
|
771
795
|
digTerms,
|
|
772
796
|
digOrTerms,
|
|
@@ -807,20 +831,10 @@ function createNetToolsHandler(config) {
|
|
|
807
831
|
subdomain: digSubdomain
|
|
808
832
|
});
|
|
809
833
|
|
|
810
|
-
//
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
// Size Memory
|
|
815
|
-
// 100 ~900KB
|
|
816
|
-
// 200 1.8MB
|
|
817
|
-
// 300 2.6MB
|
|
818
|
-
// 400 3.4MB
|
|
819
|
-
// 500 4.2MB
|
|
820
|
-
// Add DNS resolution caching to avoid redundant dig lookups
|
|
821
|
-
const digResultCache = new Map();
|
|
822
|
-
const DIG_CACHE_TTL = 300000; // 5 minutes cache TTL
|
|
823
|
-
const DIG_MAX_CACHE_SIZE = 400; // Smaller cache for dig due to shorter TTL
|
|
834
|
+
// Whois cache is global (globalWhoisResultCache) — shared across all handler instances
|
|
835
|
+
// Whois data is per root domain and doesn't change based on search terms
|
|
836
|
+
// Dig cache is global (globalDigResultCache) — shared across all handler instances
|
|
837
|
+
// DNS results are the same regardless of search terms
|
|
824
838
|
|
|
825
839
|
return async function handleNetToolsCheck(domain, fullSubdomain) {
|
|
826
840
|
// Use fullSubdomain parameter instead of originalDomain to maintain consistency
|
|
@@ -844,19 +858,17 @@ function createNetToolsHandler(config) {
|
|
|
844
858
|
|
|
845
859
|
// Move the logToConsoleAndFile function declaration from later in the file to here:
|
|
846
860
|
function logToConsoleAndFile(message) {
|
|
861
|
+
// Note: This function needs access to forceDebug, debugLogFile, and fs from the parent scope
|
|
862
|
+
// These are passed in via the config object to createNetToolsHandler
|
|
863
|
+
// forceDebug, debugLogFile, and fs are available in this closure
|
|
864
|
+
|
|
865
|
+
// Always log to console when in debug mode
|
|
847
866
|
if (forceDebug) {
|
|
848
867
|
console.log(formatLogMessage('debug', message));
|
|
849
868
|
}
|
|
850
869
|
|
|
851
|
-
if
|
|
852
|
-
|
|
853
|
-
const timestamp = new Date().toISOString();
|
|
854
|
-
const cleanMessage = stripAnsiColors(message);
|
|
855
|
-
bufferedLogWrite(debugLogFile, `${timestamp} [debug nettools] ${cleanMessage}\n`);
|
|
856
|
-
} catch (logErr) {
|
|
857
|
-
// Silently fail file logging to avoid disrupting whois operations
|
|
858
|
-
}
|
|
859
|
-
} else if (debugLogFile && fs) {
|
|
870
|
+
// Also log to file if debug file logging is enabled
|
|
871
|
+
if (debugLogFile && fs) {
|
|
860
872
|
try {
|
|
861
873
|
const timestamp = new Date().toISOString();
|
|
862
874
|
const cleanMessage = stripAnsiColors(message);
|
|
@@ -982,9 +994,9 @@ function createNetToolsHandler(config) {
|
|
|
982
994
|
const now = Date.now();
|
|
983
995
|
let whoisResult = null;
|
|
984
996
|
|
|
985
|
-
if (
|
|
986
|
-
const cachedEntry =
|
|
987
|
-
if (now - cachedEntry.timestamp <
|
|
997
|
+
if (globalWhoisResultCache.has(whoisCacheKey)) {
|
|
998
|
+
const cachedEntry = globalWhoisResultCache.get(whoisCacheKey);
|
|
999
|
+
if (now - cachedEntry.timestamp < GLOBAL_WHOIS_CACHE_TTL) {
|
|
988
1000
|
if (forceDebug) {
|
|
989
1001
|
const age = Math.round((now - cachedEntry.timestamp) / 1000);
|
|
990
1002
|
const serverInfo = (selectedServer && selectedServer !== '') ? ` (server: ${selectedServer})` : ' (default server)';
|
|
@@ -998,7 +1010,7 @@ function createNetToolsHandler(config) {
|
|
|
998
1010
|
});
|
|
999
1011
|
} else {
|
|
1000
1012
|
// Cache expired, remove it
|
|
1001
|
-
|
|
1013
|
+
globalWhoisResultCache.delete(whoisCacheKey);
|
|
1002
1014
|
if (forceDebug) {
|
|
1003
1015
|
logToConsoleAndFile(`${messageColors.highlight('[whois-cache]')} Cache expired for ${whoisRootDomain}, performing fresh lookup`);
|
|
1004
1016
|
}
|
|
@@ -1030,7 +1042,7 @@ function createNetToolsHandler(config) {
|
|
|
1030
1042
|
!whoisResult.error.toLowerCase().includes('connection') &&
|
|
1031
1043
|
!whoisResult.error.toLowerCase().includes('network'))) {
|
|
1032
1044
|
|
|
1033
|
-
|
|
1045
|
+
globalWhoisResultCache.set(whoisCacheKey, {
|
|
1034
1046
|
result: whoisResult,
|
|
1035
1047
|
timestamp: now
|
|
1036
1048
|
});
|
|
@@ -1183,17 +1195,17 @@ function createNetToolsHandler(config) {
|
|
|
1183
1195
|
}
|
|
1184
1196
|
|
|
1185
1197
|
// Periodic whois cache cleanup to prevent memory leaks
|
|
1186
|
-
if (
|
|
1198
|
+
if (globalWhoisResultCache.size > GLOBAL_WHOIS_CACHE_MAX) {
|
|
1187
1199
|
const now = Date.now();
|
|
1188
1200
|
let cleanedCount = 0;
|
|
1189
|
-
for (const [key, entry] of
|
|
1190
|
-
if (now - entry.timestamp >
|
|
1191
|
-
|
|
1201
|
+
for (const [key, entry] of globalWhoisResultCache.entries()) {
|
|
1202
|
+
if (now - entry.timestamp > GLOBAL_WHOIS_CACHE_TTL) {
|
|
1203
|
+
globalWhoisResultCache.delete(key);
|
|
1192
1204
|
cleanedCount++;
|
|
1193
1205
|
}
|
|
1194
1206
|
}
|
|
1195
1207
|
if (forceDebug && cleanedCount > 0) {
|
|
1196
|
-
logToConsoleAndFile(`${messageColors.highlight('[whois-cache]')} Cleaned ${cleanedCount} expired entries, cache size: ${
|
|
1208
|
+
logToConsoleAndFile(`${messageColors.highlight('[whois-cache]')} Cleaned ${cleanedCount} expired entries, cache size: ${globalWhoisResultCache.size}`);
|
|
1197
1209
|
}
|
|
1198
1210
|
}
|
|
1199
1211
|
}
|
|
@@ -1216,16 +1228,16 @@ function createNetToolsHandler(config) {
|
|
|
1216
1228
|
const now = Date.now();
|
|
1217
1229
|
let digResult = null;
|
|
1218
1230
|
|
|
1219
|
-
if (
|
|
1220
|
-
const cachedEntry =
|
|
1221
|
-
if (now - cachedEntry.timestamp <
|
|
1231
|
+
if (globalDigResultCache.has(digCacheKey)) {
|
|
1232
|
+
const cachedEntry = globalDigResultCache.get(digCacheKey);
|
|
1233
|
+
if (now - cachedEntry.timestamp < GLOBAL_DIG_CACHE_TTL) {
|
|
1222
1234
|
if (forceDebug) {
|
|
1223
1235
|
logToConsoleAndFile(`${messageColors.highlight('[dig-cache]')} Using cached result for ${digDomain} (${digRecordType}) [age: ${Math.round((now - cachedEntry.timestamp) / 1000)}s]`);
|
|
1224
1236
|
}
|
|
1225
1237
|
digResult = cachedEntry.result;
|
|
1226
1238
|
} else {
|
|
1227
1239
|
// Cache expired, remove it
|
|
1228
|
-
|
|
1240
|
+
globalDigResultCache.delete(digCacheKey);
|
|
1229
1241
|
}
|
|
1230
1242
|
}
|
|
1231
1243
|
|
|
@@ -1233,7 +1245,7 @@ function createNetToolsHandler(config) {
|
|
|
1233
1245
|
digResult = await digLookup(digDomain, digRecordType, 5000); // 5 second timeout for dig
|
|
1234
1246
|
|
|
1235
1247
|
// Cache the result for future use
|
|
1236
|
-
|
|
1248
|
+
globalDigResultCache.set(digCacheKey, {
|
|
1237
1249
|
result: digResult,
|
|
1238
1250
|
timestamp: now
|
|
1239
1251
|
});
|
|
@@ -1303,17 +1315,17 @@ function createNetToolsHandler(config) {
|
|
|
1303
1315
|
}
|
|
1304
1316
|
|
|
1305
1317
|
// Periodic dig cache cleanup to prevent memory leaks
|
|
1306
|
-
if (
|
|
1318
|
+
if (globalDigResultCache.size > GLOBAL_DIG_CACHE_MAX) {
|
|
1307
1319
|
const now = Date.now();
|
|
1308
1320
|
let cleanedCount = 0;
|
|
1309
|
-
for (const [key, entry] of
|
|
1310
|
-
if (now - entry.timestamp >
|
|
1311
|
-
|
|
1321
|
+
for (const [key, entry] of globalDigResultCache.entries()) {
|
|
1322
|
+
if (now - entry.timestamp > GLOBAL_DIG_CACHE_TTL) {
|
|
1323
|
+
globalDigResultCache.delete(key);
|
|
1312
1324
|
cleanedCount++;
|
|
1313
1325
|
}
|
|
1314
1326
|
}
|
|
1315
1327
|
if (forceDebug && cleanedCount > 0) {
|
|
1316
|
-
logToConsoleAndFile(`${messageColors.highlight('[dig-cache]')} Cleaned ${cleanedCount} expired entries, cache size: ${
|
|
1328
|
+
logToConsoleAndFile(`${messageColors.highlight('[dig-cache]')} Cleaned ${cleanedCount} expired entries, cache size: ${globalDigResultCache.size}`);
|
|
1317
1329
|
}
|
|
1318
1330
|
}
|
|
1319
1331
|
}
|
|
@@ -1387,12 +1399,7 @@ function createNetToolsHandler(config) {
|
|
|
1387
1399
|
|
|
1388
1400
|
// Add whois server info to log if custom server was used
|
|
1389
1401
|
const serverInfo = whoisServer ? ` (whois-server: ${selectWhoisServer(whoisServer)})` : '';
|
|
1390
|
-
|
|
1391
|
-
if (bufferedLogWrite) {
|
|
1392
|
-
bufferedLogWrite(matchedUrlsLogFile, logLine);
|
|
1393
|
-
} else {
|
|
1394
|
-
fs.appendFileSync(matchedUrlsLogFile, logLine);
|
|
1395
|
-
}
|
|
1402
|
+
fs.appendFileSync(matchedUrlsLogFile, `${timestamp} [match][${simplifiedUrl}] ${domain} (${matchType.join(' + ')})${serverInfo}\n`);
|
|
1396
1403
|
}
|
|
1397
1404
|
}
|
|
1398
1405
|
|
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
|
|
@@ -1447,7 +1447,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1447
1447
|
'--disable-features=SafeBrowsing',
|
|
1448
1448
|
'--disable-dev-shm-usage',
|
|
1449
1449
|
'--disable-sync',
|
|
1450
|
-
'--
|
|
1450
|
+
'--disable-gpu', // WebGL null-context handled by fingerprint.js Proxy mock
|
|
1451
1451
|
'--mute-audio',
|
|
1452
1452
|
'--disable-translate',
|
|
1453
1453
|
'--window-size=1920,1080',
|
|
@@ -2174,14 +2174,14 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2174
2174
|
}
|
|
2175
2175
|
|
|
2176
2176
|
await page.setExtraHTTPHeaders({
|
|
2177
|
-
'Sec-CH-UA': '"
|
|
2177
|
+
'Sec-CH-UA': '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"',
|
|
2178
2178
|
'Sec-CH-UA-Platform': `"${platform}"`,
|
|
2179
2179
|
'Sec-CH-UA-Platform-Version': `"${platformVersion}"`,
|
|
2180
2180
|
'Sec-CH-UA-Mobile': '?0',
|
|
2181
2181
|
'Sec-CH-UA-Arch': `"${arch}"`,
|
|
2182
2182
|
'Sec-CH-UA-Bitness': '"64"',
|
|
2183
|
-
'Sec-CH-UA-Full-Version': '"
|
|
2184
|
-
'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"'
|
|
2185
2185
|
});
|
|
2186
2186
|
}
|
|
2187
2187
|
} catch (fingerprintErr) {
|
|
@@ -2515,7 +2515,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2515
2515
|
if (forceDebug) {
|
|
2516
2516
|
console.log(formatLogMessage('debug', `Blocking potential infinite iframe loop: ${checkedUrl}`));
|
|
2517
2517
|
}
|
|
2518
|
-
request.abort(
|
|
2518
|
+
request.abort();
|
|
2519
2519
|
return;
|
|
2520
2520
|
}
|
|
2521
2521
|
|
|
@@ -2730,9 +2730,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2730
2730
|
whoisDelay: siteConfig.whois_delay !== undefined ? siteConfig.whois_delay : whois_delay,
|
|
2731
2731
|
whoisServer,
|
|
2732
2732
|
whoisServerMode: siteConfig.whois_server_mode || whois_server_mode,
|
|
2733
|
-
bufferedLogWrite,
|
|
2734
2733
|
debugLogFile,
|
|
2735
|
-
fs,
|
|
2736
2734
|
digTerms,
|
|
2737
2735
|
digOrTerms,
|
|
2738
2736
|
digRecordType,
|
|
@@ -2838,9 +2836,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2838
2836
|
whoisDelay: siteConfig.whois_delay !== undefined ? siteConfig.whois_delay : whois_delay, // Site-specific or global fallback
|
|
2839
2837
|
whoisServer, // Pass whois server configuration
|
|
2840
2838
|
whoisServerMode: siteConfig.whois_server_mode || whois_server_mode,
|
|
2841
|
-
|
|
2842
|
-
debugLogFile, // Pass debug log file for whois error logging
|
|
2843
|
-
fs, // Pass fs module for file operations
|
|
2839
|
+
debugLogFile,
|
|
2844
2840
|
digTerms,
|
|
2845
2841
|
digOrTerms,
|
|
2846
2842
|
digRecordType,
|
|
@@ -3823,6 +3819,20 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3823
3819
|
});
|
|
3824
3820
|
} catch (gcErr) { /* ignore */ }
|
|
3825
3821
|
|
|
3822
|
+
// Force screenshot — always capture regardless of success/failure
|
|
3823
|
+
if (siteConfig.screenshot === 'force' && page && !page.isClosed()) {
|
|
3824
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
3825
|
+
const safeUrl = currentUrl.replace(/https?:\/\//, '').replace(/[^a-zA-Z0-9]/g, '_').substring(0, 80);
|
|
3826
|
+
const filename = `screenshots/${safeUrl}-${timestamp}.png`;
|
|
3827
|
+
try {
|
|
3828
|
+
if (!fs.existsSync('screenshots')) fs.mkdirSync('screenshots', { recursive: true });
|
|
3829
|
+
await page.screenshot({ path: filename, type: 'png', fullPage: true });
|
|
3830
|
+
console.log(formatLogMessage('info', `Screenshot saved: ${filename}`));
|
|
3831
|
+
} catch (screenshotErr) {
|
|
3832
|
+
console.warn(messageColors.warn(`[screenshot failed] ${currentUrl}: ${screenshotErr.message}`));
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3826
3836
|
try {
|
|
3827
3837
|
await page.close();
|
|
3828
3838
|
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.54",
|
|
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": {
|