@fanboynz/network-scanner 2.0.2 → 2.0.4

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,6 +211,14 @@ async function isPageSafeToClose(page, forceDebug) {
211
211
  return true; // Already closed
212
212
  }
213
213
 
214
+ // EXTRA SAFETY: Never close pages that might be in injection process
215
+ try {
216
+ const url = page.url();
217
+ if (url && url !== 'about:blank' && Date.now() - (pageCreationTracker.get(page) || 0) < 30000) {
218
+ return false; // Don't close recently created pages (within 30 seconds)
219
+ }
220
+ } catch (err) { /* ignore */ }
221
+
214
222
  const usage = pageUsageTracker.get(page);
215
223
  if (!usage) {
216
224
  // No usage data - assume safe if page exists for a while
@@ -991,4 +999,4 @@ if (originalPageClose) {
991
999
  }
992
1000
  return originalPageClose.apply(this, args);
993
1001
  };
994
- }
1002
+ }
package/lib/cloudflare.js CHANGED
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Cloudflare bypass and challenge handling module - Optimized with smart detection and adaptive timeouts
3
+ * Version: 2.6.1 - timeoutId is not defined & race condition fix
4
+ * Version: 2.6.0 - Memory leak fixes and timeout cleanup
3
5
  * Version: 2.5.0 - Fix Frame Lifecycle issue, Timing and Race condition
4
6
  * Version: 2.4.1 - Bump timeout values
5
7
  * Version: 2.4.0 - Fix possible endless loops with retry logic and loop detection
@@ -16,7 +18,7 @@ const { formatLogMessage } = require('./colorize');
16
18
  /**
17
19
  * Module version information
18
20
  */
19
- const CLOUDFLARE_MODULE_VERSION = '2.5.0';
21
+ const CLOUDFLARE_MODULE_VERSION = '2.6.1';
20
22
 
21
23
  /**
22
24
  * Timeout constants for various operations (in milliseconds)
@@ -133,6 +135,8 @@ class CloudflareDetectionCache {
133
135
  this.ttl = ttl;
134
136
  this.hits = 0;
135
137
  this.misses = 0;
138
+ // Prevent memory buildup in long-running processes
139
+ this.cleanupInterval = setInterval(() => this.cleanupExpired(), ttl / 10);
136
140
  }
137
141
 
138
142
  getCacheKey(url) {
@@ -175,6 +179,20 @@ class CloudflareDetectionCache {
175
179
  }
176
180
  }
177
181
 
182
+ cleanupExpired() {
183
+ const now = Date.now();
184
+ for (const [key, value] of this.cache.entries()) {
185
+ if (now - value.timestamp >= this.ttl) {
186
+ this.cache.delete(key);
187
+ }
188
+ }
189
+ }
190
+
191
+ destroy() {
192
+ if (this.cleanupInterval) clearInterval(this.cleanupInterval);
193
+ this.clear();
194
+ }
195
+
178
196
  clear() {
179
197
  this.cache.clear();
180
198
  this.hits = 0;
@@ -302,24 +320,32 @@ async function safePageEvaluate(page, func, timeout = TIMEOUTS.PAGE_EVALUATION_S
302
320
  let lastError = null;
303
321
 
304
322
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
323
+ let timeoutId = null;
324
+
305
325
  try {
306
- // Validate page/frame state before evaluation
326
+ // Validate page state before evaluation
307
327
  if (page.isClosed()) {
308
328
  throw new Error('Page is closed');
309
329
  }
310
330
 
311
- // Check if main frame is still attached
312
- const mainFrame = page.mainFrame();
313
- if (mainFrame.isDetached()) {
314
- throw new Error('Main frame is detached');
315
- }
316
331
 
317
332
  const result = await Promise.race([
318
- page.evaluate(func),
319
- new Promise((_, reject) =>
320
- setTimeout(() => reject(new Error('Page evaluation timeout')), timeout)
321
- )
333
+ page.evaluate(() => {
334
+ // Validate execution context inside evaluation to prevent race
335
+ if (typeof window === 'undefined' || !document) {
336
+ throw new Error('Execution context invalid');
337
+ }
338
+ return (func)();
339
+ }),
340
+ new Promise((_, reject) => {
341
+ timeoutId = setTimeout(() => reject(new Error('Page evaluation timeout')), timeout);
342
+ })
322
343
  ]);
344
+
345
+ // Clear timeout if evaluation completed first
346
+ if (timeoutId) {
347
+ clearTimeout(timeoutId);
348
+ }
323
349
 
324
350
  if (forceDebug && attempt > 1) {
325
351
  console.log(formatLogMessage('cloudflare', `Page evaluation succeeded on attempt ${attempt}`));
@@ -327,6 +353,11 @@ async function safePageEvaluate(page, func, timeout = TIMEOUTS.PAGE_EVALUATION_S
327
353
 
328
354
  return result;
329
355
  } catch (error) {
356
+ // Ensure timeout is cleared on any error
357
+ if (timeoutId) {
358
+ clearTimeout(timeoutId);
359
+ }
360
+
330
361
  lastError = error;
331
362
  const errorType = categorizeError(error);
332
363
 
@@ -396,9 +427,9 @@ async function safeClick(page, selector, timeout = TIMEOUTS.CLICK_TIMEOUT) {
396
427
  try {
397
428
  return await Promise.race([
398
429
  page.click(selector, { timeout: timeout }),
399
- new Promise((_, reject) =>
400
- setTimeout(() => reject(new Error('Click timeout')), timeout + TIMEOUTS.CLICK_TIMEOUT_BUFFER)
401
- )
430
+ new Promise((_, reject) => {
431
+ setTimeout(() => reject(new Error('Click timeout')), timeout + TIMEOUTS.CLICK_TIMEOUT_BUFFER);
432
+ })
402
433
  ]);
403
434
  } catch (error) {
404
435
  throw new Error(`Click failed: ${error.message}`);
@@ -412,9 +443,9 @@ async function safeWaitForNavigation(page, timeout = TIMEOUTS.NAVIGATION_TIMEOUT
412
443
  try {
413
444
  return await Promise.race([
414
445
  page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: timeout }),
415
- new Promise((_, reject) =>
416
- setTimeout(() => reject(new Error('Navigation timeout')), timeout + TIMEOUTS.NAVIGATION_TIMEOUT_BUFFER)
417
- )
446
+ new Promise((_, reject) => {
447
+ setTimeout(() => reject(new Error('Navigation timeout')), timeout + TIMEOUTS.NAVIGATION_TIMEOUT_BUFFER);
448
+ })
418
449
  ]);
419
450
  } catch (error) {
420
451
  console.warn(formatLogMessage('cloudflare', `Navigation wait failed: ${error.message}`));
@@ -933,16 +964,29 @@ async function attemptChallengeSolveWithTimeout(page, currentUrl, challengeInfo,
933
964
  error: null,
934
965
  method: null
935
966
  };
967
+
968
+ let timeoutId = null;
936
969
 
937
970
  try {
971
+ const timeoutPromise = new Promise((_, reject) => {
972
+ timeoutId = setTimeout(() => reject(new Error('Challenge solving timeout')), FAST_TIMEOUTS.CHALLENGE_SOLVING);
973
+ });
938
974
  // Reduced timeout for challenge solving
939
- return await Promise.race([
975
+ const finalResult = await Promise.race([
940
976
  attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug),
941
- new Promise((_, reject) =>
942
- setTimeout(() => reject(new Error('Challenge solving timeout')), FAST_TIMEOUTS.CHALLENGE_SOLVING)
943
- )
977
+ timeoutPromise
944
978
  ]);
979
+ // Clear timeout if operation completed first
980
+ if (timeoutId) {
981
+ clearTimeout(timeoutId);
982
+ }
983
+ return finalResult;
984
+
945
985
  } catch (error) {
986
+ // Clear timeout on error
987
+ if (timeoutId) {
988
+ clearTimeout(timeoutId);
989
+ }
946
990
  result.error = `Challenge solving timed out: ${error.message}`;
947
991
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Challenge solving timeout for ${currentUrl}`));
948
992
  return result;
@@ -1164,8 +1208,14 @@ async function waitForJSChallengeCompletion(page, forceDebug = false) {
1164
1208
  error: null
1165
1209
  };
1166
1210
 
1211
+ let timeoutId = null;
1212
+
1167
1213
  try {
1168
1214
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Waiting for JS challenge completion`));
1215
+
1216
+ const timeoutPromise = new Promise((_, reject) => {
1217
+ timeoutId = setTimeout(() => reject(new Error('JS challenge timeout')), TIMEOUTS.JS_CHALLENGE_BUFFER);
1218
+ });
1169
1219
 
1170
1220
  // Reduced timeout for JS challenge completion
1171
1221
  await Promise.race([
@@ -1178,14 +1228,22 @@ async function waitForJSChallengeCompletion(page, forceDebug = false) {
1178
1228
  },
1179
1229
  { timeout: FAST_TIMEOUTS.JS_CHALLENGE }
1180
1230
  ),
1181
- new Promise((_, reject) =>
1182
- setTimeout(() => reject(new Error('JS challenge timeout')), TIMEOUTS.JS_CHALLENGE_BUFFER)
1183
- )
1231
+ timeoutPromise
1184
1232
  ]);
1233
+
1234
+ // Clear timeout if completion detected first
1235
+ if (timeoutId) {
1236
+ clearTimeout(timeoutId);
1237
+ }
1185
1238
 
1186
1239
  result.success = true;
1187
1240
  if (forceDebug) console.log(formatLogMessage('cloudflare', `JS challenge completed automatically`));
1188
1241
  } catch (error) {
1242
+ // Clear timeout on error
1243
+ if (timeoutId) {
1244
+ clearTimeout(timeoutId);
1245
+ }
1246
+
1189
1247
  result.error = `JS challenge timeout: ${error.message}`;
1190
1248
  if (forceDebug) console.log(formatLogMessage('cloudflare', `JS challenge wait failed: ${error.message}`));
1191
1249
  }
@@ -1752,6 +1810,15 @@ function clearDetectionCache() {
1752
1810
  detectionCache.clear();
1753
1811
  }
1754
1812
 
1813
+ /**
1814
+ * Cleanup function to prevent memory leaks in long-running processes
1815
+ */
1816
+ function cleanup() {
1817
+ if (detectionCache) {
1818
+ detectionCache.destroy();
1819
+ }
1820
+ }
1821
+
1755
1822
  module.exports = {
1756
1823
  analyzeCloudflareChallenge,
1757
1824
  handlePhishingWarning,
@@ -1775,5 +1842,7 @@ module.exports = {
1775
1842
  ERROR_TYPES,
1776
1843
  RETRY_CONFIG,
1777
1844
  getRetryConfig,
1778
- detectChallengeLoop
1779
- };
1845
+ detectChallengeLoop,
1846
+ // Memory management
1847
+ cleanup
1848
+ };
package/nwss.js CHANGED
@@ -1,4 +1,4 @@
1
- // === Network scanner script (nwss.js) v2.0.2 ===
1
+ // === Network scanner script (nwss.js) v2.0.4 ===
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
@@ -127,7 +127,7 @@ const { navigateWithRedirectHandling, handleRedirectTimeout } = require('./lib/r
127
127
  const { monitorBrowserHealth, isBrowserHealthy, isQuicklyResponsive, performGroupWindowCleanup, performRealtimeWindowCleanup, trackPageForRealtime, updatePageUsage } = require('./lib/browserhealth');
128
128
 
129
129
  // --- Script Configuration & Constants ---
130
- const VERSION = '2.0.2'; // Script version
130
+ const VERSION = '2.0.4'; // Script version
131
131
 
132
132
  // get startTime
133
133
  const startTime = Date.now();
@@ -1059,17 +1059,48 @@ function matchesIgnoreDomain(domain, ignorePatterns) {
1059
1059
  }
1060
1060
 
1061
1061
  function setupFrameHandling(page, forceDebug) {
1062
+ // Track active frames and clear on navigation to prevent detached frame access
1063
+ let activeFrames = new Set();
1064
+
1065
+ // Clear frame tracking on navigation to prevent stale references
1066
+ page.on('framenavigated', (frame) => {
1067
+ if (frame === page.mainFrame()) {
1068
+ // Main frame navigated - clear all tracked frames
1069
+ activeFrames.clear();
1070
+ }
1071
+ });
1072
+
1062
1073
  // Handle frame creation with error suppression
1063
1074
  page.on('frameattached', async (frame) => {
1064
- // Enhanced frame handling for Puppeteer 23.x
1075
+ // Enhanced frame handling with detached frame protection
1065
1076
  try {
1066
- // Check if frame is valid before processing
1067
- if (frame.isDetached()) {
1077
+ // Multiple checks for frame validity to prevent detached frame errors
1078
+ if (!frame) {
1068
1079
  if (forceDebug) {
1069
- console.log(formatLogMessage('debug', `Skipping detached frame`));
1080
+ console.log(formatLogMessage('debug', `Skipping null frame`));
1070
1081
  }
1071
1082
  return;
1072
1083
  }
1084
+
1085
+ // Enhanced frame validation with multiple safety checks
1086
+ let frameUrl;
1087
+ try {
1088
+ // Test frame accessibility first
1089
+ frameUrl = frame.url();
1090
+
1091
+ // Check if frame is detached (if method exists)
1092
+ if (frame.isDetached && frame.isDetached()) {
1093
+ if (forceDebug) {
1094
+ console.log(formatLogMessage('debug', `Skipping detached frame`));
1095
+ }
1096
+ return;
1097
+ }
1098
+ } catch (frameAccessError) {
1099
+ // Frame is not accessible (likely detached)
1100
+ return;
1101
+ }
1102
+
1103
+ activeFrames.add(frame);
1073
1104
  } catch (detachError) {
1074
1105
  // Frame state checking can throw in 23.x, handle gracefully
1075
1106
  if (forceDebug) {
@@ -1079,9 +1110,7 @@ function setupFrameHandling(page, forceDebug) {
1079
1110
  }
1080
1111
 
1081
1112
  if (frame.parentFrame()) { // Only handle child frames, not main frame
1082
- try {
1083
- const frameUrl = frame.url();
1084
-
1113
+ try {
1085
1114
  if (forceDebug) {
1086
1115
  console.log(formatLogMessage('debug', `New frame attached: ${frameUrl || 'about:blank'}`));
1087
1116
  }
@@ -1139,7 +1168,17 @@ function setupFrameHandling(page, forceDebug) {
1139
1168
  });
1140
1169
  // Handle frame navigations (keep this for monitoring)
1141
1170
  page.on('framenavigated', (frame) => {
1142
- const frameUrl = frame.url();
1171
+ let frameUrl;
1172
+
1173
+ // Skip if frame is not in our active set
1174
+ if (!activeFrames.has(frame)) return;
1175
+
1176
+ try {
1177
+ frameUrl = frame.url();
1178
+ } catch (urlErr) {
1179
+ // Frame likely detached during navigation
1180
+ return;
1181
+ }
1143
1182
  if (forceDebug &&
1144
1183
  frameUrl &&
1145
1184
  frameUrl !== 'about:blank' &&
@@ -1154,8 +1193,18 @@ function setupFrameHandling(page, forceDebug) {
1154
1193
 
1155
1194
  // Optional: Handle frame detachment for cleanup
1156
1195
  page.on('framedetached', (frame) => {
1196
+ // Remove from active tracking
1197
+ activeFrames.delete(frame);
1198
+
1199
+ // Skip logging if we can't access frame URL
1200
+ let frameUrl;
1157
1201
  if (forceDebug) {
1158
- const frameUrl = frame.url();
1202
+ try {
1203
+ frameUrl = frame.url();
1204
+ } catch (urlErr) {
1205
+ // Frame already detached, can't get URL
1206
+ return;
1207
+ }
1159
1208
  if (frameUrl &&
1160
1209
  frameUrl !== 'about:blank' &&
1161
1210
  frameUrl !== 'about:srcdoc' &&
@@ -1583,6 +1632,11 @@ function setupFrameHandling(page, forceDebug) {
1583
1632
  const shouldInjectEvalForPage = siteConfig.evaluateOnNewDocument === true || globalEvalOnDoc;
1584
1633
  let evalOnDocSuccess = false; // Track injection success for fallback logic
1585
1634
 
1635
+ // PREVENT realtime cleanup during injection to avoid "Session closed" errors
1636
+ if (shouldInjectEvalForPage && siteConfig.window_cleanup === "realtime") {
1637
+ updatePageUsage(page, true); // Mark page as actively processing BEFORE injection
1638
+ }
1639
+
1586
1640
  if (shouldInjectEvalForPage) {
1587
1641
  if (forceDebug) {
1588
1642
  if (globalEvalOnDoc) {
@@ -1595,8 +1649,13 @@ function setupFrameHandling(page, forceDebug) {
1595
1649
  // Strategy 1: Try full injection with health check
1596
1650
  let browserResponsive = false;
1597
1651
  try {
1652
+ // Check if browser is still connected before attempting health check
1653
+ if (!browserInstance.isConnected()) {
1654
+ throw new Error('Browser not connected');
1655
+ }
1656
+
1598
1657
  await Promise.race([
1599
- browserInstance.version(), // Quick responsiveness test
1658
+ browserInstance.pages(), // Simple existence check that doesn't require active session
1600
1659
  new Promise((_, reject) =>
1601
1660
  setTimeout(() => reject(new Error('Browser health check timeout')), 3000)
1602
1661
  )
@@ -1616,6 +1675,13 @@ function setupFrameHandling(page, forceDebug) {
1616
1675
  await Promise.race([
1617
1676
  // Main injection with all safety checks
1618
1677
  page.evaluateOnNewDocument(() => {
1678
+ // Prevent duplicate injections
1679
+ if (window.__nwss_injection_applied) {
1680
+ console.log('[evalOnDoc] Already injected, skipping');
1681
+ return;
1682
+ }
1683
+ window.__nwss_injection_applied = true;
1684
+
1619
1685
  // Wrap everything in try-catch to prevent page crashes
1620
1686
  try {
1621
1687
  // Add timeout check within the injection
@@ -1719,7 +1785,18 @@ function setupFrameHandling(page, forceDebug) {
1719
1785
  // Strategy 3: Fallback - Try minimal injection (just fetch monitoring)
1720
1786
  try {
1721
1787
  await Promise.race([
1722
- page.evaluateOnNewDocument(() => {
1788
+ (async () => {
1789
+ // Validate page state before minimal injection
1790
+ if (!page || page.isClosed()) {
1791
+ throw new Error('Page is closed');
1792
+ }
1793
+
1794
+ const pageUrl = await page.url().catch(() => 'about:blank');
1795
+ if (pageUrl === 'about:blank') {
1796
+ throw new Error('Cannot inject on about:blank');
1797
+ }
1798
+
1799
+ return page.evaluateOnNewDocument(() => {
1723
1800
  // Minimal injection - just fetch monitoring
1724
1801
  if (window.fetch) {
1725
1802
  const originalFetch = window.fetch;
@@ -1732,7 +1809,8 @@ function setupFrameHandling(page, forceDebug) {
1732
1809
  }
1733
1810
  };
1734
1811
  }
1735
- }),
1812
+ });
1813
+ })(),
1736
1814
  new Promise((_, reject) =>
1737
1815
  setTimeout(() => reject(new Error('Minimal injection timeout')), 3000)
1738
1816
  )
@@ -1760,6 +1838,10 @@ function setupFrameHandling(page, forceDebug) {
1760
1838
  if (!evalOnDocSuccess) {
1761
1839
  console.warn(formatLogMessage('warn', `[evalOnDoc] All injection strategies failed for ${currentUrl} - continuing with standard request monitoring only`));
1762
1840
  }
1841
+ // Allow realtime cleanup to proceed after injection completes
1842
+ if (shouldInjectEvalForPage && siteConfig.window_cleanup === "realtime") {
1843
+ updatePageUsage(page, false); // Mark page as idle after injection
1844
+ }
1763
1845
  }
1764
1846
  // --- END: evaluateOnNewDocument for Fetch/XHR Interception ---
1765
1847
 
@@ -2161,8 +2243,15 @@ function setupFrameHandling(page, forceDebug) {
2161
2243
  // This prevents redirect destinations from being marked as third-party
2162
2244
  const isFirstParty = checkedRootDomain && firstPartyDomains.has(checkedRootDomain);
2163
2245
 
2164
- // Block infinite iframe loops
2165
- const frameUrl = request.frame() ? request.frame().url() : '';
2246
+ // Block infinite iframe loops - safely access frame URL
2247
+ const frameUrl = (() => {
2248
+ try {
2249
+ const frame = request.frame();
2250
+ return frame ? frame.url() : '';
2251
+ } catch (err) {
2252
+ return '';
2253
+ }
2254
+ })();
2166
2255
  if (frameUrl && frameUrl.includes('creative.dmzjmp.com') &&
2167
2256
  request.url().includes('go.dmzjmp.com/api/models')) {
2168
2257
  if (forceDebug) {
@@ -2174,8 +2263,18 @@ function setupFrameHandling(page, forceDebug) {
2174
2263
 
2175
2264
  // Enhanced debug logging to show which frame the request came from
2176
2265
  if (forceDebug) {
2177
- const frameUrl = request.frame() ? request.frame().url() : 'unknown-frame';
2178
- const isMainFrame = request.frame() === page.mainFrame();
2266
+ let frameUrl = 'unknown-frame';
2267
+ let isMainFrame = false;
2268
+
2269
+ try {
2270
+ const frame = request.frame();
2271
+ if (frame) {
2272
+ frameUrl = frame.url();
2273
+ isMainFrame = frame === page.mainFrame();
2274
+ }
2275
+ } catch (frameErr) {
2276
+ frameUrl = 'detached-frame';
2277
+ }
2179
2278
  console.log(formatLogMessage('debug', `${messageColors.highlight('[req]')}[frame: ${isMainFrame ? 'main' : 'iframe'}] ${frameUrl} → ${request.url()}`));
2180
2279
  }
2181
2280
 
@@ -2997,6 +3096,14 @@ function setupFrameHandling(page, forceDebug) {
2997
3096
  }
2998
3097
 
2999
3098
  for (let i = 1; i <= totalReloads; i++) {
3099
+ // Clear any stale frame references before reload
3100
+ try {
3101
+ await page.evaluate(() => {
3102
+ // Force cleanup of any pending frame operations
3103
+ if (window.requestIdleCallback) window.requestIdleCallback(() => {});
3104
+ });
3105
+ } catch (cleanupErr) { /* ignore */ }
3106
+
3000
3107
  if (siteConfig.clear_sitedata === true) {
3001
3108
  try {
3002
3109
  let reloadClearSession = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanboynz/network-scanner",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
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": {