@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.
@@ -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/142.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/142.0.0.0 Safari/537.36"],
59
- ['chrome_linux', "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"],
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
- await page.evaluateOnNewDocument((userAgent, debugEnabled) => {
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: () => undefined });
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
- runtime: {
432
- onConnect: { addListener: () => {}, removeListener: () => {} },
433
- onMessage: { addListener: () => {}, removeListener: () => {} },
434
- sendMessage: () => {},
435
- connect: () => ({
436
- onMessage: { addListener: () => {}, removeListener: () => {} },
437
- postMessage: () => {},
438
- disconnect: () => {}
439
- }),
440
- getManifest: () => ({
441
- name: "Chrome",
442
- version: "142.0.0.0",
443
- manifest_version: 3,
444
- description: "Chrome Browser"
445
- }),
446
- getURL: (path) => `chrome-extension://invalid/${path}`,
447
- id: undefined,
448
- getPlatformInfo: (callback) => callback({
449
- os: navigator.platform.includes('Win') ? 'win' :
450
- navigator.platform.includes('Mac') ? 'mac' : 'linux',
451
- arch: 'x86-64',
452
- nacl_arch: 'x86-64'
453
- })
454
- },
455
- storage: {
456
- local: {
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: 'Intel Inc.', // VENDOR
776
- 37446: 'Intel(R) UHD Graphics 630', // RENDERER (more realistic)
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 null-context safety net prevents ad script crashes in headless
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 (ctx !== null || (type !== 'webgl' && type !== 'experimental-webgl' && type !== 'webgl2')) return ctx;
852
- // Return minimal mock so scripts calling getShaderPrecisionFormat etc. don't crash
1020
+ if (type !== 'webgl' && type !== 'experimental-webgl' && type !== 'webgl2') return ctx;
1021
+
853
1022
  const noop = () => {};
854
- return new Proxy({}, {
855
- get(_, prop) {
856
- if (prop === 'getShaderPrecisionFormat') return () => ({ rangeMin: 127, rangeMax: 127, precision: 23 });
857
- if (prop === 'getParameter') return (p) => ({ 37445: 'Intel Inc.', 37446: 'Intel(R) UHD Graphics 630' }[p] || 0);
858
- if (prop === 'getSupportedExtensions') return () => [];
859
- if (prop === 'getExtension') return () => null;
860
- if (prop === 'canvas') return this;
861
- if (prop === 'drawingBufferWidth') return 1920;
862
- if (prop === 'drawingBufferHeight') return 1080;
863
- return noop;
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 null-context safety net'); // Permissions API spoofing
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
- safeExecute(() => {
963
- const originalMatchMedia = window.matchMedia;
964
- window.matchMedia = function(query) {
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
- const originalCreateOffer = pc.createOffer;
986
- pc.createOffer = function() {
987
- return Promise.reject(new Error('WebRTC disabled'));
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
- // Add slight noise to text measurements to prevent precise fingerprinting
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 + (Math.random() - 0.5) * 0.1 }
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
- return Math.floor(originalOffsetWidth.get.call(this) + (Math.random() - 0.5) * 2);
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
- // Noise already applied by getImageData override
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
- return {
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
- return {
53
+ validateWhoisAvailability._cached = {
40
54
  isAvailable: true,
41
55
  version: 'whois (version unknown)'
42
56
  };
43
57
  } catch (e) {
44
- return {
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
- return {
75
+ validateDigAvailability._cached = {
60
76
  isAvailable: true,
61
77
  version: result.split('\n')[0].trim()
62
78
  };
63
79
  } catch (error) {
64
- return {
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
- // Get short output first
639
- const { stdout, stderr } = await execWithTimeout(`dig +short "${cleanDomain}" ${recordType}`, timeout);
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
- // Also get full dig output for detailed analysis
651
- const { stdout: fullOutput } = await execWithTimeout(`dig "${cleanDomain}" ${recordType}`, timeout);
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: stdout.trim(),
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
- // Add whois resolution caching to avoid redundant whois lookups
811
- const whoisResultCache = new Map();
812
- const WHOIS_CACHE_TTL = 900000; // 15 minutes cache TTL (whois data changes less frequently)
813
- const MAX_CACHE_SIZE = 400; // Larger cache for whois due to longer TTL
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 (debugLogFile && bufferedLogWrite) {
852
- try {
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 (whoisResultCache.has(whoisCacheKey)) {
986
- const cachedEntry = whoisResultCache.get(whoisCacheKey);
987
- if (now - cachedEntry.timestamp < WHOIS_CACHE_TTL) {
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
- whoisResultCache.delete(whoisCacheKey);
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
- whoisResultCache.set(whoisCacheKey, {
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 (whoisResultCache.size > MAX_CACHE_SIZE) {
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 whoisResultCache.entries()) {
1190
- if (now - entry.timestamp > WHOIS_CACHE_TTL) {
1191
- whoisResultCache.delete(key);
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: ${whoisResultCache.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 (digResultCache.has(digCacheKey)) {
1220
- const cachedEntry = digResultCache.get(digCacheKey);
1221
- if (now - cachedEntry.timestamp < DIG_CACHE_TTL) {
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
- digResultCache.delete(digCacheKey);
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
- digResultCache.set(digCacheKey, {
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 (digResultCache.size > DIG_MAX_CACHE_SIZE) {
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 digResultCache.entries()) {
1310
- if (now - entry.timestamp > DIG_CACHE_TTL) {
1311
- digResultCache.delete(key);
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: ${digResultCache.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
- const logLine = `${timestamp} [match][${simplifiedUrl}] ${domain} (${matchType.join(' + ')})${serverInfo}\n`;
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/142.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/142.0.0.0 Safari/537.36"],
104
- ['chrome_linux', "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"],
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 Capture screenshot on load failure
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
- '--use-gl=swiftshader', // Software WebGL prevents ad script crashes in headless
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': '"Chromium";v="142", "Not=A?Brand";v="24", "Google Chrome";v="142"',
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': '"142.0.7444,69"',
2184
- 'Sec-CH-UA-Full-Version-List': '"Chromium";v="142.0.7444,69", "Not=A?Brand";v="24.0.0.0", "Google Chrome";v="142.0.7444,69"'
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('blockedbyclient');
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
- bufferedLogWrite,
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.52",
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": {