@fanboynz/network-scanner 2.0.30 → 2.0.32

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.
@@ -211,14 +211,33 @@ async function cleanupUserDataDir(userDataDir, forceDebug = false) {
211
211
  * @returns {Promise<void>}
212
212
  */
213
213
  async function gracefulBrowserCleanup(browser, forceDebug = false) {
214
+ // FIX: Check browser connection before operations
215
+ if (!browser || !browser.isConnected()) {
216
+ if (forceDebug) console.log(`[debug] [browser] Browser not connected, skipping cleanup`);
217
+ return;
218
+ }
214
219
  if (forceDebug) console.log(`[debug] [browser] Getting all browser pages...`);
215
- const pages = await browser.pages();
220
+ let pages;
221
+ try {
222
+ pages = await browser.pages();
223
+ } catch (pagesErr) {
224
+ if (forceDebug) console.log(`[debug] [browser] Failed to get pages: ${pagesErr.message}`);
225
+ return;
226
+ }
216
227
  if (forceDebug) console.log(`[debug] [browser] Found ${pages.length} pages to close`);
217
228
 
218
229
  await Promise.all(pages.map(async (page) => {
219
230
  if (!page.isClosed()) {
220
231
  try {
221
- if (forceDebug) console.log(`[debug] [browser] Closing page: ${page.url()}`);
232
+ // FIX: Wrap page.url() in try-catch to handle race condition
233
+ let pageUrl = 'unknown';
234
+ try {
235
+ pageUrl = page.url();
236
+ } catch (urlErr) {
237
+ // Page closed between check and url call
238
+ }
239
+
240
+ if (forceDebug) console.log(`[debug] [browser] Closing page: ${pageUrl}`);
222
241
  await page.close();
223
242
  if (forceDebug) console.log(`[debug] [browser] Page closed successfully`);
224
243
  } catch (err) {
@@ -229,8 +248,18 @@ async function gracefulBrowserCleanup(browser, forceDebug = false) {
229
248
  }));
230
249
 
231
250
  if (forceDebug) console.log(`[debug] [browser] All pages closed, closing browser...`);
232
- await browser.close();
233
- if (forceDebug) console.log(`[debug] [browser] Browser closed successfully`);
251
+
252
+ // FIX: Check browser is still connected before closing
253
+ try {
254
+ if (browser.isConnected()) {
255
+ await browser.close();
256
+ if (forceDebug) console.log(`[debug] [browser] Browser closed successfully`);
257
+ } else {
258
+ if (forceDebug) console.log(`[debug] [browser] Browser already disconnected`);
259
+ }
260
+ } catch (closeErr) {
261
+ if (forceDebug) console.log(`[debug] [browser] Browser close failed: ${closeErr.message}`);
262
+ }
234
263
  }
235
264
 
236
265
  /**
@@ -492,6 +492,10 @@ async function performRealtimeWindowCleanup(browserInstance, threshold = REALTIM
492
492
  */
493
493
  async function isPageFromPreviousScan(page, forceDebug) {
494
494
  try {
495
+ // FIX: Check page state first before any operations
496
+ if (page.isClosed()) {
497
+ return true; // Closed pages should be cleaned up
498
+ }
495
499
  // Cache page.url() for all checks in this function
496
500
  const pageUrl = page.url();
497
501
 
@@ -524,9 +528,13 @@ async function isPageFromPreviousScan(page, forceDebug) {
524
528
  return false; // Conservative - don't close unless we're sure
525
529
  } catch (err) {
526
530
  if (forceDebug) {
527
- // Cache URL for error logging
528
- const pageUrl = page.url();
529
- console.log(formatLogMessage('debug', `[isPageFromPreviousScan] Error evaluating page ${pageUrl}: ${err.message}`));
531
+ try {
532
+ // Cache URL for error logging - wrap in try-catch as page might be closed
533
+ const pageUrl = page.url();
534
+ console.log(formatLogMessage('debug', `[isPageFromPreviousScan] Error evaluating page ${pageUrl}: ${err.message}`));
535
+ } catch (urlErr) {
536
+ console.log(formatLogMessage('debug', `[isPageFromPreviousScan] Error evaluating page: ${err.message}`));
537
+ }
530
538
  }
531
539
  return false; // Conservative - don't close if we can't evaluate
532
540
  }
@@ -1104,16 +1112,25 @@ async function cleanupPageBeforeReload(page, forceDebug = false) {
1104
1112
  // Wait a bit for navigation to stop
1105
1113
  await new Promise(resolve => setTimeout(resolve, 500));
1106
1114
 
1115
+ // FIX: Check if page is still open after delay before cleanup
1116
+ if (page.isClosed()) {
1117
+ if (forceDebug) {
1118
+ console.log(formatLogMessage('debug', 'Page closed during cleanup delay'));
1119
+ }
1120
+ return false;
1121
+ }
1122
+
1107
1123
  // Now do the full cleanup
1108
- await page.evaluate(() => {
1109
- // Stop all media elements
1110
- document.querySelectorAll('video, audio').forEach(media => {
1111
- try {
1112
- media.pause();
1113
- media.src = '';
1114
- media.load();
1115
- } catch(e) {}
1116
- });
1124
+ try {
1125
+ await page.evaluate(() => {
1126
+ // Stop all media elements
1127
+ document.querySelectorAll('video, audio').forEach(media => {
1128
+ try {
1129
+ media.pause();
1130
+ media.src = '';
1131
+ media.load();
1132
+ } catch(e) {}
1133
+ });
1117
1134
 
1118
1135
  // Clear all timers and intervals
1119
1136
  const highestId = setTimeout(() => {}, 0);
@@ -1144,7 +1161,15 @@ async function cleanupPageBeforeReload(page, forceDebug = false) {
1144
1161
 
1145
1162
  // Force garbage collection if available
1146
1163
  if (window.gc) window.gc();
1147
- });
1164
+ });
1165
+
1166
+ } catch (evalErr) {
1167
+ // Page closed during cleanup
1168
+ if (forceDebug) {
1169
+ console.log(formatLogMessage('debug', `Page cleanup evaluation failed: ${evalErr.message}`));
1170
+ }
1171
+ return false;
1172
+ }
1148
1173
 
1149
1174
  if (forceDebug) {
1150
1175
  console.log(formatLogMessage('debug', 'Page resources cleaned before reload'));
@@ -10,18 +10,24 @@ const { formatLogMessage } = require('./colorize');
10
10
  */
11
11
  class DomainCache {
12
12
  constructor(options = {}) {
13
+ // V8 Optimization: Initialize all properties in constructor for stable hidden class
13
14
  this.cache = new Set();
15
+
16
+ // V8 Optimization: Use consistent object shape (no dynamic property addition)
14
17
  this.stats = {
15
18
  totalDetected: 0,
16
19
  totalSkipped: 0,
17
20
  cacheHits: 0,
18
21
  cacheMisses: 0
19
22
  };
20
- this.options = {
21
- enableLogging: options.enableLogging || false,
22
- logPrefix: options.logPrefix || '[domain-cache]',
23
- maxCacheSize: options.maxCacheSize || 10000 // Prevent memory leaks
24
- };
23
+
24
+ // V8 Optimization: Store options directly instead of nested object for faster property access
25
+ this.enableLogging = options.enableLogging || false;
26
+ this.logPrefix = options.logPrefix || '[domain-cache]';
27
+ this.maxCacheSize = options.maxCacheSize || 10000; // Prevent memory leaks
28
+
29
+ // V8 Optimization: Pre-calculate 90% target to avoid repeated Math.floor
30
+ this.targetCacheSize = Math.floor(this.maxCacheSize * 0.9);
25
31
  }
26
32
 
27
33
  /**
@@ -40,14 +46,14 @@ class DomainCache {
40
46
  this.stats.totalSkipped++;
41
47
  this.stats.cacheHits++;
42
48
 
43
- if (this.options.enableLogging) {
44
- console.log(formatLogMessage('debug', `${this.options.logPrefix} Cache HIT: ${domain} (skipped)`));
49
+ if (this.enableLogging) {
50
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cache HIT: ${domain} (skipped)`));
45
51
  }
46
52
  } else {
47
53
  this.stats.cacheMisses++;
48
54
 
49
- if (this.options.enableLogging) {
50
- console.log(formatLogMessage('debug', `${this.options.logPrefix} Cache MISS: ${domain} (processing)`));
55
+ if (this.enableLogging) {
56
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cache MISS: ${domain} (processing)`));
51
57
  }
52
58
  }
53
59
 
@@ -63,25 +69,77 @@ class DomainCache {
63
69
  return false;
64
70
  }
65
71
 
66
- // Prevent cache from growing too large
67
- if (this.cache.size >= this.options.maxCacheSize) {
68
- this.clearOldestEntries(Math.floor(this.options.maxCacheSize * 0.1)); // Remove 10% of entries
69
- }
70
-
71
72
  const wasNew = !this.cache.has(domain);
72
73
  this.cache.add(domain);
73
74
 
74
75
  if (wasNew) {
75
76
  this.stats.totalDetected++;
76
77
 
77
- if (this.options.enableLogging) {
78
- console.log(formatLogMessage('debug', `${this.options.logPrefix} Marked as detected: ${domain} (cache size: ${this.cache.size})`));
78
+ if (this.enableLogging) {
79
+ console.log(formatLogMessage('debug', `${this.logPrefix} Marked as detected: ${domain} (cache size: ${this.cache.size})`));
80
+ }
81
+ }
82
+
83
+ // Check size AFTER adding to prevent race where multiple threads see same size
84
+ // and all trigger cleanup before any adds complete
85
+ // V8 Optimization: Use pre-calculated targetCacheSize
86
+ if (this.cache.size > this.maxCacheSize) {
87
+ const toRemove = this.cache.size - this.targetCacheSize;
88
+ if (toRemove > 0) {
89
+ this.clearOldestEntries(toRemove);
79
90
  }
80
91
  }
81
92
 
82
93
  return wasNew;
83
94
  }
84
95
 
96
+ /**
97
+ * Atomically check if domain was detected and mark it if new (race-condition free)
98
+ * This method combines isDomainAlreadyDetected + markDomainAsDetected in one atomic operation
99
+ * @param {string} domain - Domain to check and potentially mark
100
+ * @returns {boolean} True if domain was ALREADY detected (should skip), false if NEW (should process)
101
+ */
102
+ checkAndMark(domain) {
103
+ if (!domain || typeof domain !== 'string') {
104
+ return false;
105
+ }
106
+
107
+ const wasAlreadyDetected = this.cache.has(domain);
108
+
109
+ if (wasAlreadyDetected) {
110
+ // Domain already exists - update skip stats and return true (should skip)
111
+ this.stats.totalSkipped++;
112
+ this.stats.cacheHits++;
113
+
114
+ if (this.enableLogging) {
115
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cache HIT: ${domain} (skipped)`));
116
+ }
117
+ return true; // Already detected, should skip
118
+ }
119
+
120
+ // Domain is NEW - mark it as detected
121
+ this.stats.cacheMisses++;
122
+
123
+ this.cache.add(domain);
124
+ this.stats.totalDetected++;
125
+
126
+ if (this.enableLogging) {
127
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cache MISS: ${domain} (processing and marked, cache size: ${this.cache.size})`));
128
+ }
129
+
130
+ // Check size AFTER adding to prevent race where multiple threads see same size
131
+ // and all trigger cleanup before any adds complete
132
+ // V8 Optimization: Use pre-calculated targetCacheSize
133
+ if (this.cache.size > this.maxCacheSize) {
134
+ const toRemove = this.cache.size - this.targetCacheSize;
135
+ if (toRemove > 0) {
136
+ this.clearOldestEntries(toRemove);
137
+ }
138
+ }
139
+
140
+ return false; // New domain, should process
141
+ }
142
+
85
143
  /**
86
144
  * Clear oldest entries from cache (basic LRU simulation)
87
145
  * Note: Set doesn't maintain insertion order in all Node.js versions,
@@ -96,8 +154,8 @@ class DomainCache {
96
154
 
97
155
  toRemove.forEach(domain => this.cache.delete(domain));
98
156
 
99
- if (this.options.enableLogging) {
100
- console.log(formatLogMessage('debug', `${this.options.logPrefix} Cleared ${toRemove.length} old entries, cache size now: ${this.cache.size}`));
157
+ if (this.enableLogging) {
158
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cleared ${toRemove.length} old entries, cache size now: ${this.cache.size}`));
101
159
  }
102
160
  }
103
161
 
@@ -128,8 +186,8 @@ class DomainCache {
128
186
  cacheMisses: 0
129
187
  };
130
188
 
131
- if (this.options.enableLogging) {
132
- console.log(formatLogMessage('debug', `${this.options.logPrefix} Cache cleared (${previousSize} entries removed)`));
189
+ if (this.enableLogging) {
190
+ console.log(formatLogMessage('debug', `${this.logPrefix} Cache cleared (${previousSize} entries removed)`));
133
191
  }
134
192
  }
135
193
 
@@ -158,8 +216,8 @@ class DomainCache {
158
216
  removeDomain(domain) {
159
217
  const wasRemoved = this.cache.delete(domain);
160
218
 
161
- if (wasRemoved && this.options.enableLogging) {
162
- console.log(formatLogMessage('debug', `${this.options.logPrefix} Removed from cache: ${domain}`));
219
+ if (wasRemoved && this.enableLogging) {
220
+ console.log(formatLogMessage('debug', `${this.logPrefix} Removed from cache: ${domain}`));
163
221
  }
164
222
 
165
223
  return wasRemoved;
@@ -193,6 +251,7 @@ class DomainCache {
193
251
  return {
194
252
  isDomainAlreadyDetected: this.isDomainAlreadyDetected.bind(this),
195
253
  markDomainAsDetected: this.markDomainAsDetected.bind(this),
254
+ checkAndMark: this.checkAndMark.bind(this),
196
255
  getSkippedCount: () => this.stats.totalSkipped,
197
256
  getCacheSize: () => this.cache.size,
198
257
  getStats: this.getStats.bind(this)
@@ -261,6 +320,16 @@ function markDomainAsDetected(domain) {
261
320
  cache.markDomainAsDetected(domain);
262
321
  }
263
322
 
323
+ /**
324
+ * Atomically check and mark a domain (race-condition free)
325
+ * @param {string} domain - Domain to check and mark
326
+ * @returns {boolean} True if already detected (skip), false if new (process)
327
+ */
328
+ function checkAndMark(domain) {
329
+ const cache = getGlobalDomainCache();
330
+ return cache.checkAndMark(domain);
331
+ }
332
+
264
333
  /**
265
334
  * Get total domains skipped (legacy wrapper)
266
335
  * @returns {number} Number of domains skipped
@@ -291,6 +360,7 @@ module.exports = {
291
360
  // Legacy wrapper functions for backward compatibility
292
361
  isDomainAlreadyDetected,
293
362
  markDomainAsDetected,
363
+ checkAndMark,
294
364
  getTotalDomainsSkipped,
295
365
  getDetectedDomainsCount
296
366
  };
@@ -268,14 +268,19 @@ function generateCSIData() {
268
268
  */
269
269
  async function validatePageForInjection(page, currentUrl, forceDebug) {
270
270
  try {
271
- if (page.isClosed()) return false;
271
+ if (!page || page.isClosed()) return false;
272
+
273
+ if (!page.browser().isConnected()) {
274
+ if (forceDebug) console.log(`[debug] Page validation failed - browser disconnected: ${currentUrl}`);
275
+ return false;
276
+ }
272
277
  await Promise.race([
273
278
  page.evaluate(() => document.readyState || 'loading'),
274
- new Promise((_, reject) => setTimeout(() => reject(new Error('Page unresponsive')), 3000))
279
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Page evaluation timeout')), 1500))
275
280
  ]);
276
281
  return true;
277
282
  } catch (validationErr) {
278
- if (forceDebug) console.log(`[debug] Skipping fingerprint protection - page unresponsive: ${currentUrl}`);
283
+ if (forceDebug) console.log(`[debug] Page validation failed - ${validationErr.message}: ${currentUrl}`);
279
284
  return false;
280
285
  }
281
286
  }
@@ -362,7 +367,13 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
362
367
  const ua = USER_AGENT_COLLECTIONS.get(siteConfig.userAgent.toLowerCase());
363
368
 
364
369
  if (ua) {
365
- await page.setUserAgent(ua);
370
+ // FIX: Wrap setUserAgent in try-catch to handle race condition
371
+ try {
372
+ await page.setUserAgent(ua);
373
+ } catch (uaErr) {
374
+ if (forceDebug) console.log(`[debug] Could not set user agent - page closed: ${currentUrl}`);
375
+ return;
376
+ }
366
377
 
367
378
  if (forceDebug) console.log(`[debug] Applying stealth protection for ${currentUrl}`);
368
379
 
@@ -1317,6 +1328,20 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
1317
1328
  }, 'enhanced mouse/pointer spoofing');
1318
1329
 
1319
1330
  safeExecute(() => {
1331
+ // Filter DevTools/automation traces from console.debug
1332
+ const originalConsoleDebug = console.debug;
1333
+ console.debug = function(...args) {
1334
+ const message = args.join(' ');
1335
+ if (typeof message === 'string' && (
1336
+ message.includes('DevTools') ||
1337
+ message.includes('Runtime.evaluate') ||
1338
+ message.includes('Page.addScriptToEvaluateOnNewDocument') ||
1339
+ message.includes('Protocol error'))) {
1340
+ return; // Silently drop DevTools-related debug messages
1341
+ }
1342
+ return originalConsoleDebug.apply(this, args);
1343
+ };
1344
+
1320
1345
  const originalConsoleError = console.error;
1321
1346
  console.error = function(...args) {
1322
1347
  const message = args.join(' ');
@@ -1331,6 +1356,22 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
1331
1356
  return originalConsoleError.apply(this, arguments);
1332
1357
  };
1333
1358
  }, 'console error suppression');
1359
+
1360
+ // Hide source URL indicators (data: URLs reveal script injection)
1361
+ safeExecute(() => {
1362
+ const originalLocation = window.location;
1363
+ Object.defineProperty(window, 'location', {
1364
+ value: new Proxy(originalLocation, {
1365
+ get: function(target, prop) {
1366
+ if (prop === 'href' && target[prop] && target[prop].includes('data:')) {
1367
+ return 'about:blank';
1368
+ }
1369
+ return target[prop];
1370
+ }
1371
+ }),
1372
+ configurable: false
1373
+ });
1374
+ }, 'location URL masking');
1334
1375
 
1335
1376
  }, ua, forceDebug);
1336
1377
  } catch (stealthErr) {
@@ -1422,8 +1463,15 @@ async function applyFingerprintProtection(page, siteConfig, forceDebug, currentU
1422
1463
 
1423
1464
  // Validate page state before injection
1424
1465
  if (!(await validatePageForInjection(page, currentUrl, forceDebug))) return;
1425
-
1426
- const currentUserAgent = await page.evaluate(() => navigator.userAgent);
1466
+
1467
+ // FIX: Wrap page.evaluate in try-catch to handle race condition
1468
+ let currentUserAgent;
1469
+ try {
1470
+ currentUserAgent = await page.evaluate(() => navigator.userAgent);
1471
+ } catch (evalErr) {
1472
+ if (forceDebug) console.log(`[debug] Could not get user agent - page closed: ${currentUrl}`);
1473
+ return;
1474
+ }
1427
1475
 
1428
1476
  const spoof = fingerprintSetting === 'random' ? generateRealisticFingerprint(currentUserAgent) : {
1429
1477
  deviceMemory: 8,
package/lib/grep.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const { spawnSync } = require('child_process');
6
+ const crypto = require('crypto');
6
7
  const path = require('path');
7
8
  const os = require('os');
8
9
  const { colorize, colors, messageColors, tags, formatLogMessage } = require('./colorize');
@@ -21,23 +22,48 @@ const GREP_DEFAULTS = {
21
22
  GREP_NOT_FOUND_STATUS: 1,
22
23
  CURL_SUCCESS_STATUS: 0,
23
24
  VERSION_LINE_INDEX: 0,
24
- RANDOM_STRING_LENGTH: 9
25
+ RANDOM_STRING_LENGTH: 9,
26
+ TEMP_DIR_PREFIX: 'grep_search_'
25
27
  };
26
28
 
27
29
  /**
28
- * Creates a temporary file with content for grep processing
30
+ * Creates a temporary directory and file with content for grep processing
31
+ * Uses mkdtempSync to avoid race conditions from filename collisions
29
32
  * @param {string} content - The content to write to temp file
30
33
  * @param {string} prefix - Prefix for temp filename
31
- * @returns {string} Path to the created temporary file
34
+ * @returns {object} Object containing tempDir and tempFile paths
32
35
  */
33
36
  function createTempFile(content, prefix = 'scanner_grep') {
34
37
  const tempDir = os.tmpdir();
35
- const tempFile = path.join(tempDir, `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, GREP_DEFAULTS.RANDOM_STRING_LENGTH)}.tmp`);
38
+
39
+ // Create a unique temporary directory to avoid race conditions
40
+ const uniqueTempDir = fs.mkdtempSync(path.join(tempDir, GREP_DEFAULTS.TEMP_DIR_PREFIX));
41
+
42
+ // Use cryptographically secure random ID for additional uniqueness
43
+ const uniqueId = crypto.randomBytes(8).toString('hex');
44
+ const tempFile = path.join(uniqueTempDir, `${prefix}_${uniqueId}.tmp`);
36
45
 
37
46
  try {
38
- fs.writeFileSync(tempFile, content, 'utf8');
39
- return tempFile;
47
+ // Write atomically with error handling
48
+ fs.writeFileSync(tempFile, content, {
49
+ encoding: 'utf8',
50
+ mode: 0o600 // Restrict permissions for security
51
+ });
52
+
53
+ return { tempDir: uniqueTempDir, tempFile };
40
54
  } catch (error) {
55
+ // Clean up temp directory on write failure
56
+ try {
57
+ if (fs.existsSync(tempFile)) {
58
+ fs.unlinkSync(tempFile);
59
+ }
60
+ if (fs.existsSync(uniqueTempDir)) {
61
+ fs.rmdirSync(uniqueTempDir);
62
+ }
63
+ } catch (cleanupErr) {
64
+ // Ignore cleanup errors, report original error
65
+ }
66
+
41
67
  throw new Error(`Failed to create temp file: ${error.message}`);
42
68
  }
43
69
  }
@@ -64,8 +90,10 @@ async function grepContent(content, searchPatterns, options = {}) {
64
90
  let tempFile = null;
65
91
 
66
92
  try {
67
- // Create temporary file with content
68
- tempFile = createTempFile(content, 'grep_search');
93
+ // Create temporary directory and file with content
94
+ const tempResult = createTempFile(content, 'grep_search');
95
+ tempDir = tempResult.tempDir;
96
+ tempFile = tempResult.tempFile;
69
97
 
70
98
  const allMatches = [];
71
99
  let firstMatch = null;
@@ -119,7 +147,7 @@ async function grepContent(content, searchPatterns, options = {}) {
119
147
  } catch (error) {
120
148
  throw new Error(`Grep search failed: ${error.message}`);
121
149
  } finally {
122
- // Clean up temporary file
150
+ // Clean up temporary file and directory
123
151
  if (tempFile) {
124
152
  try {
125
153
  fs.unlinkSync(tempFile);
@@ -127,6 +155,13 @@ async function grepContent(content, searchPatterns, options = {}) {
127
155
  console.warn(formatLogMessage('warn', `[grep] Failed to cleanup temp file ${tempFile}: ${cleanupErr.message}`));
128
156
  }
129
157
  }
158
+ if (tempDir) {
159
+ try {
160
+ fs.rmdirSync(tempDir);
161
+ } catch (cleanupErr) {
162
+ console.warn(formatLogMessage('warn', `[grep] Failed to cleanup temp directory ${tempDir}: ${cleanupErr.message}`));
163
+ }
164
+ }
130
165
  }
131
166
  }
132
167
 
@@ -0,0 +1,510 @@
1
+ // === Referrer Header Generation Module ===
2
+ // This module handles generation of referrer headers for different traffic simulation modes
3
+
4
+ /**
5
+ * Performance utility: Get random element from array
6
+ * Reduces code duplication and improves readability
7
+ * @param {Array} array - Array to select from
8
+ * @returns {*} Random element from array
9
+ */
10
+ function getRandomElement(array) {
11
+ return array[Math.floor(Math.random() * array.length)];
12
+ }
13
+
14
+ /**
15
+ * Referrer URL collections for different modes
16
+ */
17
+ const REFERRER_COLLECTIONS = Object.freeze({
18
+ SEARCH_ENGINES: [
19
+ 'https://www.google.com/search?q=',
20
+ 'https://www.bing.com/search?q=',
21
+ 'https://duckduckgo.com/?q=',
22
+ 'https://search.yahoo.com/search?p=',
23
+ 'https://yandex.com/search/?text=',
24
+ 'https://www.baidu.com/s?wd=',
25
+ 'https://www.startpage.com/sp/search?query=',
26
+ 'https://search.brave.com/search?q='
27
+ ],
28
+
29
+ SOCIAL_MEDIA: [
30
+ 'https://www.facebook.com/',
31
+ 'https://twitter.com/',
32
+ 'https://www.linkedin.com/',
33
+ 'https://www.reddit.com/',
34
+ 'https://www.instagram.com/',
35
+ 'https://www.pinterest.com/',
36
+ 'https://www.tiktok.com/',
37
+ 'https://www.youtube.com/',
38
+ 'https://discord.com/channels/',
39
+ 'https://t.me/',
40
+ 'https://www.snapchat.com/',
41
+ 'https://www.tumblr.com/',
42
+ 'https://www.threads.net/',
43
+ 'https://mastodon.social/'
44
+ ],
45
+
46
+ NEWS_SITES: [
47
+ 'https://news.google.com/',
48
+ 'https://www.reddit.com/r/news/',
49
+ 'https://news.ycombinator.com/',
50
+ 'https://www.bbc.com/news',
51
+ 'https://www.cnn.com/',
52
+ 'https://techcrunch.com/',
53
+ 'https://www.theverge.com/'
54
+ ],
55
+
56
+ DEFAULT_SEARCH_TERMS: [
57
+ 'reviews', 'deals', 'discount', 'price', 'buy', 'shop', 'store',
58
+ 'compare', 'best', 'top', 'guide', 'how to', 'tutorial', 'tips',
59
+ 'news', 'update', 'latest', 'new', 'trending', 'popular', 'cheap',
60
+ 'free', 'download', 'online', 'service', 'product', 'website'
61
+ ],
62
+
63
+ ECOMMERCE_TERMS: [
64
+ 'buy online', 'shopping', 'store', 'sale', 'discount', 'coupon',
65
+ 'free shipping', 'best price', 'deals', 'outlet', 'marketplace'
66
+ ],
67
+
68
+ TECH_TERMS: [
69
+ 'software', 'app', 'download', 'tutorial', 'guide', 'review',
70
+ 'comparison', 'features', 'specs', 'performance', 'benchmark'
71
+ ]
72
+ });
73
+
74
+ /**
75
+ * Generates a random search term based on context or defaults
76
+ * @param {Array} customTerms - Custom search terms provided by user
77
+ * @param {string} context - Context hint for term selection (e.g., 'ecommerce', 'tech')
78
+ * @returns {string} Selected search term
79
+ */
80
+ function generateSearchTerm(customTerms, context = null) {
81
+ if (customTerms && customTerms.length > 0) {
82
+ return getRandomElement(customTerms);
83
+ }
84
+
85
+ // Use context-specific terms if available
86
+ let termCollection = REFERRER_COLLECTIONS.DEFAULT_SEARCH_TERMS;
87
+ if (context === 'ecommerce') {
88
+ termCollection = REFERRER_COLLECTIONS.ECOMMERCE_TERMS;
89
+ } else if (context === 'tech') {
90
+ termCollection = REFERRER_COLLECTIONS.TECH_TERMS;
91
+ }
92
+
93
+ return getRandomElement(termCollection);
94
+ }
95
+
96
+ /**
97
+ * Generates a search engine referrer URL
98
+ * @param {Array} searchTerms - Custom search terms
99
+ * @param {string} context - Context for term selection
100
+ * @param {boolean} forceDebug - Debug logging flag
101
+ * @returns {string} Generated search engine referrer URL
102
+ */
103
+ function generateSearchReferrer(searchTerms, context, forceDebug) {
104
+ const randomEngine = getRandomElement(REFERRER_COLLECTIONS.SEARCH_ENGINES);
105
+
106
+ const searchTerm = generateSearchTerm(searchTerms, context);
107
+ const referrerUrl = randomEngine + encodeURIComponent(searchTerm);
108
+
109
+ if (forceDebug) {
110
+ console.log(`[debug] Generated search referrer: ${referrerUrl} (engine: ${randomEngine.split('//')[1].split('/')[0]}, term: "${searchTerm}")`);
111
+ }
112
+
113
+ return referrerUrl;
114
+ }
115
+
116
+ /**
117
+ * Generates a social media referrer URL
118
+ * @param {boolean} forceDebug - Debug logging flag
119
+ * @returns {string} Generated social media referrer URL
120
+ */
121
+ function generateSocialMediaReferrer(forceDebug) {
122
+ const randomSocial = getRandomElement(REFERRER_COLLECTIONS.SOCIAL_MEDIA);
123
+
124
+ if (forceDebug) {
125
+ console.log(`[debug] Generated social media referrer: ${randomSocial}`);
126
+ }
127
+
128
+ return randomSocial;
129
+ }
130
+
131
+ /**
132
+ * Generates a news site referrer URL
133
+ * @param {boolean} forceDebug - Debug logging flag
134
+ * @returns {string} Generated news site referrer URL
135
+ */
136
+ function generateNewsReferrer(forceDebug) {
137
+ const randomNews = getRandomElement(REFERRER_COLLECTIONS.NEWS_SITES);
138
+
139
+ if (forceDebug) {
140
+ console.log(`[debug] Generated news referrer: ${randomNews}`);
141
+ }
142
+
143
+ return randomNews;
144
+ }
145
+
146
+ /**
147
+ * Validates a URL string
148
+ * @param {string} url - URL to validate
149
+ * @returns {boolean} True if valid HTTP/HTTPS URL
150
+ */
151
+ function isValidUrl(url) {
152
+ return typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://'));
153
+ }
154
+
155
+ /**
156
+ * Checks if a URL should have its referrer disabled
157
+ * @param {string} targetUrl - The URL being visited
158
+ * @param {Array} disableList - Array of URLs/patterns that should have no referrer
159
+ * @param {boolean} forceDebug - Debug logging flag
160
+ * @returns {boolean} True if referrer should be disabled for this URL
161
+ */
162
+ function shouldDisableReferrer(targetUrl, disableList, forceDebug = false) {
163
+ // Fast path: early return for empty/invalid inputs
164
+ if (!disableList?.length || !targetUrl || typeof targetUrl !== 'string') {
165
+ return false;
166
+ }
167
+
168
+ // Parse target URL once (performance optimization)
169
+ let targetHostname = null;
170
+ let targetUrlParsed = false;
171
+
172
+ try {
173
+ targetHostname = new URL(targetUrl).hostname;
174
+ targetUrlParsed = true;
175
+ } catch (e) {
176
+ // Invalid URL - can only do string matching
177
+ targetUrlParsed = false;
178
+ }
179
+
180
+ for (const disablePattern of disableList) {
181
+ if (typeof disablePattern !== 'string') continue;
182
+
183
+ // Fast check: Exact URL match (no parsing needed)
184
+ if (targetUrl === disablePattern) {
185
+ if (forceDebug) console.log(`[debug] Referrer disabled for exact match: ${targetUrl}`);
186
+ return true;
187
+ }
188
+
189
+ // Domain/hostname match (use cached parsed URL)
190
+ if (targetUrlParsed) {
191
+ try {
192
+ const disableHostname = new URL(disablePattern).hostname;
193
+ if (targetHostname === disableHostname) {
194
+ if (forceDebug) console.log(`[debug] Referrer disabled for domain match: ${targetHostname}`);
195
+ return true;
196
+ }
197
+ } catch (e) {
198
+ // disablePattern is not a valid URL, try substring match below
199
+ }
200
+ }
201
+
202
+ // Fallback: Simple substring match (for patterns like 'example.com')
203
+ if (!targetUrlParsed || disablePattern.includes('/') === false) {
204
+ if (targetUrl.includes(disablePattern)) {
205
+ if (forceDebug) console.log(`[debug] Referrer disabled for pattern match: ${disablePattern} in ${targetUrl}`);
206
+ return true;
207
+ }
208
+ }
209
+ }
210
+
211
+ return false;
212
+ }
213
+
214
+ /**
215
+ * Generates a referrer URL based on the specified mode and options
216
+ * @param {Object|string|Array} referrerConfig - Referrer configuration
217
+ * @param {boolean} forceDebug - Debug logging flag
218
+ * @returns {string} Generated referrer URL or empty string
219
+ */
220
+ function generateReferrerUrl(referrerConfig, forceDebug = false) {
221
+ try {
222
+ // Handle simple string URLs
223
+ if (typeof referrerConfig === 'string') {
224
+ const url = isValidUrl(referrerConfig) ? referrerConfig : '';
225
+ if (forceDebug && url) {
226
+ console.log(`[debug] Using direct referrer URL: ${url}`);
227
+ } else if (forceDebug && !url) {
228
+ console.log(`[debug] Invalid referrer URL provided: ${referrerConfig}`);
229
+ }
230
+ return url;
231
+ }
232
+
233
+ // Handle arrays - pick random URL
234
+ if (Array.isArray(referrerConfig)) {
235
+ if (referrerConfig.length === 0) {
236
+ if (forceDebug) console.log(`[debug] Empty referrer array provided`);
237
+ return '';
238
+ }
239
+
240
+ const randomUrl = getRandomElement(referrerConfig);
241
+ const url = isValidUrl(randomUrl) ? randomUrl : '';
242
+
243
+ if (forceDebug) {
244
+ console.log(`[debug] Selected referrer from array (${referrerConfig.length} options): ${url || 'invalid URL'}`);
245
+ }
246
+
247
+ return url;
248
+ }
249
+
250
+ // Handle object modes
251
+ if (typeof referrerConfig === 'object' && referrerConfig !== null && referrerConfig.mode) {
252
+ switch (referrerConfig.mode) {
253
+ case 'random_search': {
254
+ const searchTerms = referrerConfig.search_terms;
255
+ const context = referrerConfig.context; // Optional context hint
256
+ return generateSearchReferrer(searchTerms, context, forceDebug);
257
+ }
258
+
259
+ case 'social_media': {
260
+ return generateSocialMediaReferrer(forceDebug);
261
+ }
262
+
263
+ case 'news_sites': {
264
+ return generateNewsReferrer(forceDebug);
265
+ }
266
+
267
+ case 'direct_navigation': {
268
+ if (forceDebug) console.log(`[debug] Using direct navigation (no referrer)`);
269
+ return '';
270
+ }
271
+
272
+ case 'custom': {
273
+ const url = isValidUrl(referrerConfig.url) ? referrerConfig.url : '';
274
+ if (forceDebug) {
275
+ console.log(`[debug] Using custom referrer URL: ${url || 'invalid URL provided'}`);
276
+ }
277
+ return url;
278
+ }
279
+
280
+ case 'mixed': {
281
+ // Randomly choose between different referrer types
282
+ const modes = ['random_search', 'social_media', 'news_sites'];
283
+ const randomMode = getRandomElement(modes);
284
+
285
+ if (forceDebug) console.log(`[debug] Mixed mode selected: ${randomMode}`);
286
+
287
+ const mixedConfig = { mode: randomMode };
288
+ if (randomMode === 'random_search' && referrerConfig.search_terms) {
289
+ mixedConfig.search_terms = referrerConfig.search_terms;
290
+ mixedConfig.context = referrerConfig.context;
291
+ }
292
+
293
+ return generateReferrerUrl(mixedConfig, forceDebug);
294
+ }
295
+
296
+ default: {
297
+ if (forceDebug) console.log(`[debug] Unknown referrer mode: ${referrerConfig.mode}`);
298
+ return '';
299
+ }
300
+ }
301
+ }
302
+
303
+ if (forceDebug) console.log(`[debug] Invalid referrer configuration type: ${typeof referrerConfig}`);
304
+ return '';
305
+ } catch (err) {
306
+ if (forceDebug) console.log(`[debug] Referrer generation failed: ${err.message}`);
307
+ return '';
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Main function to determine referrer for a specific URL
313
+ * Handles both referrer generation and referrer_disable functionality
314
+ * @param {string} targetUrl - The URL being visited
315
+ * @param {Object|string|Array} referrerConfig - Referrer configuration
316
+ * @param {Array} referrerDisable - Array of URLs that should have no referrer
317
+ * @param {boolean} forceDebug - Debug logging flag
318
+ * @returns {string} Generated referrer URL or empty string if disabled/none
319
+ */
320
+ function getReferrerForUrl(targetUrl, referrerConfig, referrerDisable, forceDebug = false) {
321
+ // Check if referrer should be disabled for this specific URL
322
+ if (shouldDisableReferrer(targetUrl, referrerDisable, forceDebug)) {
323
+ return '';
324
+ }
325
+
326
+ // Generate referrer normally if not disabled
327
+ return generateReferrerUrl(referrerConfig, forceDebug);
328
+ }
329
+
330
+ /**
331
+ * Validates referrer configuration
332
+ * @param {Object|string|Array} referrerConfig - Referrer configuration to validate
333
+ * @returns {Object} Validation result with isValid flag and error messages
334
+ */
335
+ function validateReferrerConfig(referrerConfig) {
336
+ const result = { isValid: true, errors: [], warnings: [] };
337
+
338
+ if (!referrerConfig) {
339
+ result.isValid = false;
340
+ result.errors.push('Referrer configuration is required');
341
+ return result;
342
+ }
343
+
344
+ // Validate string URLs
345
+ if (typeof referrerConfig === 'string') {
346
+ if (!isValidUrl(referrerConfig)) {
347
+ result.isValid = false;
348
+ result.errors.push('String referrer must be a valid HTTP/HTTPS URL');
349
+ }
350
+ return result;
351
+ }
352
+
353
+ // Validate arrays
354
+ if (Array.isArray(referrerConfig)) {
355
+ if (referrerConfig.length === 0) {
356
+ result.warnings.push('Empty referrer array will result in no referrer');
357
+ return result;
358
+ }
359
+
360
+ // Fast validation: check only first and last items if array is large
361
+ const itemsToCheck = referrerConfig.length > 10
362
+ ? [referrerConfig[0], referrerConfig[referrerConfig.length - 1]]
363
+ : referrerConfig;
364
+
365
+ itemsToCheck.forEach((url, index) => {
366
+ if (!isValidUrl(url)) {
367
+ const actualIndex = itemsToCheck === referrerConfig ? index : (index === 0 ? 0 : referrerConfig.length - 1);
368
+ result.errors.push(`Array item ${actualIndex} is not a valid HTTP/HTTPS URL: ${url}`);
369
+ result.isValid = false;
370
+ }
371
+ });
372
+
373
+ if (referrerConfig.length > 10 && itemsToCheck.length < referrerConfig.length) {
374
+ result.warnings.push(`Large array (${referrerConfig.length} items): only validated first and last items for performance`);
375
+ }
376
+
377
+ return result;
378
+ }
379
+
380
+ // Validate object modes
381
+ if (typeof referrerConfig === 'object') {
382
+ const validModes = ['random_search', 'social_media', 'news_sites', 'direct_navigation', 'custom', 'mixed'];
383
+
384
+ if (!referrerConfig.mode) {
385
+ result.isValid = false;
386
+ result.errors.push('Object referrer configuration must have a "mode" property');
387
+ return result;
388
+ }
389
+
390
+ if (!validModes.includes(referrerConfig.mode)) {
391
+ result.isValid = false;
392
+ result.errors.push(`Invalid referrer mode: ${referrerConfig.mode}. Valid modes: ${validModes.join(', ')}`);
393
+ return result;
394
+ }
395
+
396
+ // Mode-specific validation
397
+ switch (referrerConfig.mode) {
398
+ case 'custom':
399
+ if (!referrerConfig.url) {
400
+ result.isValid = false;
401
+ result.errors.push('Custom mode requires a "url" property');
402
+ } else if (!isValidUrl(referrerConfig.url)) {
403
+ result.isValid = false;
404
+ result.errors.push('Custom mode URL must be a valid HTTP/HTTPS URL');
405
+ }
406
+ break;
407
+
408
+ case 'random_search':
409
+ if (referrerConfig.search_terms && !Array.isArray(referrerConfig.search_terms)) {
410
+ result.warnings.push('search_terms should be an array of strings');
411
+ }
412
+ if (referrerConfig.search_terms && referrerConfig.search_terms.length === 0) {
413
+ result.warnings.push('Empty search_terms array will use default terms');
414
+ }
415
+ break;
416
+ }
417
+
418
+ return result;
419
+ }
420
+
421
+ result.isValid = false;
422
+ result.errors.push('Referrer configuration must be a string, array, or object');
423
+ return result;
424
+ }
425
+
426
+ /**
427
+ * Validates referrer_disable configuration
428
+ * @param {Array} referrerDisable - Array of URLs/patterns to disable referrer for
429
+ * @returns {Object} Validation result with isValid flag and error messages
430
+ */
431
+ function validateReferrerDisable(referrerDisable) {
432
+ const result = { isValid: true, errors: [], warnings: [] };
433
+
434
+ if (!referrerDisable) {
435
+ return result; // referrer_disable is optional
436
+ }
437
+
438
+ if (!Array.isArray(referrerDisable)) {
439
+ result.isValid = false;
440
+ result.errors.push('referrer_disable must be an array of URLs/patterns');
441
+ return result;
442
+ }
443
+
444
+ if (referrerDisable.length === 0) {
445
+ result.warnings.push('Empty referrer_disable array has no effect');
446
+ return result;
447
+ }
448
+
449
+ referrerDisable.forEach((pattern, index) => {
450
+ if (typeof pattern !== 'string') {
451
+ result.errors.push(`referrer_disable item ${index} must be a string (got ${typeof pattern})`);
452
+ result.isValid = false;
453
+ } else if (pattern.trim() === '') {
454
+ result.warnings.push(`referrer_disable item ${index} is empty string`);
455
+ } else if (!pattern.includes('.') && !pattern.includes('/')) {
456
+ result.warnings.push(`referrer_disable item ${index} "${pattern}" might be too broad - consider using full URLs or hostnames`);
457
+ }
458
+ });
459
+
460
+ if (referrerDisable.length > 100) {
461
+ result.warnings.push('Large referrer_disable list (>100 items) may impact performance');
462
+ }
463
+
464
+ return result;
465
+ }
466
+
467
+ /**
468
+ * Gets available referrer modes and their descriptions
469
+ * @returns {Object} Object containing mode descriptions
470
+ */
471
+ function getReferrerModes() {
472
+ return {
473
+ 'random_search': 'Generate random search engine referrers with customizable search terms',
474
+ 'social_media': 'Use random social media platform referrers',
475
+ 'news_sites': 'Use random news website referrers',
476
+ 'direct_navigation': 'No referrer (simulates direct URL entry)',
477
+ 'custom': 'Use a specific custom referrer URL',
478
+ 'mixed': 'Randomly mix different referrer types for varied traffic simulation'
479
+ };
480
+ }
481
+
482
+ /**
483
+ * Gets statistics about available referrer collections
484
+ * @returns {Object} Statistics about referrer collections
485
+ */
486
+ function getReferrerStats() {
487
+ return {
488
+ searchEngines: REFERRER_COLLECTIONS.SEARCH_ENGINES.length,
489
+ socialMedia: REFERRER_COLLECTIONS.SOCIAL_MEDIA.length,
490
+ newsSites: REFERRER_COLLECTIONS.NEWS_SITES.length,
491
+ defaultSearchTerms: REFERRER_COLLECTIONS.DEFAULT_SEARCH_TERMS.length,
492
+ ecommerceTerms: REFERRER_COLLECTIONS.ECOMMERCE_TERMS.length,
493
+ techTerms: REFERRER_COLLECTIONS.TECH_TERMS.length
494
+ };
495
+ }
496
+
497
+ module.exports = {
498
+ generateReferrerUrl,
499
+ getReferrerForUrl,
500
+ shouldDisableReferrer,
501
+ validateReferrerConfig,
502
+ validateReferrerDisable,
503
+ getReferrerModes,
504
+ getReferrerStats,
505
+ generateSearchReferrer,
506
+ generateSocialMediaReferrer,
507
+ generateNewsReferrer,
508
+ isValidUrl,
509
+ REFERRER_COLLECTIONS
510
+ };
package/nwss.js CHANGED
@@ -1,4 +1,4 @@
1
- // === Network scanner script (nwss.js) v2.0.29 ===
1
+ // === Network scanner script (nwss.js) v2.0.32 ===
2
2
 
3
3
  // puppeteer for browser automation, fs for file system operations, psl for domain parsing.
4
4
  // const pLimit = require('p-limit'); // Will be dynamically imported
@@ -48,6 +48,8 @@ const { clearPersistentCache } = require('./lib/smart-cache');
48
48
  const { initializeDryRunCollections, addDryRunMatch, addDryRunNetTools, processDryRunResults, writeDryRunOutput } = require('./lib/dry-run');
49
49
  // Enhanced site data clearing functionality
50
50
  const { clearSiteData } = require('./lib/clear_sitedata');
51
+ // Referrer header generation
52
+ const { getReferrerForUrl, validateReferrerConfig, validateReferrerDisable } = require('./lib/referrer');
51
53
 
52
54
  // Fast setTimeout helper for Puppeteer 22.x compatibility
53
55
  // Uses standard Promise constructor for better performance than node:timers/promises
@@ -143,7 +145,7 @@ const { navigateWithRedirectHandling, handleRedirectTimeout } = require('./lib/r
143
145
  const { monitorBrowserHealth, isBrowserHealthy, isQuicklyResponsive, performGroupWindowCleanup, performRealtimeWindowCleanup, trackPageForRealtime, updatePageUsage, cleanupPageBeforeReload } = require('./lib/browserhealth');
144
146
 
145
147
  // --- Script Configuration & Constants ---
146
- const VERSION = '2.0.29'; // Script version
148
+ const VERSION = '2.0.32'; // Script version
147
149
 
148
150
  // get startTime
149
151
  const startTime = Date.now();
@@ -368,9 +370,22 @@ if (validateConfig) {
368
370
  // Validate referrer_headers format
369
371
  for (const site of sites) {
370
372
  if (site.referrer_headers && typeof site.referrer_headers === 'object' && !Array.isArray(site.referrer_headers)) {
371
- const validModes = ['random_search', 'social_media', 'direct_navigation', 'custom'];
372
- if (site.referrer_headers.mode && !validModes.includes(site.referrer_headers.mode)) {
373
- console.warn(`⚠ Invalid referrer_headers mode: ${site.referrer_headers.mode}. Valid modes: ${validModes.join(', ')}`);
373
+ const validation = validateReferrerConfig(site.referrer_headers);
374
+ if (!validation.isValid) {
375
+ console.warn(`⚠ Invalid referrer_headers configuration: ${validation.errors.join(', ')}`);
376
+ }
377
+ if (validation.warnings.length > 0) {
378
+ console.warn(`⚠ Referrer warnings: ${validation.warnings.join(', ')}`);
379
+ }
380
+ }
381
+ // Validate referrer_disable format
382
+ if (site.referrer_disable) {
383
+ const disableValidation = validateReferrerDisable(site.referrer_disable);
384
+ if (!disableValidation.isValid) {
385
+ console.warn(`⚠ Invalid referrer_disable configuration: ${disableValidation.errors.join(', ')}`);
386
+ }
387
+ if (disableValidation.warnings.length > 0) {
388
+ console.warn(`⚠ Referrer disable warnings: ${disableValidation.warnings.join(', ')}`);
374
389
  }
375
390
  }
376
391
  }
@@ -551,6 +566,7 @@ Redirect Handling Options:
551
566
  bypass_cache: true/false Skip all caching for this site's URLs (default: false)
552
567
  referrer_headers: "url" or ["url1", "url2"] Set referrer header for realistic traffic sources
553
568
  custom_headers: {"Header": "value"} Add custom HTTP headers to requests
569
+ referrer_disable: ["url1", "url2"] Disable referrer headers for specific URLs
554
570
 
555
571
  Cloudflare Protection Options:
556
572
  cloudflare_phish: true/false Auto-click through Cloudflare phishing warnings (default: false)
@@ -606,6 +622,10 @@ Referrer Header Options:
606
622
  referrer_headers: {"mode": "random_search", "search_terms": ["term1"]} Smart search engine traffic
607
623
  referrer_headers: {"mode": "social_media"} Random social media referrers
608
624
  referrer_headers: {"mode": "direct_navigation"} No referrer (direct access)
625
+ referrer_headers: {"mode": "news_sites"} Random news website referrers
626
+ referrer_headers: {"mode": "custom", "url": "https://example.com"} Custom referrer URL
627
+ referrer_headers: {"mode": "mixed"} Mixed referrer types for varied traffic
628
+ referrer_disable: ["https://example.com/no-ref", "sensitive-site.com"] Disable referrer for specific URLs
609
629
  custom_headers: {"Header": "Value"} Additional HTTP headers
610
630
  `);
611
631
  process.exit(0);
@@ -1808,7 +1828,15 @@ function setupFrameHandling(page, forceDebug) {
1808
1828
  throw new Error('Page is closed');
1809
1829
  }
1810
1830
 
1811
- const pageUrl = await page.url().catch(() => 'about:blank');
1831
+ // FIX: Properly wrap page.url() in try-catch to handle race condition
1832
+ let pageUrl;
1833
+ try {
1834
+ pageUrl = await page.url();
1835
+ } catch (urlErr) {
1836
+ // Page closed between isClosed check and url call
1837
+ throw new Error('Page closed while getting URL');
1838
+ }
1839
+
1812
1840
  if (pageUrl === 'about:blank') {
1813
1841
  throw new Error('Cannot inject on about:blank');
1814
1842
  }
@@ -2848,20 +2876,23 @@ function setupFrameHandling(page, forceDebug) {
2848
2876
  // --- Runtime CSS Element Blocking (Fallback) ---
2849
2877
  // Apply CSS blocking after page load as a fallback in case evaluateOnNewDocument didn't work
2850
2878
  if (cssBlockedSelectors && Array.isArray(cssBlockedSelectors) && cssBlockedSelectors.length > 0) {
2851
- try {
2852
- await page.evaluate((selectors) => {
2853
- const existingStyle = document.querySelector('#css-blocker-runtime');
2854
- if (!existingStyle) {
2855
- const style = document.createElement('style');
2856
- style.id = 'css-blocker-runtime';
2857
- style.type = 'text/css';
2858
- const cssRules = selectors.map(selector => `${selector} { display: none !important; visibility: hidden !important; }`).join('\n');
2859
- style.innerHTML = cssRules;
2860
- document.head.appendChild(style);
2861
- }
2862
- }, cssBlockedSelectors);
2863
- } catch (cssRuntimeErr) {
2864
- console.warn(formatLogMessage('warn', `[css_blocked] Failed to apply runtime CSS blocking for ${currentUrl}: ${cssRuntimeErr.message}`));
2879
+ // FIX: Check page state before evaluation
2880
+ if (page && !page.isClosed()) {
2881
+ try {
2882
+ await page.evaluate((selectors) => {
2883
+ const existingStyle = document.querySelector('#css-blocker-runtime');
2884
+ if (!existingStyle) {
2885
+ const style = document.createElement('style');
2886
+ style.id = 'css-blocker-runtime';
2887
+ style.type = 'text/css';
2888
+ const cssRules = selectors.map(selector => `${selector} { display: none !important; visibility: hidden !important; }`).join('\n');
2889
+ style.innerHTML = cssRules;
2890
+ document.head.appendChild(style);
2891
+ }
2892
+ }, cssBlockedSelectors);
2893
+ } catch (cssRuntimeErr) {
2894
+ console.warn(formatLogMessage('warn', `[css_blocked] Failed to apply runtime CSS blocking for ${currentUrl}: ${cssRuntimeErr.message}`));
2895
+ }
2865
2896
  }
2866
2897
  }
2867
2898
 
@@ -2885,11 +2916,8 @@ function setupFrameHandling(page, forceDebug) {
2885
2916
  timeout: Math.min(timeout, TIMEOUTS.DEFAULT_PAGE), // Cap at default page timeout
2886
2917
  // Puppeteer 23.x: Fixed referrer header handling
2887
2918
  ...(siteConfig.referrer_headers && (() => {
2888
- const referrerUrl = Array.isArray(siteConfig.referrer_headers)
2889
- ? siteConfig.referrer_headers[0]
2890
- : siteConfig.referrer_headers;
2891
- // Ensure referrer is a valid string URL, not an object
2892
- return typeof referrerUrl === 'string' && referrerUrl.startsWith('http')
2919
+ const referrerUrl = getReferrerForUrl(currentUrl, siteConfig.referrer_headers, siteConfig.referrer_disable, forceDebug);
2920
+ return referrerUrl
2893
2921
  ? { referer: referrerUrl }
2894
2922
  : {};
2895
2923
  })())
@@ -3069,7 +3097,15 @@ function setupFrameHandling(page, forceDebug) {
3069
3097
  }
3070
3098
 
3071
3099
  console.log(formatLogMessage('info', `${messageColors.loaded('Loaded:')} (${siteCounter}/${totalUrls}) ${currentUrl}`));
3072
- await page.evaluate(() => { console.log('Safe to evaluate on loaded page.'); });
3100
+
3101
+ // FIX: Check page state before evaluation
3102
+ if (page && !page.isClosed()) {
3103
+ try {
3104
+ await page.evaluate(() => { console.log('Safe to evaluate on loaded page.'); });
3105
+ } catch (evalErr) {
3106
+ // Page closed during evaluation - safe to ignore
3107
+ }
3108
+ }
3073
3109
 
3074
3110
  // Mark page as processing frames
3075
3111
  updatePageUsage(page, true);
@@ -3146,10 +3182,19 @@ function setupFrameHandling(page, forceDebug) {
3146
3182
  const networkIdleTimeout = Math.min(timeout / 2, TIMEOUTS.NETWORK_IDLE_MAX); // Balanced: 10s timeout
3147
3183
  const actualDelay = Math.min(delayMs, TIMEOUTS.NETWORK_IDLE); // Balanced: 2s delay for stability
3148
3184
 
3149
- await page.waitForNetworkIdle({
3150
- idleTime: networkIdleTime,
3151
- timeout: networkIdleTimeout
3152
- });
3185
+ // FIX: Check page state before waiting for network idle
3186
+ if (page && !page.isClosed()) {
3187
+ try {
3188
+ await page.waitForNetworkIdle({
3189
+ idleTime: networkIdleTime,
3190
+ timeout: networkIdleTimeout
3191
+ });
3192
+ } catch (networkIdleErr) {
3193
+ // Page closed or network idle timeout - continue anyway
3194
+ if (forceDebug) console.log(formatLogMessage('debug', `Network idle wait failed: ${networkIdleErr.message}`));
3195
+ }
3196
+ }
3197
+
3153
3198
  // Use fast timeout helper for Puppeteer 23.x compatibility with better performance
3154
3199
  await fastTimeout(actualDelay);
3155
3200
 
@@ -3330,6 +3375,11 @@ function setupFrameHandling(page, forceDebug) {
3330
3375
  // Fallback to standard reload if force reload failed or wasn't attempted
3331
3376
  if (!reloadSuccess) {
3332
3377
  try {
3378
+ // FIX: Check page state before reload validation
3379
+ if (page.isClosed()) {
3380
+ throw new Error('Page closed before reload check');
3381
+ }
3382
+
3333
3383
  const canReload = await page.evaluate(() => {
3334
3384
  return !!(document && document.body);
3335
3385
  }).catch(() => false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanboynz/network-scanner",
3
- "version": "2.0.30",
3
+ "version": "2.0.32",
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": {