@fanboynz/network-scanner 2.0.2 → 2.0.3

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,6 @@
1
1
  /**
2
2
  * Cloudflare bypass and challenge handling module - Optimized with smart detection and adaptive timeouts
3
+ * Version: 2.6.0 - Memory leak fixes and timeout cleanup
3
4
  * Version: 2.5.0 - Fix Frame Lifecycle issue, Timing and Race condition
4
5
  * Version: 2.4.1 - Bump timeout values
5
6
  * Version: 2.4.0 - Fix possible endless loops with retry logic and loop detection
@@ -16,7 +17,7 @@ const { formatLogMessage } = require('./colorize');
16
17
  /**
17
18
  * Module version information
18
19
  */
19
- const CLOUDFLARE_MODULE_VERSION = '2.5.0';
20
+ const CLOUDFLARE_MODULE_VERSION = '2.6.0';
20
21
 
21
22
  /**
22
23
  * Timeout constants for various operations (in milliseconds)
@@ -133,6 +134,8 @@ class CloudflareDetectionCache {
133
134
  this.ttl = ttl;
134
135
  this.hits = 0;
135
136
  this.misses = 0;
137
+ // Prevent memory buildup in long-running processes
138
+ this.cleanupInterval = setInterval(() => this.cleanupExpired(), ttl / 10);
136
139
  }
137
140
 
138
141
  getCacheKey(url) {
@@ -175,6 +178,20 @@ class CloudflareDetectionCache {
175
178
  }
176
179
  }
177
180
 
181
+ cleanupExpired() {
182
+ const now = Date.now();
183
+ for (const [key, value] of this.cache.entries()) {
184
+ if (now - value.timestamp >= this.ttl) {
185
+ this.cache.delete(key);
186
+ }
187
+ }
188
+ }
189
+
190
+ destroy() {
191
+ if (this.cleanupInterval) clearInterval(this.cleanupInterval);
192
+ this.clear();
193
+ }
194
+
178
195
  clear() {
179
196
  this.cache.clear();
180
197
  this.hits = 0;
@@ -314,12 +331,19 @@ async function safePageEvaluate(page, func, timeout = TIMEOUTS.PAGE_EVALUATION_S
314
331
  throw new Error('Main frame is detached');
315
332
  }
316
333
 
334
+ let timeoutId = null;
317
335
  const result = await Promise.race([
318
336
  page.evaluate(func),
319
- new Promise((_, reject) =>
320
- setTimeout(() => reject(new Error('Page evaluation timeout')), timeout)
321
- )
337
+ new Promise((_, reject) => {
338
+ timeoutId = setTimeout(() => reject(new Error('Page evaluation timeout')), timeout);
339
+ })
322
340
  ]);
341
+
342
+ // Clear timeout if evaluation completed first
343
+ if (timeoutId) {
344
+ clearTimeout(timeoutId);
345
+ timeoutId = null;
346
+ }
323
347
 
324
348
  if (forceDebug && attempt > 1) {
325
349
  console.log(formatLogMessage('cloudflare', `Page evaluation succeeded on attempt ${attempt}`));
@@ -327,6 +351,12 @@ async function safePageEvaluate(page, func, timeout = TIMEOUTS.PAGE_EVALUATION_S
327
351
 
328
352
  return result;
329
353
  } catch (error) {
354
+ // Ensure timeout is cleared on any error
355
+ if (timeoutId) {
356
+ clearTimeout(timeoutId);
357
+ timeoutId = null;
358
+ }
359
+
330
360
  lastError = error;
331
361
  const errorType = categorizeError(error);
332
362
 
@@ -396,9 +426,10 @@ async function safeClick(page, selector, timeout = TIMEOUTS.CLICK_TIMEOUT) {
396
426
  try {
397
427
  return await Promise.race([
398
428
  page.click(selector, { timeout: timeout }),
399
- new Promise((_, reject) =>
400
- setTimeout(() => reject(new Error('Click timeout')), timeout + TIMEOUTS.CLICK_TIMEOUT_BUFFER)
401
- )
429
+ new Promise((_, reject) => {
430
+ const timerId = setTimeout(() => reject(new Error('Click timeout')), timeout + TIMEOUTS.CLICK_TIMEOUT_BUFFER);
431
+ // Timer will be cleared when promise resolves/rejects
432
+ })
402
433
  ]);
403
434
  } catch (error) {
404
435
  throw new Error(`Click failed: ${error.message}`);
@@ -412,9 +443,10 @@ 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
+ const timerId = setTimeout(() => reject(new Error('Navigation timeout')), timeout + TIMEOUTS.NAVIGATION_TIMEOUT_BUFFER);
448
+ // Timer will be cleared when promise resolves/rejects
449
+ })
418
450
  ]);
419
451
  } catch (error) {
420
452
  console.warn(formatLogMessage('cloudflare', `Navigation wait failed: ${error.message}`));
@@ -931,18 +963,30 @@ async function attemptChallengeSolveWithTimeout(page, currentUrl, challengeInfo,
931
963
  const result = {
932
964
  success: false,
933
965
  error: null,
934
- method: null
966
+ method: null,
967
+ _timeoutId: null
935
968
  };
936
969
 
937
970
  try {
971
+ const timeoutPromise = new Promise((_, reject) => {
972
+ result._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 (result._timeoutId) {
981
+ clearTimeout(result._timeoutId);
982
+ }
983
+ return finalResult;
984
+
945
985
  } catch (error) {
986
+ // Clear timeout on error
987
+ if (result._timeoutId) {
988
+ clearTimeout(result._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;
@@ -1161,11 +1205,16 @@ async function handleEmbeddedIframeChallenge(page, forceDebug = false) {
1161
1205
  async function waitForJSChallengeCompletion(page, forceDebug = false) {
1162
1206
  const result = {
1163
1207
  success: false,
1164
- error: null
1208
+ error: null,
1209
+ _timeoutId: null
1165
1210
  };
1166
1211
 
1167
1212
  try {
1168
1213
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Waiting for JS challenge completion`));
1214
+
1215
+ const timeoutPromise = new Promise((_, reject) => {
1216
+ result._timeoutId = setTimeout(() => reject(new Error('JS challenge timeout')), TIMEOUTS.JS_CHALLENGE_BUFFER);
1217
+ });
1169
1218
 
1170
1219
  // Reduced timeout for JS challenge completion
1171
1220
  await Promise.race([
@@ -1178,14 +1227,22 @@ async function waitForJSChallengeCompletion(page, forceDebug = false) {
1178
1227
  },
1179
1228
  { timeout: FAST_TIMEOUTS.JS_CHALLENGE }
1180
1229
  ),
1181
- new Promise((_, reject) =>
1182
- setTimeout(() => reject(new Error('JS challenge timeout')), TIMEOUTS.JS_CHALLENGE_BUFFER)
1183
- )
1230
+ timeoutPromise
1184
1231
  ]);
1232
+
1233
+ // Clear timeout if completion detected first
1234
+ if (result._timeoutId) {
1235
+ clearTimeout(result._timeoutId);
1236
+ }
1185
1237
 
1186
1238
  result.success = true;
1187
1239
  if (forceDebug) console.log(formatLogMessage('cloudflare', `JS challenge completed automatically`));
1188
1240
  } catch (error) {
1241
+ // Clear timeout on error
1242
+ if (result._timeoutId) {
1243
+ clearTimeout(result._timeoutId);
1244
+ }
1245
+
1189
1246
  result.error = `JS challenge timeout: ${error.message}`;
1190
1247
  if (forceDebug) console.log(formatLogMessage('cloudflare', `JS challenge wait failed: ${error.message}`));
1191
1248
  }
@@ -1752,6 +1809,15 @@ function clearDetectionCache() {
1752
1809
  detectionCache.clear();
1753
1810
  }
1754
1811
 
1812
+ /**
1813
+ * Cleanup function to prevent memory leaks in long-running processes
1814
+ */
1815
+ function cleanup() {
1816
+ if (detectionCache) {
1817
+ detectionCache.destroy();
1818
+ }
1819
+ }
1820
+
1755
1821
  module.exports = {
1756
1822
  analyzeCloudflareChallenge,
1757
1823
  handlePhishingWarning,
@@ -1775,5 +1841,7 @@ module.exports = {
1775
1841
  ERROR_TYPES,
1776
1842
  RETRY_CONFIG,
1777
1843
  getRetryConfig,
1778
- detectChallengeLoop
1844
+ detectChallengeLoop,
1845
+ // Memory management
1846
+ cleanup
1779
1847
  };
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.3 ===
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.3'; // 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.3",
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": {