@fanboynz/network-scanner 2.0.51 → 2.0.52

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.
@@ -843,7 +843,28 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
843
843
  }
844
844
  }, 'WebGL spoofing');
845
845
 
846
- // Permissions API spoofing
846
+ // WebGL null-context safety net � prevents ad script crashes in headless
847
+ safeExecute(() => {
848
+ const originalGetContext = HTMLCanvasElement.prototype.getContext;
849
+ HTMLCanvasElement.prototype.getContext = function(type, attrs) {
850
+ 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
853
+ 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;
864
+ }
865
+ });
866
+ };
867
+ }, 'WebGL null-context safety net'); // Permissions API spoofing
847
868
  //
848
869
  safeExecute(() => {
849
870
  if (navigator.permissions?.query) {
package/nwss.js CHANGED
@@ -1367,9 +1367,17 @@ function setupFrameHandling(page, forceDebug) {
1367
1367
  if (launchHeadless) {
1368
1368
  const puppeteerInfo = detectPuppeteerVersion();
1369
1369
 
1370
+ // Check if any site needs fingerprint protection — use stealth-friendly headless mode
1371
+ const needsStealth = sites.some(site => site.fingerprint_protection);
1372
+
1370
1373
  if (puppeteerInfo.useShellMode) {
1371
- headlessMode = 'shell'; // Use fast chrome-headless-shell for 22.x+
1372
- if (forceDebug) console.log(formatLogMessage('debug', `Using chrome-headless-shell (Puppeteer ${puppeteerInfo.version || 'v' + puppeteerInfo.majorVersion + '.x'})`));
1374
+ if (needsStealth) {
1375
+ headlessMode = 'new'; // Full Chrome in headless harder to detect than chrome-headless-shell
1376
+ if (forceDebug) console.log(formatLogMessage('debug', `Using headless=new for stealth (fingerprint_protection detected)`));
1377
+ } else {
1378
+ headlessMode = 'shell'; // Use fast chrome-headless-shell for 22.x+
1379
+ if (forceDebug) console.log(formatLogMessage('debug', `Using chrome-headless-shell (Puppeteer ${puppeteerInfo.version || 'v' + puppeteerInfo.majorVersion + '.x'})`));
1380
+ }
1373
1381
  } else {
1374
1382
  headlessMode = true; // Use regular headless for older versions
1375
1383
  if (forceDebug) console.log(formatLogMessage('debug', 'Could not detect Puppeteer version, using regular headless mode'));
@@ -1439,7 +1447,7 @@ function setupFrameHandling(page, forceDebug) {
1439
1447
  '--disable-features=SafeBrowsing',
1440
1448
  '--disable-dev-shm-usage',
1441
1449
  '--disable-sync',
1442
- '--disable-gpu',
1450
+ '--use-gl=swiftshader', // Software WebGL — prevents ad script crashes in headless
1443
1451
  '--mute-audio',
1444
1452
  '--disable-translate',
1445
1453
  '--window-size=1920,1080',
@@ -1494,7 +1502,9 @@ function setupFrameHandling(page, forceDebug) {
1494
1502
 
1495
1503
  // Log which headless mode is being used
1496
1504
  if (forceDebug && launchHeadless) {
1497
- console.log(formatLogMessage('debug', `Using chrome-headless-shell for maximum performance`));
1505
+ const needsStealth = sites.some(site => site.fingerprint_protection);
1506
+ const modeLabel = needsStealth ? 'headless=new (stealth mode)' : 'chrome-headless-shell (performance mode)';
1507
+ console.log(formatLogMessage('debug', `Using ${modeLabel}`));
1498
1508
  }
1499
1509
 
1500
1510
  // Initial cleanup of any existing Chrome temp files - always comprehensive on startup
@@ -1652,6 +1662,11 @@ function setupFrameHandling(page, forceDebug) {
1652
1662
  let cdpSessionManager = null;
1653
1663
  // Use Map to track domains and their resource types for --adblock-rules or --dry-run
1654
1664
  const matchedDomains = (adblockRulesMode || siteConfig.adblock_rules || dryRunMode) ? new Map() : new Set();
1665
+
1666
+ // Local domain dedup scoped to THIS processUrl call only
1667
+ // Prevents cross-config contamination from the global domain cache
1668
+ const localDetectedDomains = new Set();
1669
+ const isLocallyDetected = (domain) => localDetectedDomains.has(domain);
1655
1670
 
1656
1671
  // Initialize dry run matches collection
1657
1672
  if (dryRunMode) {
@@ -2430,6 +2445,7 @@ function setupFrameHandling(page, forceDebug) {
2430
2445
 
2431
2446
  // Mark full subdomain as detected for future reference
2432
2447
  markDomainAsDetected(cacheKey);
2448
+ localDetectedDomains.add(cacheKey);
2433
2449
 
2434
2450
  // Also mark in smart cache with context (if cache is enabled)
2435
2451
  if (smartCache) {
@@ -2499,7 +2515,7 @@ function setupFrameHandling(page, forceDebug) {
2499
2515
  if (forceDebug) {
2500
2516
  console.log(formatLogMessage('debug', `Blocking potential infinite iframe loop: ${checkedUrl}`));
2501
2517
  }
2502
- request.abort();
2518
+ request.abort('blockedbyclient');
2503
2519
  return;
2504
2520
  }
2505
2521
 
@@ -2534,7 +2550,7 @@ function setupFrameHandling(page, forceDebug) {
2534
2550
  if (forceDebug) {
2535
2551
  console.log(formatLogMessage('debug', `${messageColors.blocked('[adblock]')} ${checkedUrl} (${result.reason})`));
2536
2552
  }
2537
- request.abort();
2553
+ request.abort('blockedbyclient');
2538
2554
  return;
2539
2555
  }
2540
2556
  adblockStats.allowed++;
@@ -2614,7 +2630,7 @@ function setupFrameHandling(page, forceDebug) {
2614
2630
  }
2615
2631
  }
2616
2632
 
2617
- request.abort();
2633
+ request.abort('blockedbyclient');
2618
2634
  return;
2619
2635
  }
2620
2636
 
@@ -2724,7 +2740,7 @@ function setupFrameHandling(page, forceDebug) {
2724
2740
  dryRunCallback: dryRunMode ? createEnhancedDryRunCallback(matchedDomains, forceDebug) : null,
2725
2741
  matchedDomains,
2726
2742
  addMatchedDomain,
2727
- isDomainAlreadyDetected,
2743
+ isDomainAlreadyDetected: isLocallyDetected,
2728
2744
  onWhoisResult: smartCache ? (domain, result) => smartCache.cacheNetTools(domain, 'whois', result) : undefined,
2729
2745
  onDigResult: smartCache ? (domain, result, recordType) => smartCache.cacheNetTools(domain, 'dig', result, recordType) : undefined,
2730
2746
  cachedWhois: smartCache ? smartCache.getCachedNetTools(reqDomain, 'whois') : null,
@@ -2773,8 +2789,8 @@ function setupFrameHandling(page, forceDebug) {
2773
2789
  }
2774
2790
  } else if (hasNetTools && !hasSearchString && !hasSearchStringAnd) {
2775
2791
  // If nettools are configured (whois/dig), perform checks on the domain
2776
- // Skip nettools check if full subdomain was already detected
2777
- if (isDomainAlreadyDetected(fullSubdomain)) {
2792
+ // Skip nettools check if full subdomain was already detected in THIS scan
2793
+ if (localDetectedDomains.has(fullSubdomain)) {
2778
2794
  if (forceDebug) {
2779
2795
  console.log(formatLogMessage('debug', `Skipping nettools check for already detected subdomain: ${fullSubdomain}`));
2780
2796
  }
@@ -2833,7 +2849,7 @@ function setupFrameHandling(page, forceDebug) {
2833
2849
  dryRunCallback: dryRunMode ? createEnhancedDryRunCallback(matchedDomains, forceDebug) : null,
2834
2850
  matchedDomains,
2835
2851
  addMatchedDomain,
2836
- isDomainAlreadyDetected,
2852
+ isDomainAlreadyDetected: isLocallyDetected,
2837
2853
  // Add cache callbacks if smart cache is available and caching is enabled
2838
2854
  onWhoisResult: smartCache ? (domain, result) => {
2839
2855
  smartCache.cacheNetTools(domain, 'whois', result);
@@ -2863,8 +2879,8 @@ function setupFrameHandling(page, forceDebug) {
2863
2879
  }
2864
2880
  } else {
2865
2881
  // If searchstring or searchstring_and IS defined (with or without nettools), queue for content checking
2866
- // Skip searchstring check if full subdomain was already detected
2867
- if (isDomainAlreadyDetected(fullSubdomain)) {
2882
+ // Skip searchstring check if full subdomain was already detected in THIS scan
2883
+ if (localDetectedDomains.has(fullSubdomain)) {
2868
2884
  if (forceDebug) {
2869
2885
  console.log(formatLogMessage('debug', `Skipping searchstring check for already detected subdomain: ${fullSubdomain}`));
2870
2886
  }
@@ -2920,7 +2936,7 @@ function setupFrameHandling(page, forceDebug) {
2920
2936
  searchStringsAnd,
2921
2937
  matchedDomains,
2922
2938
  addMatchedDomain, // Pass the helper function
2923
- isDomainAlreadyDetected,
2939
+ isDomainAlreadyDetected: isLocallyDetected,
2924
2940
  onContentFetched: smartCache && !ignoreCache ? (url, content) => {
2925
2941
  // Only cache if not bypassing cache
2926
2942
  if (!shouldBypassCacheForUrl(url, siteConfig)) {
@@ -2956,7 +2972,7 @@ function setupFrameHandling(page, forceDebug) {
2956
2972
  regexes,
2957
2973
  matchedDomains,
2958
2974
  addMatchedDomain,
2959
- isDomainAlreadyDetected,
2975
+ isDomainAlreadyDetected: isLocallyDetected,
2960
2976
  onContentFetched: smartCache && !ignoreCache ? (url, content) => {
2961
2977
  // Only cache if not bypassing cache
2962
2978
  if (!shouldBypassCacheForUrl(url, siteConfig)) {
@@ -3027,7 +3043,7 @@ function setupFrameHandling(page, forceDebug) {
3027
3043
  matchedDomains,
3028
3044
  addMatchedDomain, // Pass the helper function
3029
3045
  bypassCache: (url) => shouldBypassCacheForUrl(url, siteConfig),
3030
- isDomainAlreadyDetected,
3046
+ isDomainAlreadyDetected: isLocallyDetected,
3031
3047
  onContentFetched: smartCache && !ignoreCache ? (url, content) => {
3032
3048
  // Only cache if not bypassing cache
3033
3049
  if (!shouldBypassCacheForUrl(url, siteConfig)) {
@@ -3374,8 +3390,16 @@ function setupFrameHandling(page, forceDebug) {
3374
3390
 
3375
3391
  // Mark page as processing during interactions
3376
3392
  updatePageUsage(page, true);
3377
- // Use enhanced interaction module
3378
- await performPageInteraction(page, currentUrl, interactionConfig, forceDebug);
3393
+ // Use enhanced interaction module with hard abort timeout
3394
+ const INTERACTION_HARD_TIMEOUT = 15000;
3395
+ try {
3396
+ await Promise.race([
3397
+ performPageInteraction(page, currentUrl, interactionConfig, forceDebug),
3398
+ new Promise((_, reject) => setTimeout(() => reject(new Error('interaction hard timeout')), INTERACTION_HARD_TIMEOUT))
3399
+ ]);
3400
+ } catch (interactTimeoutErr) {
3401
+ if (forceDebug) console.log(formatLogMessage('debug', `[interaction] Aborted after ${INTERACTION_HARD_TIMEOUT}ms: ${interactTimeoutErr.message}`));
3402
+ }
3379
3403
  }
3380
3404
 
3381
3405
  const delayMs = DEFAULT_DELAY;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanboynz/network-scanner",
3
- "version": "2.0.51",
3
+ "version": "2.0.52",
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": {