@fanboynz/network-scanner 2.0.30 → 2.0.31

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'));
@@ -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,479 @@
1
+ // === Referrer Header Generation Module ===
2
+ // This module handles generation of referrer headers for different traffic simulation modes
3
+
4
+ /**
5
+ * Referrer URL collections for different modes
6
+ */
7
+ const REFERRER_COLLECTIONS = Object.freeze({
8
+ SEARCH_ENGINES: [
9
+ 'https://www.google.com/search?q=',
10
+ 'https://www.bing.com/search?q=',
11
+ 'https://duckduckgo.com/?q=',
12
+ 'https://search.yahoo.com/search?p=',
13
+ 'https://yandex.com/search/?text=',
14
+ 'https://www.baidu.com/s?wd=',
15
+ 'https://www.startpage.com/sp/search?query=',
16
+ 'https://search.brave.com/search?q='
17
+ ],
18
+
19
+ SOCIAL_MEDIA: [
20
+ 'https://www.facebook.com/',
21
+ 'https://twitter.com/',
22
+ 'https://www.linkedin.com/',
23
+ 'https://www.reddit.com/',
24
+ 'https://www.instagram.com/',
25
+ 'https://www.pinterest.com/',
26
+ 'https://www.tiktok.com/',
27
+ 'https://www.youtube.com/',
28
+ 'https://discord.com/channels/',
29
+ 'https://t.me/',
30
+ 'https://www.snapchat.com/',
31
+ 'https://www.tumblr.com/',
32
+ 'https://www.threads.net/',
33
+ 'https://mastodon.social/'
34
+ ],
35
+
36
+ NEWS_SITES: [
37
+ 'https://news.google.com/',
38
+ 'https://www.reddit.com/r/news/',
39
+ 'https://news.ycombinator.com/',
40
+ 'https://www.bbc.com/news',
41
+ 'https://www.cnn.com/',
42
+ 'https://techcrunch.com/',
43
+ 'https://www.theverge.com/'
44
+ ],
45
+
46
+ DEFAULT_SEARCH_TERMS: [
47
+ 'reviews', 'deals', 'discount', 'price', 'buy', 'shop', 'store',
48
+ 'compare', 'best', 'top', 'guide', 'how to', 'tutorial', 'tips',
49
+ 'news', 'update', 'latest', 'new', 'trending', 'popular', 'cheap',
50
+ 'free', 'download', 'online', 'service', 'product', 'website'
51
+ ],
52
+
53
+ ECOMMERCE_TERMS: [
54
+ 'buy online', 'shopping', 'store', 'sale', 'discount', 'coupon',
55
+ 'free shipping', 'best price', 'deals', 'outlet', 'marketplace'
56
+ ],
57
+
58
+ TECH_TERMS: [
59
+ 'software', 'app', 'download', 'tutorial', 'guide', 'review',
60
+ 'comparison', 'features', 'specs', 'performance', 'benchmark'
61
+ ]
62
+ });
63
+
64
+ /**
65
+ * Generates a random search term based on context or defaults
66
+ * @param {Array} customTerms - Custom search terms provided by user
67
+ * @param {string} context - Context hint for term selection (e.g., 'ecommerce', 'tech')
68
+ * @returns {string} Selected search term
69
+ */
70
+ function generateSearchTerm(customTerms, context = null) {
71
+ if (customTerms && customTerms.length > 0) {
72
+ return customTerms[Math.floor(Math.random() * customTerms.length)];
73
+ }
74
+
75
+ // Use context-specific terms if available
76
+ let termCollection = REFERRER_COLLECTIONS.DEFAULT_SEARCH_TERMS;
77
+ if (context === 'ecommerce') {
78
+ termCollection = REFERRER_COLLECTIONS.ECOMMERCE_TERMS;
79
+ } else if (context === 'tech') {
80
+ termCollection = REFERRER_COLLECTIONS.TECH_TERMS;
81
+ }
82
+
83
+ return termCollection[Math.floor(Math.random() * termCollection.length)];
84
+ }
85
+
86
+ /**
87
+ * Generates a search engine referrer URL
88
+ * @param {Array} searchTerms - Custom search terms
89
+ * @param {string} context - Context for term selection
90
+ * @param {boolean} forceDebug - Debug logging flag
91
+ * @returns {string} Generated search engine referrer URL
92
+ */
93
+ function generateSearchReferrer(searchTerms, context, forceDebug) {
94
+ const randomEngine = REFERRER_COLLECTIONS.SEARCH_ENGINES[
95
+ Math.floor(Math.random() * REFERRER_COLLECTIONS.SEARCH_ENGINES.length)
96
+ ];
97
+ const searchTerm = generateSearchTerm(searchTerms, context);
98
+ const referrerUrl = randomEngine + encodeURIComponent(searchTerm);
99
+
100
+ if (forceDebug) {
101
+ console.log(`[debug] Generated search referrer: ${referrerUrl} (engine: ${randomEngine.split('//')[1].split('/')[0]}, term: "${searchTerm}")`);
102
+ }
103
+
104
+ return referrerUrl;
105
+ }
106
+
107
+ /**
108
+ * Generates a social media referrer URL
109
+ * @param {boolean} forceDebug - Debug logging flag
110
+ * @returns {string} Generated social media referrer URL
111
+ */
112
+ function generateSocialMediaReferrer(forceDebug) {
113
+ const randomSocial = REFERRER_COLLECTIONS.SOCIAL_MEDIA[
114
+ Math.floor(Math.random() * REFERRER_COLLECTIONS.SOCIAL_MEDIA.length)
115
+ ];
116
+
117
+ if (forceDebug) {
118
+ console.log(`[debug] Generated social media referrer: ${randomSocial}`);
119
+ }
120
+
121
+ return randomSocial;
122
+ }
123
+
124
+ /**
125
+ * Generates a news site referrer URL
126
+ * @param {boolean} forceDebug - Debug logging flag
127
+ * @returns {string} Generated news site referrer URL
128
+ */
129
+ function generateNewsReferrer(forceDebug) {
130
+ const randomNews = REFERRER_COLLECTIONS.NEWS_SITES[
131
+ Math.floor(Math.random() * REFERRER_COLLECTIONS.NEWS_SITES.length)
132
+ ];
133
+
134
+ if (forceDebug) {
135
+ console.log(`[debug] Generated news referrer: ${randomNews}`);
136
+ }
137
+
138
+ return randomNews;
139
+ }
140
+
141
+ /**
142
+ * Validates a URL string
143
+ * @param {string} url - URL to validate
144
+ * @returns {boolean} True if valid HTTP/HTTPS URL
145
+ */
146
+ function isValidUrl(url) {
147
+ return typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://'));
148
+ }
149
+
150
+ /**
151
+ * Checks if a URL should have its referrer disabled
152
+ * @param {string} targetUrl - The URL being visited
153
+ * @param {Array} disableList - Array of URLs/patterns that should have no referrer
154
+ * @param {boolean} forceDebug - Debug logging flag
155
+ * @returns {boolean} True if referrer should be disabled for this URL
156
+ */
157
+ function shouldDisableReferrer(targetUrl, disableList, forceDebug = false) {
158
+ if (!disableList || !Array.isArray(disableList) || disableList.length === 0) {
159
+ return false;
160
+ }
161
+
162
+ if (!targetUrl || typeof targetUrl !== 'string') {
163
+ return false;
164
+ }
165
+
166
+ for (const disablePattern of disableList) {
167
+ if (typeof disablePattern !== 'string') continue;
168
+
169
+ // Exact URL match
170
+ if (targetUrl === disablePattern) {
171
+ if (forceDebug) console.log(`[debug] Referrer disabled for exact URL match: ${targetUrl}`);
172
+ return true;
173
+ }
174
+
175
+ // Domain/hostname match
176
+ try {
177
+ const targetHostname = new URL(targetUrl).hostname;
178
+ const disableHostname = new URL(disablePattern).hostname;
179
+ if (targetHostname === disableHostname) {
180
+ if (forceDebug) console.log(`[debug] Referrer disabled for domain match: ${targetHostname}`);
181
+ return true;
182
+ }
183
+ } catch (e) {
184
+ // If pattern is not a valid URL, try simple string matching
185
+ if (targetUrl.includes(disablePattern)) {
186
+ if (forceDebug) console.log(`[debug] Referrer disabled for pattern match: ${disablePattern} in ${targetUrl}`);
187
+ return true;
188
+ }
189
+ }
190
+ }
191
+
192
+ return false;
193
+ }
194
+
195
+ /**
196
+ * Generates a referrer URL based on the specified mode and options
197
+ * @param {Object|string|Array} referrerConfig - Referrer configuration
198
+ * @param {boolean} forceDebug - Debug logging flag
199
+ * @returns {string} Generated referrer URL or empty string
200
+ */
201
+ function generateReferrerUrl(referrerConfig, forceDebug = false) {
202
+ try {
203
+ // Handle simple string URLs
204
+ if (typeof referrerConfig === 'string') {
205
+ const url = isValidUrl(referrerConfig) ? referrerConfig : '';
206
+ if (forceDebug && url) {
207
+ console.log(`[debug] Using direct referrer URL: ${url}`);
208
+ } else if (forceDebug && !url) {
209
+ console.log(`[debug] Invalid referrer URL provided: ${referrerConfig}`);
210
+ }
211
+ return url;
212
+ }
213
+
214
+ // Handle arrays - pick random URL
215
+ if (Array.isArray(referrerConfig)) {
216
+ if (referrerConfig.length === 0) {
217
+ if (forceDebug) console.log(`[debug] Empty referrer array provided`);
218
+ return '';
219
+ }
220
+
221
+ const randomUrl = referrerConfig[Math.floor(Math.random() * referrerConfig.length)];
222
+ const url = isValidUrl(randomUrl) ? randomUrl : '';
223
+
224
+ if (forceDebug) {
225
+ console.log(`[debug] Selected referrer from array (${referrerConfig.length} options): ${url || 'invalid URL'}`);
226
+ }
227
+
228
+ return url;
229
+ }
230
+
231
+ // Handle object modes
232
+ if (typeof referrerConfig === 'object' && referrerConfig !== null && referrerConfig.mode) {
233
+ switch (referrerConfig.mode) {
234
+ case 'random_search': {
235
+ const searchTerms = referrerConfig.search_terms;
236
+ const context = referrerConfig.context; // Optional context hint
237
+ return generateSearchReferrer(searchTerms, context, forceDebug);
238
+ }
239
+
240
+ case 'social_media': {
241
+ return generateSocialMediaReferrer(forceDebug);
242
+ }
243
+
244
+ case 'news_sites': {
245
+ return generateNewsReferrer(forceDebug);
246
+ }
247
+
248
+ case 'direct_navigation': {
249
+ if (forceDebug) console.log(`[debug] Using direct navigation (no referrer)`);
250
+ return '';
251
+ }
252
+
253
+ case 'custom': {
254
+ const url = isValidUrl(referrerConfig.url) ? referrerConfig.url : '';
255
+ if (forceDebug) {
256
+ console.log(`[debug] Using custom referrer URL: ${url || 'invalid URL provided'}`);
257
+ }
258
+ return url;
259
+ }
260
+
261
+ case 'mixed': {
262
+ // Randomly choose between different referrer types
263
+ const modes = ['random_search', 'social_media', 'news_sites'];
264
+ const randomMode = modes[Math.floor(Math.random() * modes.length)];
265
+
266
+ if (forceDebug) console.log(`[debug] Mixed mode selected: ${randomMode}`);
267
+
268
+ const mixedConfig = { mode: randomMode };
269
+ if (randomMode === 'random_search' && referrerConfig.search_terms) {
270
+ mixedConfig.search_terms = referrerConfig.search_terms;
271
+ mixedConfig.context = referrerConfig.context;
272
+ }
273
+
274
+ return generateReferrerUrl(mixedConfig, forceDebug);
275
+ }
276
+
277
+ default: {
278
+ if (forceDebug) console.log(`[debug] Unknown referrer mode: ${referrerConfig.mode}`);
279
+ return '';
280
+ }
281
+ }
282
+ }
283
+
284
+ if (forceDebug) console.log(`[debug] Invalid referrer configuration type: ${typeof referrerConfig}`);
285
+ return '';
286
+ } catch (err) {
287
+ if (forceDebug) console.log(`[debug] Referrer generation failed: ${err.message}`);
288
+ return '';
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Main function to determine referrer for a specific URL
294
+ * Handles both referrer generation and referrer_disable functionality
295
+ * @param {string} targetUrl - The URL being visited
296
+ * @param {Object|string|Array} referrerConfig - Referrer configuration
297
+ * @param {Array} referrerDisable - Array of URLs that should have no referrer
298
+ * @param {boolean} forceDebug - Debug logging flag
299
+ * @returns {string} Generated referrer URL or empty string if disabled/none
300
+ */
301
+ function getReferrerForUrl(targetUrl, referrerConfig, referrerDisable, forceDebug = false) {
302
+ // Check if referrer should be disabled for this specific URL
303
+ if (shouldDisableReferrer(targetUrl, referrerDisable, forceDebug)) {
304
+ return '';
305
+ }
306
+
307
+ // Generate referrer normally if not disabled
308
+ return generateReferrerUrl(referrerConfig, forceDebug);
309
+ }
310
+
311
+ /**
312
+ * Validates referrer configuration
313
+ * @param {Object|string|Array} referrerConfig - Referrer configuration to validate
314
+ * @returns {Object} Validation result with isValid flag and error messages
315
+ */
316
+ function validateReferrerConfig(referrerConfig) {
317
+ const result = { isValid: true, errors: [], warnings: [] };
318
+
319
+ if (!referrerConfig) {
320
+ result.isValid = false;
321
+ result.errors.push('Referrer configuration is required');
322
+ return result;
323
+ }
324
+
325
+ // Validate string URLs
326
+ if (typeof referrerConfig === 'string') {
327
+ if (!isValidUrl(referrerConfig)) {
328
+ result.isValid = false;
329
+ result.errors.push('String referrer must be a valid HTTP/HTTPS URL');
330
+ }
331
+ return result;
332
+ }
333
+
334
+ // Validate arrays
335
+ if (Array.isArray(referrerConfig)) {
336
+ if (referrerConfig.length === 0) {
337
+ result.warnings.push('Empty referrer array will result in no referrer');
338
+ } else {
339
+ referrerConfig.forEach((url, index) => {
340
+ if (!isValidUrl(url)) {
341
+ result.errors.push(`Array item ${index} is not a valid HTTP/HTTPS URL: ${url}`);
342
+ result.isValid = false;
343
+ }
344
+ });
345
+ }
346
+ return result;
347
+ }
348
+
349
+ // Validate object modes
350
+ if (typeof referrerConfig === 'object') {
351
+ const validModes = ['random_search', 'social_media', 'news_sites', 'direct_navigation', 'custom', 'mixed'];
352
+
353
+ if (!referrerConfig.mode) {
354
+ result.isValid = false;
355
+ result.errors.push('Object referrer configuration must have a "mode" property');
356
+ return result;
357
+ }
358
+
359
+ if (!validModes.includes(referrerConfig.mode)) {
360
+ result.isValid = false;
361
+ result.errors.push(`Invalid referrer mode: ${referrerConfig.mode}. Valid modes: ${validModes.join(', ')}`);
362
+ return result;
363
+ }
364
+
365
+ // Mode-specific validation
366
+ switch (referrerConfig.mode) {
367
+ case 'custom':
368
+ if (!referrerConfig.url) {
369
+ result.isValid = false;
370
+ result.errors.push('Custom mode requires a "url" property');
371
+ } else if (!isValidUrl(referrerConfig.url)) {
372
+ result.isValid = false;
373
+ result.errors.push('Custom mode URL must be a valid HTTP/HTTPS URL');
374
+ }
375
+ break;
376
+
377
+ case 'random_search':
378
+ if (referrerConfig.search_terms && !Array.isArray(referrerConfig.search_terms)) {
379
+ result.warnings.push('search_terms should be an array of strings');
380
+ }
381
+ if (referrerConfig.search_terms && referrerConfig.search_terms.length === 0) {
382
+ result.warnings.push('Empty search_terms array will use default terms');
383
+ }
384
+ break;
385
+ }
386
+
387
+ return result;
388
+ }
389
+
390
+ result.isValid = false;
391
+ result.errors.push('Referrer configuration must be a string, array, or object');
392
+ return result;
393
+ }
394
+
395
+ /**
396
+ * Validates referrer_disable configuration
397
+ * @param {Array} referrerDisable - Array of URLs/patterns to disable referrer for
398
+ * @returns {Object} Validation result with isValid flag and error messages
399
+ */
400
+ function validateReferrerDisable(referrerDisable) {
401
+ const result = { isValid: true, errors: [], warnings: [] };
402
+
403
+ if (!referrerDisable) {
404
+ return result; // referrer_disable is optional
405
+ }
406
+
407
+ if (!Array.isArray(referrerDisable)) {
408
+ result.isValid = false;
409
+ result.errors.push('referrer_disable must be an array of URLs/patterns');
410
+ return result;
411
+ }
412
+
413
+ if (referrerDisable.length === 0) {
414
+ result.warnings.push('Empty referrer_disable array has no effect');
415
+ return result;
416
+ }
417
+
418
+ referrerDisable.forEach((pattern, index) => {
419
+ if (typeof pattern !== 'string') {
420
+ result.errors.push(`referrer_disable item ${index} must be a string (got ${typeof pattern})`);
421
+ result.isValid = false;
422
+ } else if (pattern.trim() === '') {
423
+ result.warnings.push(`referrer_disable item ${index} is empty string`);
424
+ } else if (!pattern.includes('.') && !pattern.includes('/')) {
425
+ result.warnings.push(`referrer_disable item ${index} "${pattern}" might be too broad - consider using full URLs or hostnames`);
426
+ }
427
+ });
428
+
429
+ if (referrerDisable.length > 100) {
430
+ result.warnings.push('Large referrer_disable list (>100 items) may impact performance');
431
+ }
432
+
433
+ return result;
434
+ }
435
+
436
+ /**
437
+ * Gets available referrer modes and their descriptions
438
+ * @returns {Object} Object containing mode descriptions
439
+ */
440
+ function getReferrerModes() {
441
+ return {
442
+ 'random_search': 'Generate random search engine referrers with customizable search terms',
443
+ 'social_media': 'Use random social media platform referrers',
444
+ 'news_sites': 'Use random news website referrers',
445
+ 'direct_navigation': 'No referrer (simulates direct URL entry)',
446
+ 'custom': 'Use a specific custom referrer URL',
447
+ 'mixed': 'Randomly mix different referrer types for varied traffic simulation'
448
+ };
449
+ }
450
+
451
+ /**
452
+ * Gets statistics about available referrer collections
453
+ * @returns {Object} Statistics about referrer collections
454
+ */
455
+ function getReferrerStats() {
456
+ return {
457
+ searchEngines: REFERRER_COLLECTIONS.SEARCH_ENGINES.length,
458
+ socialMedia: REFERRER_COLLECTIONS.SOCIAL_MEDIA.length,
459
+ newsSites: REFERRER_COLLECTIONS.NEWS_SITES.length,
460
+ defaultSearchTerms: REFERRER_COLLECTIONS.DEFAULT_SEARCH_TERMS.length,
461
+ ecommerceTerms: REFERRER_COLLECTIONS.ECOMMERCE_TERMS.length,
462
+ techTerms: REFERRER_COLLECTIONS.TECH_TERMS.length
463
+ };
464
+ }
465
+
466
+ module.exports = {
467
+ generateReferrerUrl,
468
+ getReferrerForUrl,
469
+ shouldDisableReferrer,
470
+ validateReferrerConfig,
471
+ validateReferrerDisable,
472
+ getReferrerModes,
473
+ getReferrerStats,
474
+ generateSearchReferrer,
475
+ generateSocialMediaReferrer,
476
+ generateNewsReferrer,
477
+ isValidUrl,
478
+ REFERRER_COLLECTIONS
479
+ };
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.31 ===
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.31'; // 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.31",
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": {