@fanboynz/network-scanner 2.0.55 → 2.0.56
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.
- package/lib/cloudflare.js +117 -65
- package/nwss.js +46 -12
- package/package.json +1 -1
package/lib/cloudflare.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cloudflare bypass and challenge handling module - Optimized with smart detection and adaptive timeouts
|
|
3
|
+
* Version: 2.7.0 - Major fixes and performance overhaul
|
|
4
|
+
* - Fix: Challenge solvers had empty if-blocks (JS/Turnstile/Legacy never executed in non-debug mode)
|
|
5
|
+
* - Fix: a[href*="continue"] false positive removed (matched nearly every website)
|
|
6
|
+
* - Perf: Domain-level detection cache (was per-URL, now per-hostname)
|
|
7
|
+
* - Perf: Timeout outcome caching (domain times out once -> all subsequent URLs skip instantly)
|
|
8
|
+
* - Perf: Short-circuit quick detection (title/URL -> fast selectors -> slow text, early return at each stage)
|
|
9
|
+
* - Perf: Eliminated body.textContent in quick detection (was extracting entire DOM text tree)
|
|
10
|
+
* - Perf: Capped body.textContent to 2KB in analyzeCloudflareChallenge
|
|
11
|
+
* - Perf: No-indicator pages skip immediately regardless of config (was 10-15s wasted)
|
|
12
|
+
* - Perf: Quick detection timeout 4s->2s, retries 2->1
|
|
13
|
+
* - Perf: PAGE_EVALUATION timeout 12s->5s, detached frame delay 3s->1s
|
|
14
|
+
* - Perf: Inner timeouts tightened to fit within outer adaptive timeouts
|
|
15
|
+
* - Perf: CHALLENGE_SOLVING 30s->12s, TURNSTILE_COMPLETION 20s->10s, JS_CHALLENGE_BUFFER 26s->12s
|
|
16
|
+
* - Perf: MAX_RETRIES 3->2, baseDelay 1000->800ms, maxDelay 8000->5000ms
|
|
17
|
+
* - Perf: Parallel detection gated behind cloudflare config (was running on every URL)
|
|
3
18
|
* Version: 2.6.3 - Fixes Cannot read properties of undefined (reading 'hasIndicators')
|
|
4
19
|
* Version: 2.6.2 - Further detached Frame fixes
|
|
5
20
|
* Version: 2.6.1 - timeoutId is not defined & race condition fix
|
|
@@ -20,7 +35,7 @@ const { formatLogMessage } = require('./colorize');
|
|
|
20
35
|
/**
|
|
21
36
|
* Module version information
|
|
22
37
|
*/
|
|
23
|
-
const CLOUDFLARE_MODULE_VERSION = '2.
|
|
38
|
+
const CLOUDFLARE_MODULE_VERSION = '2.7.0';
|
|
24
39
|
|
|
25
40
|
/**
|
|
26
41
|
* Timeout constants for various operations (in milliseconds)
|
|
@@ -28,13 +43,13 @@ const CLOUDFLARE_MODULE_VERSION = '2.6.3';
|
|
|
28
43
|
* All values tuned for maximum scanning speed while maintaining functionality
|
|
29
44
|
*/
|
|
30
45
|
const TIMEOUTS = {
|
|
31
|
-
PAGE_EVALUATION:
|
|
32
|
-
PAGE_EVALUATION_SAFE:
|
|
46
|
+
PAGE_EVALUATION: 5000, // Standard page evaluation timeout (DOM queries are instant)
|
|
47
|
+
PAGE_EVALUATION_SAFE: 5000, // Safe page evaluation with extra buffer
|
|
33
48
|
PHISHING_CLICK: 3000, // Timeout for clicking phishing continue button
|
|
34
49
|
PHISHING_NAVIGATION: 8000, // Wait for navigation after phishing bypass
|
|
35
|
-
JS_CHALLENGE_BUFFER:
|
|
36
|
-
TURNSTILE_COMPLETION:
|
|
37
|
-
TURNSTILE_COMPLETION_BUFFER:
|
|
50
|
+
JS_CHALLENGE_BUFFER: 12000, // JS challenge -- must fit within 15s adaptive outer timeout
|
|
51
|
+
TURNSTILE_COMPLETION: 10000, // Turnstile completion check -- fits within adaptive timeout
|
|
52
|
+
TURNSTILE_COMPLETION_BUFFER: 12000, // Turnstile completion with buffer
|
|
38
53
|
CLICK_TIMEOUT: 5000, // Standard click operation timeout
|
|
39
54
|
CLICK_TIMEOUT_BUFFER: 1000, // Click timeout safety buffer
|
|
40
55
|
NAVIGATION_TIMEOUT: 15000, // Standard navigation timeout
|
|
@@ -45,21 +60,21 @@ const TIMEOUTS = {
|
|
|
45
60
|
ADAPTIVE_TIMEOUT_AUTO_WITHOUT_INDICATORS: 10000, // Adaptive timeout for auto-detected without indicators
|
|
46
61
|
// New timeouts for enhanced functionality
|
|
47
62
|
RETRY_DELAY: 1000, // Delay between retry attempts
|
|
48
|
-
MAX_RETRIES:
|
|
63
|
+
MAX_RETRIES: 2, // Maximum retry attempts (only 2 fit within 25s outer timeout)
|
|
49
64
|
CHALLENGE_POLL_INTERVAL: 500, // Interval for polling challenge completion
|
|
50
65
|
CHALLENGE_MAX_POLLS: 20 // Maximum polling attempts
|
|
51
66
|
};
|
|
52
67
|
|
|
53
68
|
// Fast timeout constants - optimized for speed
|
|
54
69
|
const FAST_TIMEOUTS = {
|
|
55
|
-
QUICK_DETECTION:
|
|
70
|
+
QUICK_DETECTION: 2000, // Fast Cloudflare detection (DOM check, instant on loaded pages)
|
|
56
71
|
PHISHING_WAIT: 1000, // Fast phishing check
|
|
57
72
|
CHALLENGE_WAIT: 500, // Fast challenge detection
|
|
58
73
|
ELEMENT_INTERACTION_DELAY: 250, // Fast element interactions
|
|
59
74
|
SELECTOR_WAIT: 3000, // Fast selector waits
|
|
60
75
|
TURNSTILE_OPERATION: 6000, // Fast Turnstile operations
|
|
61
76
|
JS_CHALLENGE: 10000, // Fast JS challenge completion
|
|
62
|
-
CHALLENGE_SOLVING:
|
|
77
|
+
CHALLENGE_SOLVING: 12000, // Overall challenge solving -- fits within 15s adaptive outer
|
|
63
78
|
CHALLENGE_COMPLETION: 8000 // Fast completion check
|
|
64
79
|
};
|
|
65
80
|
|
|
@@ -68,7 +83,7 @@ const FAST_TIMEOUTS = {
|
|
|
68
83
|
* Returns {found, clicked, x, y} - coordinates allow fallback mouse.click
|
|
69
84
|
*/
|
|
70
85
|
async function clickInShadowDOM(context, selectors, forceDebug = false, waitMs = 1500) {
|
|
71
|
-
// Try Puppeteer's pierce/ selector first
|
|
86
|
+
// Try Puppeteer's pierce/ selector first -- handles CLOSED shadow roots via CDP
|
|
72
87
|
for (const selector of selectors) {
|
|
73
88
|
try {
|
|
74
89
|
// Wait for element to appear (handles delayed rendering)
|
|
@@ -77,7 +92,7 @@ async function clickInShadowDOM(context, selectors, forceDebug = false, waitMs =
|
|
|
77
92
|
if (element) {
|
|
78
93
|
const box = await element.boundingBox();
|
|
79
94
|
if (box && box.width > 0 && box.height > 0) {
|
|
80
|
-
if (forceDebug) console.log(formatLogMessage('cloudflare', `pierce/${selector} matched in ${Date.now() - start}ms
|
|
95
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `pierce/${selector} matched in ${Date.now() - start}ms -- box: ${box.width}x${box.height} at (${box.x},${box.y})`));
|
|
81
96
|
await element.click();
|
|
82
97
|
await element.dispose();
|
|
83
98
|
return { found: true, clicked: true, selector, x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
@@ -185,9 +200,9 @@ function detectChallengeLoop(url, previousUrls = []) {
|
|
|
185
200
|
* Retry configuration with exponential backoff
|
|
186
201
|
*/
|
|
187
202
|
const RETRY_CONFIG = {
|
|
188
|
-
maxAttempts:
|
|
189
|
-
baseDelay:
|
|
190
|
-
maxDelay:
|
|
203
|
+
maxAttempts: 2, // Only 2 attempts fit within 25s outer timeout
|
|
204
|
+
baseDelay: 800, // Slightly faster retry delay
|
|
205
|
+
maxDelay: 5000, // Lower max delay cap
|
|
191
206
|
backoffMultiplier: 2,
|
|
192
207
|
retryableErrors: [ERROR_TYPES.NETWORK, ERROR_TYPES.TIMEOUT, ERROR_TYPES.ELEMENT_NOT_FOUND, ERROR_TYPES.DETACHED_FRAME]
|
|
193
208
|
};
|
|
@@ -209,7 +224,7 @@ class CloudflareDetectionCache {
|
|
|
209
224
|
getCacheKey(url) {
|
|
210
225
|
try {
|
|
211
226
|
const urlObj = new URL(url);
|
|
212
|
-
return
|
|
227
|
+
return urlObj.hostname; // Domain-level caching: all URLs from same host share one entry
|
|
213
228
|
} catch {
|
|
214
229
|
return url;
|
|
215
230
|
}
|
|
@@ -441,8 +456,8 @@ async function safePageEvaluate(page, func, timeout = TIMEOUTS.PAGE_EVALUATION_S
|
|
|
441
456
|
if (forceDebug) {
|
|
442
457
|
console.warn(formatLogMessage('cloudflare', `Detached frame detected on attempt ${attempt}/${maxRetries} - using longer delay`));
|
|
443
458
|
}
|
|
444
|
-
// For detached frames,
|
|
445
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
459
|
+
// For detached frames, brief delay before retry
|
|
460
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
446
461
|
|
|
447
462
|
// For detached frames, only retry once more
|
|
448
463
|
if (attempt >= 2) {
|
|
@@ -541,44 +556,67 @@ async function quickCloudflareDetection(page, forceDebug = false) {
|
|
|
541
556
|
// Perform actual detection with enhanced error handling
|
|
542
557
|
const quickCheck = await safePageEvaluate(page, () => {
|
|
543
558
|
const title = document.title || '';
|
|
544
|
-
const bodyText = document.body ? document.body.textContent.substring(0, 500) : '';
|
|
545
559
|
const url = window.location.href;
|
|
546
560
|
|
|
547
|
-
//
|
|
548
|
-
const
|
|
561
|
+
// FAST PATH: Check title + URL first (string ops, no DOM traversal)
|
|
562
|
+
const titleMatch =
|
|
549
563
|
title.includes('Just a moment') ||
|
|
550
564
|
title.includes('Checking your browser') ||
|
|
551
565
|
title.includes('Attention Required') ||
|
|
552
|
-
title.includes('Security check')
|
|
566
|
+
title.includes('Security check');
|
|
567
|
+
|
|
568
|
+
const urlMatch =
|
|
569
|
+
url.includes('/cdn-cgi/challenge-platform/') ||
|
|
570
|
+
url.includes('cloudflare.com');
|
|
571
|
+
|
|
572
|
+
if (titleMatch || urlMatch) {
|
|
573
|
+
return { hasIndicators: true, title, url, bodySnippet: '' };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// MEDIUM PATH: Check a few fast selectors before expensive text extraction
|
|
577
|
+
const selectorMatch =
|
|
578
|
+
document.querySelector('[data-ray]') ||
|
|
579
|
+
document.querySelector('[data-cf-challenge]') ||
|
|
580
|
+
document.querySelector('.cf-challenge-running') ||
|
|
581
|
+
document.querySelector('.cf-turnstile') ||
|
|
582
|
+
document.querySelector('.cf-managed-challenge') ||
|
|
583
|
+
document.querySelector('[data-cf-managed]') ||
|
|
584
|
+
document.querySelector('script[src*="/cdn-cgi/challenge-platform/"]');
|
|
585
|
+
|
|
586
|
+
if (selectorMatch) {
|
|
587
|
+
return { hasIndicators: true, title, url, bodySnippet: '' };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// SLOW PATH: Extract limited body text only if fast checks failed
|
|
591
|
+
// Use body.innerText capped to first child nodes instead of full textContent
|
|
592
|
+
let bodyText = '';
|
|
593
|
+
if (document.body) {
|
|
594
|
+
const el = document.body.querySelector('.main-wrapper, .main-content, #challenge-body-text, .cf-challenge-container');
|
|
595
|
+
bodyText = el ? el.textContent.substring(0, 300) : (document.body.firstElementChild ? document.body.firstElementChild.textContent.substring(0, 300) : '');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const textMatch =
|
|
553
599
|
bodyText.includes('Cloudflare') ||
|
|
554
600
|
bodyText.includes('cf-ray') ||
|
|
555
601
|
bodyText.includes('Verify you are human') ||
|
|
556
602
|
bodyText.includes('This website has been reported for potential phishing') ||
|
|
557
603
|
bodyText.includes('Please wait while we verify') ||
|
|
558
|
-
bodyText.includes('Checking if the site connection is secure')
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
document.querySelector('[data-cf-challenge]') ||
|
|
563
|
-
document.querySelector('.cf-challenge-running') ||
|
|
604
|
+
bodyText.includes('Checking if the site connection is secure');
|
|
605
|
+
|
|
606
|
+
// Remaining slower selectors
|
|
607
|
+
const slowSelectorMatch =
|
|
564
608
|
document.querySelector('.cf-challenge-container') ||
|
|
565
|
-
document.querySelector('.cf-turnstile') ||
|
|
566
609
|
document.querySelector('.ctp-checkbox-container') ||
|
|
567
610
|
document.querySelector('iframe[src*="challenges.cloudflare.com"]') ||
|
|
568
|
-
document.querySelector('iframe[title*="Cloudflare security challenge"]')
|
|
569
|
-
document.querySelector('script[src*="/cdn-cgi/challenge-platform/"]') ||
|
|
570
|
-
document.querySelector('a[href*="continue"]') ||
|
|
571
|
-
// New selectors for 2025
|
|
572
|
-
document.querySelector('.cf-managed-challenge') ||
|
|
573
|
-
document.querySelector('[data-cf-managed]');
|
|
611
|
+
document.querySelector('iframe[title*="Cloudflare security challenge"]');
|
|
574
612
|
|
|
575
613
|
return {
|
|
576
|
-
hasIndicators:
|
|
614
|
+
hasIndicators: !!(textMatch || slowSelectorMatch),
|
|
577
615
|
title,
|
|
578
616
|
url,
|
|
579
617
|
bodySnippet: bodyText.substring(0, 200)
|
|
580
618
|
};
|
|
581
|
-
}, FAST_TIMEOUTS.QUICK_DETECTION, { maxRetries:
|
|
619
|
+
}, FAST_TIMEOUTS.QUICK_DETECTION, { maxRetries: 1, forceDebug });
|
|
582
620
|
|
|
583
621
|
// Cache the result
|
|
584
622
|
detectionCache.set(currentPageUrl, quickCheck);
|
|
@@ -607,7 +645,7 @@ async function quickCloudflareDetection(page, forceDebug = false) {
|
|
|
607
645
|
*/
|
|
608
646
|
async function analyzeCloudflareChallenge(page) {
|
|
609
647
|
try {
|
|
610
|
-
// CDP-level frame check
|
|
648
|
+
// CDP-level frame check -- bypasses closed shadow roots
|
|
611
649
|
const frames = page.frames();
|
|
612
650
|
const hasChallengeFrame = frames.some(f => {
|
|
613
651
|
const url = f.url();
|
|
@@ -616,7 +654,8 @@ async function analyzeCloudflareChallenge(page) {
|
|
|
616
654
|
|
|
617
655
|
const result = await safePageEvaluate(page, () => {
|
|
618
656
|
const title = document.title || '';
|
|
619
|
-
|
|
657
|
+
// Cap text extraction -- on content-heavy pages body.textContent can be megabytes
|
|
658
|
+
const bodyText = document.body ? document.body.textContent.substring(0, 2000) : '';
|
|
620
659
|
|
|
621
660
|
// Updated selectors for 2025 Cloudflare challenges
|
|
622
661
|
const hasTurnstileIframe = document.querySelector('iframe[title*="Cloudflare security challenge"]') !== null ||
|
|
@@ -649,8 +688,7 @@ async function analyzeCloudflareChallenge(page) {
|
|
|
649
688
|
bodyText.includes('Please wait while we verify');
|
|
650
689
|
|
|
651
690
|
const hasPhishingWarning = bodyText.includes('This website has been reported for potential phishing') ||
|
|
652
|
-
title.includes('Attention Required')
|
|
653
|
-
document.querySelector('a[href*="continue"]') !== null;
|
|
691
|
+
title.includes('Attention Required');
|
|
654
692
|
|
|
655
693
|
const hasTurnstileResponse = document.querySelector('input[name="cf-turnstile-response"]') !== null;
|
|
656
694
|
|
|
@@ -687,7 +725,7 @@ async function analyzeCloudflareChallenge(page) {
|
|
|
687
725
|
};
|
|
688
726
|
}, TIMEOUTS.PAGE_EVALUATION);
|
|
689
727
|
|
|
690
|
-
// Merge CDP frame detection
|
|
728
|
+
// Merge CDP frame detection -- catches iframes behind closed shadow roots
|
|
691
729
|
if (hasChallengeFrame && !result.hasTurnstileIframe) {
|
|
692
730
|
result.hasTurnstileIframe = true;
|
|
693
731
|
result.isTurnstile = true;
|
|
@@ -1088,20 +1126,10 @@ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug
|
|
|
1088
1126
|
if (jsResult.success) {
|
|
1089
1127
|
// Wait for redirect after challenge completion
|
|
1090
1128
|
try {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
return document.title !== 'Just a moment...' ||
|
|
1096
|
-
window.location.href !== origUrl ||
|
|
1097
|
-
bodyText.includes('Verification successful');
|
|
1098
|
-
},
|
|
1099
|
-
{ timeout: 10000 },
|
|
1100
|
-
startUrl
|
|
1101
|
-
);
|
|
1102
|
-
if (forceDebug) console.log(formatLogMessage('cloudflare', `Challenge page cleared for ${currentUrl}`));
|
|
1103
|
-
} catch (_) {
|
|
1104
|
-
if (forceDebug) console.log(formatLogMessage('cloudflare', `Challenge page not cleared after 10s � continuing`));
|
|
1129
|
+
await page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
1130
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `Post-challenge redirect completed for ${currentUrl}`));
|
|
1131
|
+
} catch (navErr) {
|
|
1132
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `Post-challenge redirect timeout (may already be on target page): ${navErr.message}`));
|
|
1105
1133
|
}
|
|
1106
1134
|
result.success = true;
|
|
1107
1135
|
result.method = 'js_challenge_wait';
|
|
@@ -1183,7 +1211,7 @@ async function handleEmbeddedIframeChallenge(page, forceDebug = false) {
|
|
|
1183
1211
|
try {
|
|
1184
1212
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `Checking for embedded iframe challenges`));
|
|
1185
1213
|
|
|
1186
|
-
// Use CDP-level frame detection
|
|
1214
|
+
// Use CDP-level frame detection -- bypasses closed shadow roots
|
|
1187
1215
|
const frames = page.frames();
|
|
1188
1216
|
if (forceDebug) {
|
|
1189
1217
|
console.log(formatLogMessage('cloudflare', `Available frames (${frames.length}):`));
|
|
@@ -1624,10 +1652,11 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
|
|
|
1624
1652
|
|
|
1625
1653
|
// Early return structure when no Cloudflare indicators found
|
|
1626
1654
|
// Sets attempted: false, success: true for both protection types
|
|
1627
|
-
|
|
1628
|
-
//
|
|
1629
|
-
|
|
1630
|
-
|
|
1655
|
+
// Skip immediately if no Cloudflare indicators detected
|
|
1656
|
+
// Trust the detection -- explicit config only matters when indicators ARE found
|
|
1657
|
+
// This avoids a 10s adaptive timeout on non-Cloudflare sites
|
|
1658
|
+
if (!quickDetection.hasIndicators) {
|
|
1659
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `No Cloudflare indicators found, skipping protection handling for ${currentUrl}`));
|
|
1631
1660
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `Quick detection details: title="${quickDetection.title}", bodySnippet="${quickDetection.bodySnippet}"`));
|
|
1632
1661
|
return {
|
|
1633
1662
|
phishingWarning: { attempted: false, success: true },
|
|
@@ -1663,10 +1692,24 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
|
|
|
1663
1692
|
console.log(formatLogMessage('cloudflare', `Using adaptive timeout of ${adaptiveTimeout}ms for ${currentUrl} (indicators: ${quickDetection.hasIndicators}, explicit config: ${!!(siteConfig.cloudflare_phish || siteConfig.cloudflare_bypass)})`));
|
|
1664
1693
|
}
|
|
1665
1694
|
|
|
1666
|
-
|
|
1667
|
-
|
|
1695
|
+
// Check if this domain already timed out -- skip immediately
|
|
1696
|
+
try {
|
|
1697
|
+
const outcomeCacheKey = 'outcome:' + new URL(currentUrl).hostname;
|
|
1698
|
+
const cachedOutcome = detectionCache.cache.get(outcomeCacheKey);
|
|
1699
|
+
if (cachedOutcome && cachedOutcome.data && cachedOutcome.data.timedOut && Date.now() - cachedOutcome.timestamp < detectionCache.ttl) {
|
|
1700
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `Skipping ${currentUrl} -- domain already timed out on a previous URL`));
|
|
1701
|
+
return cachedOutcome.data;
|
|
1702
|
+
}
|
|
1703
|
+
} catch (e) { /* malformed URL, proceed normally */ }
|
|
1704
|
+
|
|
1705
|
+
let adaptiveTimeoutId = null;
|
|
1706
|
+
const handlingResult = await Promise.race([
|
|
1707
|
+
performCloudflareHandling(page, currentUrl, siteConfig, cfDebug).then(r => {
|
|
1708
|
+
if (adaptiveTimeoutId) clearTimeout(adaptiveTimeoutId);
|
|
1709
|
+
return r;
|
|
1710
|
+
}),
|
|
1668
1711
|
new Promise((resolve) => {
|
|
1669
|
-
setTimeout(() => {
|
|
1712
|
+
adaptiveTimeoutId = setTimeout(() => {
|
|
1670
1713
|
console.warn(formatLogMessage('cloudflare', `Adaptive timeout (${adaptiveTimeout}ms) for ${currentUrl} - continuing with scan`));
|
|
1671
1714
|
resolve({
|
|
1672
1715
|
phishingWarning: { attempted: false, success: true },
|
|
@@ -1678,6 +1721,16 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
|
|
|
1678
1721
|
}, adaptiveTimeout);
|
|
1679
1722
|
})
|
|
1680
1723
|
]);
|
|
1724
|
+
|
|
1725
|
+
// Cache timeout results at domain level so subsequent URLs skip immediately
|
|
1726
|
+
if (handlingResult.timedOut) {
|
|
1727
|
+
try {
|
|
1728
|
+
const setOutcomeKey = 'outcome:' + new URL(currentUrl).hostname;
|
|
1729
|
+
detectionCache.cache.set(setOutcomeKey, { data: handlingResult, timestamp: Date.now() });
|
|
1730
|
+
} catch (e) { /* malformed URL, skip caching */ }
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
return handlingResult;
|
|
1681
1734
|
} catch (error) {
|
|
1682
1735
|
result.overallSuccess = false;
|
|
1683
1736
|
result.errors.push(`Cloudflare handling failed: ${error.message}`);
|
|
@@ -1809,8 +1862,7 @@ async function parallelChallengeDetection(page, forceDebug = false) {
|
|
|
1809
1862
|
{ type: 'turnstile', detected: document.querySelector('.cf-turnstile') !== null ||
|
|
1810
1863
|
document.querySelector('iframe[src*="challenges.cloudflare.com"]') !== null ||
|
|
1811
1864
|
document.querySelector('.ctp-checkbox-container') !== null },
|
|
1812
|
-
{ type: 'phishing', detected: bodyText.includes('This website has been reported for potential phishing')
|
|
1813
|
-
document.querySelector('a[href*="continue"]') !== null },
|
|
1865
|
+
{ type: 'phishing', detected: bodyText.includes('This website has been reported for potential phishing') },
|
|
1814
1866
|
{ type: 'managed', detected: document.querySelector('.cf-managed-challenge') !== null ||
|
|
1815
1867
|
document.querySelector('[data-cf-managed]') !== null }
|
|
1816
1868
|
];
|
package/nwss.js
CHANGED
|
@@ -20,7 +20,8 @@ const {
|
|
|
20
20
|
handleCloudflareProtection,
|
|
21
21
|
getCacheStats,
|
|
22
22
|
clearDetectionCache,
|
|
23
|
-
parallelChallengeDetection
|
|
23
|
+
parallelChallengeDetection,
|
|
24
|
+
cleanup: cleanupCloudflareCache
|
|
24
25
|
} = require('./lib/cloudflare');
|
|
25
26
|
// FP Bypass
|
|
26
27
|
const { handleFlowProxyProtection, getFlowProxyTimeouts } = require('./lib/flowproxy');
|
|
@@ -84,8 +85,8 @@ const TIMEOUTS = Object.freeze({
|
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
const CACHE_LIMITS = Object.freeze({
|
|
87
|
-
DISK_CACHE_SIZE:
|
|
88
|
-
MEDIA_CACHE_SIZE:
|
|
88
|
+
DISK_CACHE_SIZE: 1, // Effectively disabled — forcereload clears cache between loads
|
|
89
|
+
MEDIA_CACHE_SIZE: 1, // Effectively disabled — no media caching needed for scanning
|
|
89
90
|
DEFAULT_CACHE_PATH: '.cache',
|
|
90
91
|
DEFAULT_MAX_SIZE: 5000
|
|
91
92
|
});
|
|
@@ -1020,11 +1021,28 @@ function flushLogBuffers() {
|
|
|
1020
1021
|
for (const [filePath, entries] of _logBuffers) {
|
|
1021
1022
|
if (entries.length > 0) {
|
|
1022
1023
|
try {
|
|
1023
|
-
|
|
1024
|
+
const data = entries.join('');
|
|
1025
|
+
entries.length = 0; // Clear buffer immediately
|
|
1026
|
+
fs.writeFile(filePath, data, { flag: 'a' }, (err) => {
|
|
1027
|
+
if (err) {
|
|
1028
|
+
console.warn(formatLogMessage('warn', `Failed to flush log buffer to ${filePath}: ${err.message}`));
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1024
1031
|
} catch (err) {
|
|
1025
1032
|
console.warn(formatLogMessage('warn', `Failed to flush log buffer to ${filePath}: ${err.message}`));
|
|
1026
1033
|
}
|
|
1027
|
-
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Synchronous flush for exit handlers — guarantees data is written before process exits
|
|
1039
|
+
function flushLogBuffersSync() {
|
|
1040
|
+
for (const [filePath, entries] of _logBuffers) {
|
|
1041
|
+
if (entries.length > 0) {
|
|
1042
|
+
try {
|
|
1043
|
+
fs.appendFileSync(filePath, entries.join(''));
|
|
1044
|
+
} catch (err) { /* best effort on exit */ }
|
|
1045
|
+
entries.length = 0;
|
|
1028
1046
|
}
|
|
1029
1047
|
}
|
|
1030
1048
|
}
|
|
@@ -1426,12 +1444,19 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1426
1444
|
'--use-mock-keychain',
|
|
1427
1445
|
'--disable-client-side-phishing-detection',
|
|
1428
1446
|
'--enable-features=NetworkService',
|
|
1429
|
-
// Disk space controls -
|
|
1430
|
-
`--disk-cache-size=${CACHE_LIMITS.DISK_CACHE_SIZE}`,
|
|
1431
|
-
`--media-cache-size=${CACHE_LIMITS.MEDIA_CACHE_SIZE}`,
|
|
1447
|
+
// Disk space controls - minimal cache for scanning workloads
|
|
1448
|
+
`--disk-cache-size=${CACHE_LIMITS.DISK_CACHE_SIZE}`,
|
|
1449
|
+
`--media-cache-size=${CACHE_LIMITS.MEDIA_CACHE_SIZE}`,
|
|
1432
1450
|
'--disable-application-cache',
|
|
1433
1451
|
'--disable-offline-load-stale-cache',
|
|
1434
1452
|
'--disable-background-downloads',
|
|
1453
|
+
// DISK I/O REDUCTION: Eliminate unnecessary Chrome disk writes
|
|
1454
|
+
'--disable-breakpad', // No crash dump files
|
|
1455
|
+
'--disable-component-update', // No component update downloads
|
|
1456
|
+
'--disable-logging', // No Chrome internal log files
|
|
1457
|
+
'--log-level=3', // Fatal errors only (suppresses verbose disk logging)
|
|
1458
|
+
'--no-service-autorun', // No background service disk activity
|
|
1459
|
+
'--disable-domain-reliability', // No reliability monitor disk writes
|
|
1435
1460
|
// PERFORMANCE: Enhanced Puppeteer 23.x optimizations
|
|
1436
1461
|
'--disable-features=AudioServiceOutOfProcess,VizDisplayCompositor',
|
|
1437
1462
|
'--disable-features=TranslateUI,BlinkGenPropertyTrees,Translate',
|
|
@@ -1466,6 +1491,12 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1466
1491
|
'--disable-background-timer-throttling',
|
|
1467
1492
|
'--disable-features=site-per-process', // Better for single-site scanning
|
|
1468
1493
|
'--no-zygote', // Better process isolation
|
|
1494
|
+
// PERFORMANCE: Process and memory reduction for high concurrency
|
|
1495
|
+
'--renderer-process-limit=10', // Cap renderer processes (default: unlimited)
|
|
1496
|
+
'--disable-accelerated-2d-canvas', // Software canvas only (we spoof it anyway)
|
|
1497
|
+
'--disable-hang-monitor', // Remove per-renderer hang check overhead
|
|
1498
|
+
'--disable-features=PaintHolding', // Don't hold frames in renderer memory
|
|
1499
|
+
'--js-flags=--max-old-space-size=512', // Cap V8 heap per renderer to 512MB
|
|
1469
1500
|
...extraArgs,
|
|
1470
1501
|
],
|
|
1471
1502
|
// Optimized timeouts for Puppeteer 23.x performance
|
|
@@ -1517,7 +1548,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1517
1548
|
// Set up cleanup on process termination
|
|
1518
1549
|
process.on('SIGINT', async () => {
|
|
1519
1550
|
if (forceDebug) console.log(formatLogMessage('debug', 'SIGINT received, performing cleanup...'));
|
|
1520
|
-
|
|
1551
|
+
flushLogBuffersSync();
|
|
1521
1552
|
if (_logFlushTimer) clearInterval(_logFlushTimer);
|
|
1522
1553
|
await performEmergencyCleanup();
|
|
1523
1554
|
process.exit(0);
|
|
@@ -1525,7 +1556,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1525
1556
|
|
|
1526
1557
|
process.on('SIGTERM', async () => {
|
|
1527
1558
|
if (forceDebug) console.log(formatLogMessage('debug', 'SIGTERM received, performing cleanup...'));
|
|
1528
|
-
|
|
1559
|
+
flushLogBuffersSync();
|
|
1529
1560
|
if (_logFlushTimer) clearInterval(_logFlushTimer);
|
|
1530
1561
|
await performEmergencyCleanup();
|
|
1531
1562
|
process.exit(0);
|
|
@@ -1556,6 +1587,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1556
1587
|
}
|
|
1557
1588
|
wgDisconnectAll(forceDebug);
|
|
1558
1589
|
ovpnDisconnectAll(forceDebug);
|
|
1590
|
+
cleanupCloudflareCache();
|
|
1559
1591
|
}
|
|
1560
1592
|
|
|
1561
1593
|
let siteCounter = 0;
|
|
@@ -3219,7 +3251,9 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3219
3251
|
siteCounter++;
|
|
3220
3252
|
|
|
3221
3253
|
// Enhanced Cloudflare handling with parallel detection
|
|
3222
|
-
|
|
3254
|
+
// Only run parallel detection if cloudflare handling is explicitly configured
|
|
3255
|
+
const hasCloudflareConfig = siteConfig.cloudflare_bypass || siteConfig.cloudflare_phish;
|
|
3256
|
+
if (hasCloudflareConfig && siteConfig.cloudflare_parallel_detection !== false) {
|
|
3223
3257
|
try {
|
|
3224
3258
|
const parallelResult = await parallelChallengeDetection(page, forceDebug);
|
|
3225
3259
|
if (parallelResult.hasAnyChallenge && forceDebug) {
|
|
@@ -4429,7 +4463,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4429
4463
|
}
|
|
4430
4464
|
|
|
4431
4465
|
// Flush any remaining buffered log entries before compression/exit
|
|
4432
|
-
|
|
4466
|
+
flushLogBuffersSync();
|
|
4433
4467
|
if (_logFlushTimer) {
|
|
4434
4468
|
clearInterval(_logFlushTimer);
|
|
4435
4469
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanboynz/network-scanner",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.56",
|
|
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": {
|