@fanboynz/network-scanner 1.0.35 → 1.0.37

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.
@@ -17,7 +17,8 @@ async function checkBrowserHealth(browserInstance, timeout = 5000) {
17
17
  pageCount: 0,
18
18
  error: null,
19
19
  responseTime: 0,
20
- recommendations: []
20
+ recommendations: [],
21
+ criticalError: false
21
22
  };
22
23
 
23
24
  const startTime = Date.now();
@@ -27,6 +28,7 @@ async function checkBrowserHealth(browserInstance, timeout = 5000) {
27
28
  if (!browserInstance || browserInstance.process() === null) {
28
29
  healthResult.error = 'Browser process not running';
29
30
  healthResult.recommendations.push('Create new browser instance');
31
+ healthResult.criticalError = true;
30
32
  return healthResult;
31
33
  }
32
34
 
@@ -71,7 +73,12 @@ async function checkBrowserHealth(browserInstance, timeout = 5000) {
71
73
  try { await testPage.close(); } catch (e) { /* ignore */ }
72
74
  }
73
75
  healthResult.error = `Page creation/navigation failed: ${pageTestError.message}`;
74
- healthResult.recommendations.push('Browser restart recommended');
76
+ if (isCriticalProtocolError(pageTestError)) {
77
+ healthResult.recommendations.push('Browser restart required - critical protocol error');
78
+ healthResult.criticalError = true;
79
+ } else {
80
+ healthResult.recommendations.push('Browser restart recommended');
81
+ }
75
82
  return healthResult;
76
83
  }
77
84
 
@@ -88,10 +95,14 @@ async function checkBrowserHealth(browserInstance, timeout = 5000) {
88
95
  healthResult.responseTime = Date.now() - startTime;
89
96
 
90
97
  // Categorize error types for better recommendations
91
- if (error.message.includes('timeout') || error.message.includes('unresponsive')) {
98
+ if (error.message.includes('Runtime.callFunctionOn timed out') ||
99
+ error.message.includes('Protocol error') ||
100
+ error.message.includes('Target closed')) {
101
+ healthResult.recommendations.push('Browser restart required - critical protocol error');
102
+ healthResult.criticalError = true;
103
+ } else if (error.message.includes('timeout') || error.message.includes('unresponsive')) {
92
104
  healthResult.recommendations.push('Browser restart required - unresponsive');
93
- } else if (error.message.includes('Protocol error') || error.message.includes('Target closed')) {
94
- healthResult.recommendations.push('Browser restart required - protocol error');
105
+ healthResult.criticalError = true;
95
106
  } else {
96
107
  healthResult.recommendations.push('Browser restart recommended - unknown error');
97
108
  }
@@ -152,6 +163,27 @@ async function checkBrowserMemory(browserInstance) {
152
163
  return memoryResult;
153
164
  }
154
165
 
166
+ /**
167
+ * Detects critical protocol errors that require immediate browser restart
168
+ */
169
+ function isCriticalProtocolError(error) {
170
+ if (!error || !error.message) return false;
171
+
172
+ const criticalErrors = [
173
+ 'Runtime.callFunctionOn timed out',
174
+ 'Protocol error',
175
+ 'Target closed',
176
+ 'Session closed',
177
+ 'Connection closed',
178
+ 'Browser has been closed',
179
+ 'Runtime.evaluate timed out'
180
+ ];
181
+
182
+ return criticalErrors.some(criticalError =>
183
+ error.message.includes(criticalError)
184
+ );
185
+ }
186
+
155
187
  /**
156
188
  * Performs comprehensive browser health assessment
157
189
  * @param {import('puppeteer').Browser} browserInstance - Puppeteer browser instance
@@ -196,6 +228,9 @@ async function performHealthAssessment(browserInstance, options = {}) {
196
228
  if (!assessment.browser.healthy) {
197
229
  assessment.overall = 'unhealthy';
198
230
  assessment.needsRestart = true;
231
+ } else if (assessment.browser.criticalError) {
232
+ assessment.overall = 'critical';
233
+ assessment.needsRestart = true;
199
234
  } else if (assessment.recommendations.length > 0) {
200
235
  assessment.overall = 'degraded';
201
236
  assessment.needsRestart = assessment.recommendations.some(rec =>
@@ -252,7 +287,10 @@ async function monitorBrowserHealth(browserInstance, context = {}, options = {})
252
287
  result.assessment = assessment;
253
288
 
254
289
  // Decision logic for restart
255
- if (assessment.needsRestart) {
290
+ if (assessment.browser.criticalError) {
291
+ result.shouldRestart = true;
292
+ result.reason = `Critical protocol error detected - immediate restart required`;
293
+ } else if (assessment.needsRestart) {
256
294
  result.shouldRestart = true;
257
295
  result.reason = `Browser health: ${assessment.overall} - ${assessment.recommendations[0] || 'restart needed'}`;
258
296
  } else if (urlsSinceCleanup >= cleanupInterval) {
@@ -304,5 +342,6 @@ module.exports = {
304
342
  checkBrowserMemory,
305
343
  performHealthAssessment,
306
344
  monitorBrowserHealth,
307
- isBrowserHealthy
345
+ isBrowserHealthy,
346
+ isCriticalProtocolError
308
347
  };
package/lib/cloudflare.js CHANGED
@@ -1,40 +1,96 @@
1
1
  /**
2
- * Cloudflare bypass and challenge handling module - Updated for 2025
2
+ * Cloudflare bypass and challenge handling module - Enhanced with timeout handling
3
3
  * Handles phishing warnings, Turnstile challenges, and modern Cloudflare protections
4
4
  */
5
5
 
6
6
  /**
7
- * Cross-version compatible timeout function for Puppeteer
8
- * @param {import('puppeteer').Page} page - Puppeteer page instance
9
- * @param {number} timeout - Timeout in milliseconds
10
- * @returns {Promise<void>}
7
+ * Cross-version compatible timeout function for Puppeteer with timeout protection
11
8
  */
12
9
  async function waitForTimeout(page, timeout) {
13
10
  try {
14
11
  // Try newer Puppeteer method first
15
12
  if (typeof page.waitForTimeout === 'function') {
16
- await page.waitForTimeout(timeout);
13
+ await Promise.race([
14
+ page.waitForTimeout(timeout),
15
+ new Promise((_, reject) => setTimeout(() => reject(new Error('waitForTimeout exceeded')), timeout + 5000))
16
+ ]);
17
17
  } else if (typeof page.waitFor === 'function') {
18
- // Fallback to older Puppeteer method
19
- await page.waitFor(timeout);
18
+ await Promise.race([
19
+ page.waitFor(timeout),
20
+ new Promise((_, reject) => setTimeout(() => reject(new Error('waitFor exceeded')), timeout + 5000))
21
+ ]);
20
22
  } else {
21
- // Ultimate fallback using setTimeout
22
23
  await new Promise(resolve => setTimeout(resolve, timeout));
23
24
  }
24
25
  } catch (error) {
25
26
  // If all else fails, use setTimeout
26
- await new Promise(resolve => setTimeout(resolve, timeout));
27
+ await new Promise(resolve => setTimeout(resolve, Math.min(timeout, 5000)));
27
28
  }
28
29
  }
29
30
 
30
31
  /**
31
- * Analyzes the current page to detect Cloudflare challenges - Updated for 2025
32
- * @param {import('puppeteer').Page} page - Puppeteer page instance
33
- * @returns {Promise<object>} Challenge information object
32
+ * Safe page evaluation with timeout protection
33
+ */
34
+ async function safePageEvaluate(page, func, timeout = 10000) {
35
+ try {
36
+ return await Promise.race([
37
+ page.evaluate(func),
38
+ new Promise((_, reject) =>
39
+ setTimeout(() => reject(new Error('Page evaluation timeout')), timeout)
40
+ )
41
+ ]);
42
+ } catch (error) {
43
+ console.warn(`[cloudflare] Page evaluation failed: ${error.message}`);
44
+ return {
45
+ isChallengePresent: false,
46
+ isPhishingWarning: false,
47
+ isTurnstile: false,
48
+ isJSChallenge: false,
49
+ isChallengeCompleted: false,
50
+ error: error.message
51
+ };
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Safe element clicking with timeout protection
57
+ */
58
+ async function safeClick(page, selector, timeout = 5000) {
59
+ try {
60
+ return await Promise.race([
61
+ page.click(selector, { timeout: timeout }),
62
+ new Promise((_, reject) =>
63
+ setTimeout(() => reject(new Error('Click timeout')), timeout + 1000)
64
+ )
65
+ ]);
66
+ } catch (error) {
67
+ throw new Error(`Click failed: ${error.message}`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Safe navigation waiting with timeout protection
73
+ */
74
+ async function safeWaitForNavigation(page, timeout = 15000) {
75
+ try {
76
+ return await Promise.race([
77
+ page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: timeout }),
78
+ new Promise((_, reject) =>
79
+ setTimeout(() => reject(new Error('Navigation timeout')), timeout + 2000)
80
+ )
81
+ ]);
82
+ } catch (error) {
83
+ console.warn(`[cloudflare] Navigation wait failed: ${error.message}`);
84
+ // Don't throw - just continue
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Analyzes the current page to detect Cloudflare challenges - Enhanced with timeout protection
34
90
  */
35
91
  async function analyzeCloudflareChallenge(page) {
36
92
  try {
37
- return await page.evaluate(() => {
93
+ return await safePageEvaluate(page, () => {
38
94
  const title = document.title || '';
39
95
  const bodyText = document.body ? document.body.textContent : '';
40
96
 
@@ -50,7 +106,6 @@ async function analyzeCloudflareChallenge(page) {
50
106
  const hasTurnstileCheckbox = document.querySelector('input[type="checkbox"].ctp-checkbox') !== null ||
51
107
  document.querySelector('.ctp-checkbox') !== null;
52
108
 
53
- // Legacy challenge detection (still used on some sites)
54
109
  const hasLegacyCheckbox = document.querySelector('input[type="checkbox"]#challenge-form') !== null ||
55
110
  document.querySelector('input[type="checkbox"][name="cf_captcha_kind"]') !== null;
56
111
 
@@ -73,14 +128,11 @@ async function analyzeCloudflareChallenge(page) {
73
128
  title.includes('Attention Required') ||
74
129
  document.querySelector('a[href*="continue"]') !== null;
75
130
 
76
- // Check for Turnstile response token
77
131
  const hasTurnstileResponse = document.querySelector('input[name="cf-turnstile-response"]') !== null;
78
132
 
79
- // Check for challenge completion indicators
80
133
  const isChallengeCompleted = hasTurnstileResponse &&
81
134
  document.querySelector('input[name="cf-turnstile-response"]')?.value;
82
135
 
83
- // Enhanced challenge detection logic
84
136
  const isChallengePresent = title.includes('Just a moment') ||
85
137
  title.includes('Checking your browser') ||
86
138
  bodyText.includes('Verify you are human') ||
@@ -107,9 +159,9 @@ async function analyzeCloudflareChallenge(page) {
107
159
  hasCaptcha,
108
160
  hasTurnstileResponse,
109
161
  url: window.location.href,
110
- bodySnippet: bodyText.substring(0, 200) // First 200 chars for debugging
162
+ bodySnippet: bodyText.substring(0, 200)
111
163
  };
112
- });
164
+ }, 10000); // 10 second timeout for page evaluation
113
165
  } catch (error) {
114
166
  return {
115
167
  isChallengePresent: false,
@@ -123,11 +175,7 @@ async function analyzeCloudflareChallenge(page) {
123
175
  }
124
176
 
125
177
  /**
126
- * Handles Cloudflare phishing warnings by clicking the continue button
127
- * @param {import('puppeteer').Page} page - Puppeteer page instance
128
- * @param {string} currentUrl - Current URL being processed
129
- * @param {boolean} forceDebug - Debug mode flag
130
- * @returns {Promise<object>} Result object with success status and details
178
+ * Handles Cloudflare phishing warnings with timeout protection
131
179
  */
132
180
  async function handlePhishingWarning(page, currentUrl, forceDebug = false) {
133
181
  const result = {
@@ -140,7 +188,7 @@ async function handlePhishingWarning(page, currentUrl, forceDebug = false) {
140
188
  try {
141
189
  if (forceDebug) console.log(`[debug][cloudflare] Checking for phishing warning on ${currentUrl}`);
142
190
 
143
- // Wait a moment for the warning page to load
191
+ // Shorter wait with timeout protection
144
192
  await waitForTimeout(page, 2000);
145
193
 
146
194
  const challengeInfo = await analyzeCloudflareChallenge(page);
@@ -150,14 +198,13 @@ async function handlePhishingWarning(page, currentUrl, forceDebug = false) {
150
198
  result.details = challengeInfo;
151
199
 
152
200
  if (forceDebug) {
153
- console.log(`[debug][cloudflare] Phishing warning detected on ${currentUrl}:`);
154
- console.log(`[debug][cloudflare] Page Title: "${challengeInfo.title}"`);
155
- console.log(`[debug][cloudflare] Current URL: ${challengeInfo.url}`);
201
+ console.log(`[debug][cloudflare] Phishing warning detected on ${currentUrl}`);
156
202
  }
157
203
 
158
204
  try {
159
- await page.click('a[href*="continue"]', { timeout: 5000 });
160
- await page.waitForNavigation({ waitUntil: 'load', timeout: 30000 });
205
+ // Use safe click with shorter timeout
206
+ await safeClick(page, 'a[href*="continue"]', 3000);
207
+ await safeWaitForNavigation(page, 10000);
161
208
 
162
209
  result.success = true;
163
210
  if (forceDebug) console.log(`[debug][cloudflare] Successfully bypassed phishing warning for ${currentUrl}`);
@@ -178,11 +225,7 @@ async function handlePhishingWarning(page, currentUrl, forceDebug = false) {
178
225
  }
179
226
 
180
227
  /**
181
- * Attempts to solve Cloudflare challenges including Turnstile - Updated for 2025
182
- * @param {import('puppeteer').Page} page - Puppeteer page instance
183
- * @param {string} currentUrl - Current URL being processed
184
- * @param {boolean} forceDebug - Debug mode flag
185
- * @returns {Promise<object>} Result object with success status and details
228
+ * Attempts to solve Cloudflare challenges with timeout protection
186
229
  */
187
230
  async function handleVerificationChallenge(page, currentUrl, forceDebug = false) {
188
231
  const result = {
@@ -196,8 +239,8 @@ async function handleVerificationChallenge(page, currentUrl, forceDebug = false)
196
239
  try {
197
240
  if (forceDebug) console.log(`[debug][cloudflare] Checking for verification challenge on ${currentUrl}`);
198
241
 
199
- // Wait for potential Cloudflare challenge to appear
200
- await waitForTimeout(page, 3000);
242
+ // Shorter wait for challenges
243
+ await waitForTimeout(page, 2000);
201
244
 
202
245
  const challengeInfo = await analyzeCloudflareChallenge(page);
203
246
  result.details = challengeInfo;
@@ -206,82 +249,70 @@ async function handleVerificationChallenge(page, currentUrl, forceDebug = false)
206
249
  result.attempted = true;
207
250
 
208
251
  if (forceDebug) {
209
- console.log(`[debug][cloudflare] Challenge detected on ${currentUrl}:`);
210
- console.log(`[debug][cloudflare] Page Title: "${challengeInfo.title}"`);
211
- console.log(`[debug][cloudflare] Current URL: ${challengeInfo.url}`);
212
- console.log(`[debug][cloudflare] Is Turnstile: ${challengeInfo.isTurnstile}`);
213
- console.log(`[debug][cloudflare] Is JS Challenge: ${challengeInfo.isJSChallenge}`);
214
- console.log(`[debug][cloudflare] Has Legacy Checkbox: ${challengeInfo.hasLegacyCheckbox}`);
215
- console.log(`[debug][cloudflare] Has Turnstile Iframe: ${challengeInfo.hasTurnstileIframe}`);
216
- console.log(`[debug][cloudflare] Has Turnstile Container: ${challengeInfo.hasTurnstileContainer}`);
217
- console.log(`[debug][cloudflare] Has CAPTCHA: ${challengeInfo.hasCaptcha}`);
252
+ console.log(`[debug][cloudflare] Challenge detected on ${currentUrl}`);
218
253
  }
219
254
 
220
255
  // Check for CAPTCHA that requires human intervention
221
256
  if (challengeInfo.hasCaptcha) {
222
257
  result.requiresHuman = true;
223
258
  result.error = 'CAPTCHA detected - requires human intervention';
224
- console.warn(`? [cloudflare] CAPTCHA detected on ${currentUrl} - requires human intervention`);
225
259
  if (forceDebug) console.log(`[debug][cloudflare] Skipping automatic bypass due to CAPTCHA requirement`);
226
260
  return result;
227
261
  }
228
262
 
229
- // Attempt to solve the challenge with updated methods
230
- const solveResult = await attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug);
263
+ // Attempt to solve the challenge with timeout protection
264
+ const solveResult = await attemptChallengeSolveWithTimeout(page, currentUrl, challengeInfo, forceDebug);
231
265
  result.success = solveResult.success;
232
266
  result.error = solveResult.error;
233
267
 
234
268
  } else {
235
269
  if (forceDebug) console.log(`[debug][cloudflare] No verification challenge detected on ${currentUrl}`);
236
- result.success = true; // No challenge to handle
270
+ result.success = true;
237
271
  }
238
272
  } catch (error) {
239
273
  result.error = error.message;
240
- if (forceDebug) {
241
- console.log(`[debug][cloudflare] Challenge check failed for ${currentUrl}:`);
242
- console.log(`[debug][cloudflare] Error: ${error.message}`);
243
- console.log(`[debug][cloudflare] Stack: ${error.stack}`);
244
- }
274
+ if (forceDebug) console.log(`[debug][cloudflare] Challenge check failed for ${currentUrl}: ${error.message}`);
245
275
  }
246
276
 
247
277
  return result;
248
278
  }
249
279
 
250
280
  /**
251
- * Attempts to solve a Cloudflare challenge with modern techniques - Updated for 2025
252
- * @param {import('puppeteer').Page} page - Puppeteer page instance
253
- * @param {string} currentUrl - Current URL being processed
254
- * @param {object} challengeInfo - Challenge analysis results
255
- * @param {boolean} forceDebug - Debug mode flag
256
- * @returns {Promise<object>} Solve attempt result
281
+ * Challenge solving with overall timeout protection
257
282
  */
258
- async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug = false) {
283
+ async function attemptChallengeSolveWithTimeout(page, currentUrl, challengeInfo, forceDebug = false) {
259
284
  const result = {
260
285
  success: false,
261
286
  error: null,
262
287
  method: null
263
288
  };
264
289
 
265
- // Method 1: Handle Turnstile challenges (2025 primary method)
266
- if (challengeInfo.isTurnstile) {
267
- try {
268
- if (forceDebug) console.log(`[debug][cloudflare] Attempting Turnstile method for ${currentUrl}`);
269
-
270
- const turnstileResult = await handleTurnstileChallenge(page, forceDebug);
271
- if (turnstileResult.success) {
272
- result.success = true;
273
- result.method = 'turnstile';
274
- if (forceDebug) console.log(`[debug][cloudflare] Turnstile challenge solved successfully for ${currentUrl}`);
275
- return result;
276
- } else {
277
- if (forceDebug) console.log(`[debug][cloudflare] Turnstile method failed: ${turnstileResult.error}`);
278
- }
279
- } catch (turnstileError) {
280
- if (forceDebug) console.log(`[debug][cloudflare] Turnstile method error: ${turnstileError.message}`);
281
- }
290
+ try {
291
+ // Overall timeout for all challenge solving attempts
292
+ return await Promise.race([
293
+ attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug),
294
+ new Promise((_, reject) =>
295
+ setTimeout(() => reject(new Error('Challenge solving timeout')), 30000)
296
+ )
297
+ ]);
298
+ } catch (error) {
299
+ result.error = `Challenge solving timed out: ${error.message}`;
300
+ if (forceDebug) console.log(`[debug][cloudflare] Challenge solving timeout for ${currentUrl}`);
301
+ return result;
282
302
  }
303
+ }
304
+
305
+ /**
306
+ * Attempts to solve a Cloudflare challenge with modern techniques
307
+ */
308
+ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug = false) {
309
+ const result = {
310
+ success: false,
311
+ error: null,
312
+ method: null
313
+ };
283
314
 
284
- // Method 2: Handle JS challenges (wait for automatic completion)
315
+ // Method 1: Handle JS challenges (wait for automatic completion) - Most reliable
285
316
  if (challengeInfo.isJSChallenge) {
286
317
  try {
287
318
  if (forceDebug) console.log(`[debug][cloudflare] Attempting JS challenge wait for ${currentUrl}`);
@@ -298,6 +329,23 @@ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug
298
329
  }
299
330
  }
300
331
 
332
+ // Method 2: Handle Turnstile challenges (interactive)
333
+ if (challengeInfo.isTurnstile) {
334
+ try {
335
+ if (forceDebug) console.log(`[debug][cloudflare] Attempting Turnstile method for ${currentUrl}`);
336
+
337
+ const turnstileResult = await handleTurnstileChallenge(page, forceDebug);
338
+ if (turnstileResult.success) {
339
+ result.success = true;
340
+ result.method = 'turnstile';
341
+ if (forceDebug) console.log(`[debug][cloudflare] Turnstile challenge solved successfully for ${currentUrl}`);
342
+ return result;
343
+ }
344
+ } catch (turnstileError) {
345
+ if (forceDebug) console.log(`[debug][cloudflare] Turnstile method error: ${turnstileError.message}`);
346
+ }
347
+ }
348
+
301
349
  // Method 3: Legacy checkbox interaction (fallback)
302
350
  if (challengeInfo.hasLegacyCheckbox) {
303
351
  try {
@@ -315,52 +363,51 @@ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug
315
363
  }
316
364
  }
317
365
 
318
- // Method 4: Alternative element clicking (final fallback)
366
+ if (!result.success) {
367
+ result.error = result.error || 'All challenge bypass methods failed';
368
+ }
369
+
370
+ return result;
371
+ }
372
+
373
+ /**
374
+ * Waits for JS challenge completion with timeout protection
375
+ */
376
+ async function waitForJSChallengeCompletion(page, forceDebug = false) {
377
+ const result = {
378
+ success: false,
379
+ error: null
380
+ };
381
+
319
382
  try {
320
- if (forceDebug) console.log(`[debug][cloudflare] Trying alternative click method for ${currentUrl}`);
383
+ if (forceDebug) console.log(`[debug][cloudflare] Waiting for JS challenge completion`);
321
384
 
322
- const alternatives = [
323
- '.cf-challenge-running',
324
- '[data-ray]',
325
- '.challenge-stage',
326
- '.challenge-form'
327
- ];
385
+ // Wait for challenge to complete automatically with shorter timeout
386
+ await Promise.race([
387
+ page.waitForFunction(
388
+ () => {
389
+ return !document.body.textContent.includes('Checking your browser') &&
390
+ !document.body.textContent.includes('Please wait while we verify') &&
391
+ !document.querySelector('.cf-challenge-running') &&
392
+ !document.querySelector('[data-cf-challenge]');
393
+ },
394
+ { timeout: 20000 }
395
+ ),
396
+ new Promise((_, reject) =>
397
+ setTimeout(() => reject(new Error('JS challenge timeout')), 25000)
398
+ )
399
+ ]);
328
400
 
329
- for (const selector of alternatives) {
330
- try {
331
- await page.click(selector, { timeout: 3000 });
332
- await waitForTimeout(page, 3000);
333
-
334
- // Check if challenge is solved
335
- const completionCheck = await checkChallengeCompletion(page);
336
- if (completionCheck.isCompleted) {
337
- result.success = true;
338
- result.method = 'alternative_click';
339
- if (forceDebug) console.log(`[debug][cloudflare] Alternative click method succeeded for ${currentUrl} using ${selector}`);
340
- return result;
341
- }
342
- } catch (clickError) {
343
- // Continue to next selector
344
- continue;
345
- }
346
- }
347
- } catch (altError) {
348
- result.error = `All bypass methods failed. Last error: ${altError.message}`;
349
- if (forceDebug) console.log(`[debug][cloudflare] All bypass methods failed for ${currentUrl}: ${altError.message}`);
350
- }
351
-
352
- if (!result.success) {
353
- result.error = result.error || 'All challenge bypass methods failed';
401
+ result.success = true;
402
+ } catch (error) {
403
+ result.error = `JS challenge timeout: ${error.message}`;
354
404
  }
355
405
 
356
406
  return result;
357
407
  }
358
408
 
359
409
  /**
360
- * Handles modern Turnstile challenges - New for 2025
361
- * @param {import('puppeteer').Page} page - Puppeteer page instance
362
- * @param {boolean} forceDebug - Debug mode flag
363
- * @returns {Promise<object>} Turnstile handling result
410
+ * Handles modern Turnstile challenges with timeout protection
364
411
  */
365
412
  async function handleTurnstileChallenge(page, forceDebug = false) {
366
413
  const result = {
@@ -369,7 +416,9 @@ async function handleTurnstileChallenge(page, forceDebug = false) {
369
416
  };
370
417
 
371
418
  try {
372
- // Wait for Turnstile iframe to load
419
+ // Much shorter timeout for Turnstile operations
420
+ const turnstileTimeout = 10000; // 10 seconds
421
+
373
422
  const turnstileSelectors = [
374
423
  'iframe[src*="challenges.cloudflare.com"]',
375
424
  'iframe[title*="Widget containing a Cloudflare"]',
@@ -379,7 +428,11 @@ async function handleTurnstileChallenge(page, forceDebug = false) {
379
428
  let turnstileFrame = null;
380
429
  for (const selector of turnstileSelectors) {
381
430
  try {
382
- await page.waitForSelector(selector, { timeout: 5000 });
431
+ await Promise.race([
432
+ page.waitForSelector(selector, { timeout: 3000 }),
433
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Selector timeout')), 4000))
434
+ ]);
435
+
383
436
  const frames = await page.frames();
384
437
  turnstileFrame = frames.find(frame =>
385
438
  frame.url().includes('challenges.cloudflare.com') ||
@@ -394,7 +447,6 @@ async function handleTurnstileChallenge(page, forceDebug = false) {
394
447
  if (turnstileFrame) {
395
448
  if (forceDebug) console.log(`[debug][cloudflare] Found Turnstile iframe`);
396
449
 
397
- // Wait for checkbox in iframe
398
450
  const checkboxSelectors = [
399
451
  'input[type="checkbox"].ctp-checkbox',
400
452
  'input[type="checkbox"]',
@@ -404,10 +456,12 @@ async function handleTurnstileChallenge(page, forceDebug = false) {
404
456
 
405
457
  for (const selector of checkboxSelectors) {
406
458
  try {
407
- await turnstileFrame.waitForSelector(selector, { timeout: 5000 });
459
+ await Promise.race([
460
+ turnstileFrame.waitForSelector(selector, { timeout: 3000 }),
461
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Checkbox timeout')), 4000))
462
+ ]);
408
463
 
409
- // Simulate human-like interaction
410
- await waitForTimeout(page, Math.random() * 1000 + 500);
464
+ await waitForTimeout(page, 500);
411
465
  await turnstileFrame.click(selector);
412
466
 
413
467
  if (forceDebug) console.log(`[debug][cloudflare] Clicked Turnstile checkbox: ${selector}`);
@@ -417,48 +471,21 @@ async function handleTurnstileChallenge(page, forceDebug = false) {
417
471
  }
418
472
  }
419
473
 
420
- // Wait for Turnstile completion
421
- await page.waitForFunction(
422
- () => {
423
- const responseInput = document.querySelector('input[name="cf-turnstile-response"]');
424
- return responseInput && responseInput.value && responseInput.value.length > 0;
425
- },
426
- { timeout: 30000 }
427
- );
474
+ // Wait for Turnstile completion with timeout
475
+ await Promise.race([
476
+ page.waitForFunction(
477
+ () => {
478
+ const responseInput = document.querySelector('input[name="cf-turnstile-response"]');
479
+ return responseInput && responseInput.value && responseInput.value.length > 0;
480
+ },
481
+ { timeout: 15000 }
482
+ ),
483
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Turnstile completion timeout')), 20000))
484
+ ]);
428
485
 
429
486
  result.success = true;
430
487
  } else {
431
- // Try container-based Turnstile (non-iframe)
432
- const containerSelectors = [
433
- '.cf-turnstile',
434
- '.ctp-checkbox-container',
435
- '.ctp-checkbox-label'
436
- ];
437
-
438
- for (const selector of containerSelectors) {
439
- try {
440
- await page.waitForSelector(selector, { timeout: 5000 });
441
-
442
- // Human-like interaction
443
- await waitForTimeout(page, Math.random() * 1000 + 500);
444
- await page.click(selector);
445
-
446
- if (forceDebug) console.log(`[debug][cloudflare] Clicked Turnstile container: ${selector}`);
447
-
448
- // Wait for completion
449
- const completionCheck = await checkChallengeCompletion(page);
450
- if (completionCheck.isCompleted) {
451
- result.success = true;
452
- break;
453
- }
454
- } catch (e) {
455
- continue;
456
- }
457
- }
458
-
459
- if (!result.success) {
460
- result.error = 'Turnstile iframe/container not found or not interactive';
461
- }
488
+ result.error = 'Turnstile iframe not found';
462
489
  }
463
490
 
464
491
  } catch (error) {
@@ -469,44 +496,7 @@ async function handleTurnstileChallenge(page, forceDebug = false) {
469
496
  }
470
497
 
471
498
  /**
472
- * Waits for JS challenge completion - New for 2025
473
- * @param {import('puppeteer').Page} page - Puppeteer page instance
474
- * @param {boolean} forceDebug - Debug mode flag
475
- * @returns {Promise<object>} JS challenge result
476
- */
477
- async function waitForJSChallengeCompletion(page, forceDebug = false) {
478
- const result = {
479
- success: false,
480
- error: null
481
- };
482
-
483
- try {
484
- if (forceDebug) console.log(`[debug][cloudflare] Waiting for JS challenge completion`);
485
-
486
- // Wait for challenge to complete automatically
487
- await page.waitForFunction(
488
- () => {
489
- return !document.body.textContent.includes('Checking your browser') &&
490
- !document.body.textContent.includes('Please wait while we verify') &&
491
- !document.querySelector('.cf-challenge-running') &&
492
- !document.querySelector('[data-cf-challenge]');
493
- },
494
- { timeout: 30000 }
495
- );
496
-
497
- result.success = true;
498
- } catch (error) {
499
- result.error = `JS challenge timeout: ${error.message}`;
500
- }
501
-
502
- return result;
503
- }
504
-
505
- /**
506
- * Handles legacy checkbox challenges - Fallback for older implementations
507
- * @param {import('puppeteer').Page} page - Puppeteer page instance
508
- * @param {boolean} forceDebug - Debug mode flag
509
- * @returns {Promise<object>} Legacy challenge result
499
+ * Handles legacy checkbox challenges with timeout protection
510
500
  */
511
501
  async function handleLegacyCheckbox(page, forceDebug = false) {
512
502
  const result = {
@@ -523,27 +513,20 @@ async function handleLegacyCheckbox(page, forceDebug = false) {
523
513
 
524
514
  for (const selector of legacySelectors) {
525
515
  try {
526
- await page.waitForSelector(selector, { timeout: 5000 });
516
+ await Promise.race([
517
+ page.waitForSelector(selector, { timeout: 3000 }),
518
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Legacy selector timeout')), 4000))
519
+ ]);
527
520
 
528
521
  const checkbox = await page.$(selector);
529
522
  if (checkbox) {
530
- const box = await checkbox.boundingBox();
531
- if (box) {
532
- // Simulate human-like mouse movement
533
- await page.mouse.move(box.x - 50, box.y - 50);
534
- await waitForTimeout(page, Math.random() * 500 + 200);
535
- await page.mouse.move(box.x + box.width/2, box.y + box.height/2, { steps: 5 });
536
- await waitForTimeout(page, Math.random() * 300 + 100);
537
-
538
- await checkbox.click();
539
- if (forceDebug) console.log(`[debug][cloudflare] Clicked legacy checkbox: ${selector}`);
540
-
541
- // Wait for challenge completion
542
- const completionCheck = await checkChallengeCompletion(page);
543
- if (completionCheck.isCompleted) {
544
- result.success = true;
545
- break;
546
- }
523
+ await checkbox.click();
524
+ if (forceDebug) console.log(`[debug][cloudflare] Clicked legacy checkbox: ${selector}`);
525
+
526
+ const completionCheck = await checkChallengeCompletion(page);
527
+ if (completionCheck.isCompleted) {
528
+ result.success = true;
529
+ break;
547
530
  }
548
531
  }
549
532
  } catch (e) {
@@ -563,28 +546,24 @@ async function handleLegacyCheckbox(page, forceDebug = false) {
563
546
  }
564
547
 
565
548
  /**
566
- * Checks if challenge has been completed - New utility function
567
- * @param {import('puppeteer').Page} page - Puppeteer page instance
568
- * @returns {Promise<object>} Completion status
549
+ * Checks if challenge has been completed with timeout protection
569
550
  */
570
551
  async function checkChallengeCompletion(page) {
571
552
  try {
572
- const isCompleted = await page.evaluate(() => {
573
- // Check for absence of challenge indicators
553
+ const isCompleted = await safePageEvaluate(page, () => {
574
554
  const noChallengeRunning = !document.querySelector('.cf-challenge-running');
575
555
  const noChallengeContainer = !document.querySelector('.cf-challenge-container');
576
556
  const noChallengePage = !document.body.textContent.includes('Checking your browser') &&
577
557
  !document.body.textContent.includes('Just a moment') &&
578
558
  !document.body.textContent.includes('Verify you are human');
579
559
 
580
- // Check for completion indicators
581
560
  const hasClearanceCookie = document.cookie.includes('cf_clearance');
582
561
  const hasTurnstileResponse = document.querySelector('input[name="cf-turnstile-response"]')?.value;
583
562
 
584
563
  return (noChallengeRunning && noChallengeContainer && noChallengePage) ||
585
564
  hasClearanceCookie ||
586
565
  hasTurnstileResponse;
587
- });
566
+ }, 5000);
588
567
 
589
568
  return { isCompleted };
590
569
  } catch (error) {
@@ -593,12 +572,7 @@ async function checkChallengeCompletion(page) {
593
572
  }
594
573
 
595
574
  /**
596
- * Main function to handle all Cloudflare challenges for a given page - Updated for 2025
597
- * @param {import('puppeteer').Page} page - Puppeteer page instance
598
- * @param {string} currentUrl - Current URL being processed
599
- * @param {object} siteConfig - Site configuration object
600
- * @param {boolean} forceDebug - Debug mode flag
601
- * @returns {Promise<object>} Combined result of all Cloudflare handling
575
+ * Main function to handle all Cloudflare challenges with comprehensive timeout protection
602
576
  */
603
577
  async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDebug = false) {
604
578
  const result = {
@@ -608,6 +582,40 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
608
582
  errors: []
609
583
  };
610
584
 
585
+ try {
586
+ // Overall timeout for all Cloudflare operations
587
+ return await Promise.race([
588
+ performCloudflareHandling(page, currentUrl, siteConfig, forceDebug),
589
+ new Promise((resolve) => {
590
+ setTimeout(() => {
591
+ console.warn(`[cloudflare] Overall timeout for ${currentUrl} - continuing with scan`);
592
+ resolve({
593
+ phishingWarning: { attempted: false, success: true },
594
+ verificationChallenge: { attempted: false, success: true },
595
+ overallSuccess: true,
596
+ errors: ['Cloudflare handling timed out']
597
+ });
598
+ }, 45000); // 45 second overall timeout
599
+ })
600
+ ]);
601
+ } catch (error) {
602
+ result.overallSuccess = false;
603
+ result.errors.push(`Cloudflare handling failed: ${error.message}`);
604
+ return result;
605
+ }
606
+ }
607
+
608
+ /**
609
+ * Performs the actual Cloudflare handling
610
+ */
611
+ async function performCloudflareHandling(page, currentUrl, siteConfig, forceDebug = false) {
612
+ const result = {
613
+ phishingWarning: { attempted: false, success: false },
614
+ verificationChallenge: { attempted: false, success: false },
615
+ overallSuccess: true,
616
+ errors: []
617
+ };
618
+
611
619
  // Handle phishing warnings if enabled
612
620
  if (siteConfig.cloudflare_phish === true) {
613
621
  const phishingResult = await handlePhishingWarning(page, currentUrl, forceDebug);
@@ -634,7 +642,6 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
634
642
  }
635
643
  }
636
644
 
637
- // Log overall result
638
645
  if (!result.overallSuccess && forceDebug) {
639
646
  console.log(`[debug][cloudflare] Overall Cloudflare handling failed for ${currentUrl}:`);
640
647
  result.errors.forEach(error => {
@@ -657,4 +664,4 @@ module.exports = {
657
664
  waitForJSChallengeCompletion,
658
665
  handleLegacyCheckbox,
659
666
  checkChallengeCompletion
660
- };
667
+ };
package/nwss.js CHANGED
@@ -1,4 +1,4 @@
1
- // === Network scanner script (nwss.js) v1.0.35 ===
1
+ // === Network scanner script (nwss.js) v1.0.37 ===
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
@@ -33,7 +33,7 @@ const { navigateWithRedirectHandling, handleRedirectTimeout } = require('./lib/r
33
33
  const { monitorBrowserHealth, isBrowserHealthy } = require('./lib/browserhealth');
34
34
 
35
35
  // --- Script Configuration & Constants ---
36
- const VERSION = '1.0.35'; // Script version
36
+ const VERSION = '1.0.37'; // Script version
37
37
  const MAX_CONCURRENT_SITES = 5;
38
38
  const RESOURCE_CLEANUP_INTERVAL = 80; // Close browser and restart every N sites to free resources
39
39
 
@@ -936,7 +936,7 @@ function setupFrameHandling(page, forceDebug) {
936
936
  '--disable-image-animation-resync'
937
937
  ],
938
938
  headless: launchHeadless ? 'shell' : false,
939
- protocolTimeout: 500000
939
+ protocolTimeout: 60000 // 60 seconds
940
940
  });
941
941
 
942
942
  // Store the user data directory on the browser object for cleanup
@@ -2091,7 +2091,21 @@ function setupFrameHandling(page, forceDebug) {
2091
2091
  }
2092
2092
 
2093
2093
  } catch (err) {
2094
- // Enhanced error handling with rule preservation for partial matches
2094
+ // Enhanced error handling with rule preservation for partial matches
2095
+ if (err.message.includes('Runtime.callFunctionOn timed out') ||
2096
+ err.message.includes('Protocol error') ||
2097
+ err.message.includes('Target closed') ||
2098
+ err.message.includes('Browser has been closed')) {
2099
+ console.error(formatLogMessage('error', `Critical browser protocol error on ${currentUrl}: ${err.message}`));
2100
+ return {
2101
+ url: currentUrl,
2102
+ rules: [],
2103
+ success: false,
2104
+ needsImmediateRestart: true,
2105
+ error: `Critical protocol error: ${err.message}`
2106
+ };
2107
+ }
2108
+
2095
2109
  if (err.message.includes('Protocol error') ||
2096
2110
  err.message.includes('Target closed') ||
2097
2111
  err.message.includes('Browser process was killed') ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanboynz/network-scanner",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
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": {