@fanboynz/network-scanner 2.0.54 → 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/browserexit.js +3 -1
- package/lib/cloudflare.js +117 -65
- package/lib/fingerprint.js +80 -30
- package/lib/interaction.js +121 -5
- package/nwss.js +73 -14
- package/package.json +1 -1
package/lib/browserexit.js
CHANGED
|
@@ -15,7 +15,7 @@ const CHROME_TEMP_PATHS = [
|
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
const CHROME_TEMP_PATTERNS = [
|
|
18
|
-
'
|
|
18
|
+
'com.google.Chrome.*', // Google Chrome temp files (no leading dot)
|
|
19
19
|
'.org.chromium.Chromium.*',
|
|
20
20
|
'puppeteer-*'
|
|
21
21
|
];
|
|
@@ -39,6 +39,7 @@ async function cleanupChromeTempFiles(options = {}) {
|
|
|
39
39
|
|
|
40
40
|
// Base cleanup commands for standard temp directories
|
|
41
41
|
const cleanupCommands = [
|
|
42
|
+
'rm -rf /tmp/com.google.Chrome.* 2>/dev/null || true',
|
|
42
43
|
'rm -rf /tmp/.com.google.Chrome.* 2>/dev/null || true',
|
|
43
44
|
'rm -rf /tmp/.org.chromium.Chromium.* 2>/dev/null || true',
|
|
44
45
|
'rm -rf /tmp/puppeteer-* 2>/dev/null || true',
|
|
@@ -48,6 +49,7 @@ async function cleanupChromeTempFiles(options = {}) {
|
|
|
48
49
|
|
|
49
50
|
// Add snap-specific cleanup if requested
|
|
50
51
|
if (includeSnapTemp || comprehensive) {
|
|
52
|
+
cleanupCommands.push('rm -rf /dev/shm/com.google.Chrome.* 2>/dev/null || true');
|
|
51
53
|
cleanupCommands.push(
|
|
52
54
|
'rm -rf /tmp/snap-private-tmp/snap.chromium/tmp/.org.chromium.Chromium.* 2>/dev/null || true',
|
|
53
55
|
'rm -rf /tmp/snap-private-tmp/snap.chromium/tmp/puppeteer-* 2>/dev/null || true'
|
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/lib/fingerprint.js
CHANGED
|
@@ -24,28 +24,6 @@ function seededRandom(seed) {
|
|
|
24
24
|
const _fingerprintCache = new Map();
|
|
25
25
|
|
|
26
26
|
// Type-specific property spoofing functions for monomorphic optimization
|
|
27
|
-
function spoofNavigatorProperties(navigator, properties, options = {}) {
|
|
28
|
-
if (!navigator || typeof navigator !== 'object') return false;
|
|
29
|
-
|
|
30
|
-
for (const [prop, descriptor] of Object.entries(properties)) {
|
|
31
|
-
if (!safeDefineProperty(navigator, prop, descriptor, options)) {
|
|
32
|
-
if (options.debug) console.log(`[fingerprint] Failed to spoof navigator.${prop}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function spoofScreenProperties(screen, properties, options = {}) {
|
|
39
|
-
if (!screen || typeof screen !== 'object') return false;
|
|
40
|
-
|
|
41
|
-
for (const [prop, descriptor] of Object.entries(properties)) {
|
|
42
|
-
if (!safeDefineProperty(screen, prop, descriptor, options)) {
|
|
43
|
-
if (options.debug) console.log(`[fingerprint] Failed to spoof screen.${prop}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
27
|
// Built-in properties that should not be modified
|
|
50
28
|
const BUILT_IN_PROPERTIES = new Set([
|
|
51
29
|
'href', 'origin', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash',
|
|
@@ -379,6 +357,27 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
379
357
|
return false;
|
|
380
358
|
};
|
|
381
359
|
})();
|
|
360
|
+
|
|
361
|
+
// Function.prototype.toString protection — make spoofed functions appear native
|
|
362
|
+
// Must be installed BEFORE any property overrides so all spoofs are protected
|
|
363
|
+
const nativeFunctionStore = new WeakMap();
|
|
364
|
+
const originalToString = Function.prototype.toString;
|
|
365
|
+
|
|
366
|
+
function maskAsNative(fn, nativeName) {
|
|
367
|
+
if (typeof fn === 'function') {
|
|
368
|
+
nativeFunctionStore.set(fn, nativeName || fn.name || '');
|
|
369
|
+
}
|
|
370
|
+
return fn;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
Function.prototype.toString = function() {
|
|
374
|
+
if (nativeFunctionStore.has(this)) {
|
|
375
|
+
return `function ${nativeFunctionStore.get(this)}() { [native code] }`;
|
|
376
|
+
}
|
|
377
|
+
return originalToString.call(this);
|
|
378
|
+
};
|
|
379
|
+
// Protect the toString override itself
|
|
380
|
+
nativeFunctionStore.set(Function.prototype.toString, 'toString');
|
|
382
381
|
|
|
383
382
|
// Create safe property definition helper
|
|
384
383
|
function safeDefinePropertyLocal(target, property, descriptor) {
|
|
@@ -439,10 +438,14 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
439
438
|
// Remove webdriver properties
|
|
440
439
|
//
|
|
441
440
|
safeExecute(() => {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
} catch (e) {}
|
|
445
|
-
|
|
441
|
+
// In real Chrome, webdriver lives on Navigator.prototype, not the instance.
|
|
442
|
+
// Override it there so Object.getOwnPropertyDescriptor(navigator, 'webdriver') returns undefined.
|
|
443
|
+
try { delete navigator.webdriver; } catch (e) {}
|
|
444
|
+
Object.defineProperty(Navigator.prototype, 'webdriver', {
|
|
445
|
+
get: () => false,
|
|
446
|
+
configurable: true,
|
|
447
|
+
enumerable: true
|
|
448
|
+
});
|
|
446
449
|
}, 'webdriver removal');
|
|
447
450
|
|
|
448
451
|
// Remove automation properties
|
|
@@ -861,6 +864,8 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
861
864
|
if (typeof originalStack === 'string') {
|
|
862
865
|
return originalStack
|
|
863
866
|
.replace(/.*puppeteer.*\n?/gi, '')
|
|
867
|
+
.replace(/.*__puppeteer_evaluation_script__.*\n?/gi, '')
|
|
868
|
+
.replace(/.*evaluateOnNewDocument.*\n?/gi, '')
|
|
864
869
|
.replace(/.*chrome-devtools.*\n?/gi, '')
|
|
865
870
|
.replace(/.*webdriver.*\n?/gi, '')
|
|
866
871
|
.replace(/.*automation.*\n?/gi, '')
|
|
@@ -1635,6 +1640,55 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1635
1640
|
});
|
|
1636
1641
|
}, 'location URL masking');
|
|
1637
1642
|
|
|
1643
|
+
// Bulk-mask all spoofed prototype methods so toString() returns "[native code]"
|
|
1644
|
+
// Must run AFTER all overrides are applied
|
|
1645
|
+
safeExecute(() => {
|
|
1646
|
+
const protoMasks = [
|
|
1647
|
+
[WebGLRenderingContext.prototype, ['getParameter', 'getExtension', 'getSupportedExtensions']],
|
|
1648
|
+
[HTMLCanvasElement.prototype, ['getContext', 'toDataURL', 'toBlob']],
|
|
1649
|
+
[CanvasRenderingContext2D.prototype, ['getImageData', 'fillText', 'strokeText', 'measureText']],
|
|
1650
|
+
[EventTarget.prototype, ['addEventListener', 'removeEventListener']],
|
|
1651
|
+
[Date.prototype, ['getTimezoneOffset']],
|
|
1652
|
+
];
|
|
1653
|
+
if (typeof WebGL2RenderingContext !== 'undefined') {
|
|
1654
|
+
protoMasks.push([WebGL2RenderingContext.prototype, ['getParameter', 'getExtension']]);
|
|
1655
|
+
}
|
|
1656
|
+
protoMasks.forEach(([proto, methods]) => {
|
|
1657
|
+
methods.forEach(name => {
|
|
1658
|
+
if (typeof proto[name] === 'function') maskAsNative(proto[name], name);
|
|
1659
|
+
});
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
// Mask navigator/window method overrides
|
|
1663
|
+
if (typeof navigator.permissions?.query === 'function') maskAsNative(navigator.permissions.query, 'query');
|
|
1664
|
+
if (typeof navigator.getBattery === 'function') maskAsNative(navigator.getBattery, 'getBattery');
|
|
1665
|
+
if (typeof speechSynthesis?.getVoices === 'function') maskAsNative(speechSynthesis.getVoices, 'getVoices');
|
|
1666
|
+
if (typeof performance.now === 'function') maskAsNative(performance.now, 'now');
|
|
1667
|
+
if (typeof Notification?.requestPermission === 'function') maskAsNative(Notification.requestPermission, 'requestPermission');
|
|
1668
|
+
if (typeof window.RTCPeerConnection === 'function') maskAsNative(window.RTCPeerConnection, 'RTCPeerConnection');
|
|
1669
|
+
if (typeof window.Image === 'function') maskAsNative(window.Image, 'Image');
|
|
1670
|
+
if (typeof window.fetch === 'function') maskAsNative(window.fetch, 'fetch');
|
|
1671
|
+
if (typeof window.PointerEvent === 'function') maskAsNative(window.PointerEvent, 'PointerEvent');
|
|
1672
|
+
|
|
1673
|
+
// Mask property getters on navigator
|
|
1674
|
+
const navProps = ['userAgentData', 'connection', 'pdfViewerEnabled', 'webdriver',
|
|
1675
|
+
'hardwareConcurrency', 'deviceMemory', 'platform', 'maxTouchPoints'];
|
|
1676
|
+
navProps.forEach(prop => {
|
|
1677
|
+
// Check both instance and prototype (webdriver lives on prototype)
|
|
1678
|
+
const desc = Object.getOwnPropertyDescriptor(navigator, prop)
|
|
1679
|
+
|| Object.getOwnPropertyDescriptor(Navigator.prototype, prop);
|
|
1680
|
+
if (desc?.get) maskAsNative(desc.get, 'get ' + prop);
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
// Mask window property getters
|
|
1684
|
+
['screenX', 'screenY', 'outerWidth', 'outerHeight'].forEach(prop => {
|
|
1685
|
+
const desc = Object.getOwnPropertyDescriptor(window, prop);
|
|
1686
|
+
if (desc?.get) maskAsNative(desc.get, 'get ' + prop);
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
if (debugEnabled) console.log('[fingerprint] toString protection applied to all spoofed functions');
|
|
1690
|
+
}, 'Function.prototype.toString bulk masking');
|
|
1691
|
+
|
|
1638
1692
|
}, ua, forceDebug, selectedGpu);
|
|
1639
1693
|
} catch (stealthErr) {
|
|
1640
1694
|
if (stealthErr.message.includes('Session closed') ||
|
|
@@ -2030,9 +2084,6 @@ async function applyAllFingerprintSpoofing(page, siteConfig, forceDebug, current
|
|
|
2030
2084
|
}
|
|
2031
2085
|
|
|
2032
2086
|
// Legacy compatibility function - maintained for backwards compatibility
|
|
2033
|
-
function safeExecuteSpoofing(spoofFunction, description, forceDebug = false) {
|
|
2034
|
-
return safeSpoofingExecution(spoofFunction, description, { debug: forceDebug });
|
|
2035
|
-
}
|
|
2036
2087
|
|
|
2037
2088
|
|
|
2038
2089
|
module.exports = {
|
|
@@ -2044,7 +2095,6 @@ module.exports = {
|
|
|
2044
2095
|
applyAllFingerprintSpoofing,
|
|
2045
2096
|
simulateHumanBehavior,
|
|
2046
2097
|
safeDefineProperty,
|
|
2047
|
-
safeExecuteSpoofing, // Legacy compatibility
|
|
2048
2098
|
safeSpoofingExecution,
|
|
2049
2099
|
DEFAULT_PLATFORM,
|
|
2050
2100
|
DEFAULT_TIMEZONE
|
package/lib/interaction.js
CHANGED
|
@@ -123,6 +123,20 @@ const ELEMENT_INTERACTION = {
|
|
|
123
123
|
MISTAKE_RATE: 0.02 // Probability of typing mistakes (0.02 = 2% chance)
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
+
// === CONTENT CLICK CONSTANTS ===
|
|
127
|
+
// For triggering document-level onclick handlers (e.g., Monetag onclick_static)
|
|
128
|
+
// These clicks target the page content area, not specific UI elements
|
|
129
|
+
// NOTE: No preDelay needed — mouse movements + scrolling already provide ~1s
|
|
130
|
+
// of activity before clicks fire, which is enough for async ad script registration
|
|
131
|
+
const CONTENT_CLICK = {
|
|
132
|
+
CLICK_COUNT: 2, // Two attempts (primary + backup if first suppressed)
|
|
133
|
+
INTER_CLICK_MIN: 300, // Minimum ms between clicks (above Monetag 250ms cooldown)
|
|
134
|
+
INTER_CLICK_MAX: 500, // Maximum ms between clicks
|
|
135
|
+
PRE_CLICK_DELAY: 300, // Small buffer for late-loading async ad scripts
|
|
136
|
+
VIEWPORT_INSET: 0.2, // Avoid outer 20% of viewport (menus, overlays)
|
|
137
|
+
MOUSE_APPROACH_STEPS: 3 // Minimal steps — just enough for non-instant movement
|
|
138
|
+
};
|
|
139
|
+
|
|
126
140
|
// === INTENSITY SETTINGS ===
|
|
127
141
|
// Pre-configured intensity levels - modify these to change overall behavior
|
|
128
142
|
const INTENSITY_SETTINGS = {
|
|
@@ -606,6 +620,97 @@ async function interactWithElements(page, options = {}) {
|
|
|
606
620
|
}
|
|
607
621
|
}
|
|
608
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Clicks random spots in the page content area to trigger document-level
|
|
625
|
+
* onclick handlers (Monetag onclick_static, similar popunder SDKs).
|
|
626
|
+
*
|
|
627
|
+
* WHY THIS EXISTS:
|
|
628
|
+
* Ad onclick SDKs attach a single listener on `document` (capture phase)
|
|
629
|
+
* that fires on ANY click with `isTrusted: true`. They don't care which
|
|
630
|
+
* element was clicked — just that a real input event reached the document.
|
|
631
|
+
* `interactWithElements()` hunts for <button>/<a> which may not exist or
|
|
632
|
+
* may be excluded by the SDK's own filter. This function simply clicks
|
|
633
|
+
* the content area of the page where the SDK will always accept the event.
|
|
634
|
+
*
|
|
635
|
+
* TIMING:
|
|
636
|
+
* - 300ms preDelay: small buffer after mouse/scroll activity (~1.2s) for
|
|
637
|
+
* any late-loading async ad scripts to finish registering listeners.
|
|
638
|
+
* - Spaces clicks 300-500ms apart (above Monetag's 250ms cooldown).
|
|
639
|
+
* - Total time: ~1.1s for 2 clicks (preDelay + move + pause + click + gap).
|
|
640
|
+
*
|
|
641
|
+
* TARGETING:
|
|
642
|
+
* - Clicks within the inner 60% of the viewport to avoid sticky headers,
|
|
643
|
+
* footers, sidebars, cookie banners, and overlay close buttons.
|
|
644
|
+
* - Each click gets a fresh random position with natural mouse approach.
|
|
645
|
+
*
|
|
646
|
+
* @param {import('puppeteer').Page} page
|
|
647
|
+
* @param {object} [options]
|
|
648
|
+
* @param {number} [options.clicks] Number of click attempts
|
|
649
|
+
* @param {number} [options.preDelay] Ms to wait before first click
|
|
650
|
+
* @param {number} [options.interClickMin] Min ms between clicks
|
|
651
|
+
* @param {number} [options.interClickMax] Max ms between clicks
|
|
652
|
+
* @param {boolean} [options.forceDebug] Log click coordinates
|
|
653
|
+
*/
|
|
654
|
+
async function performContentClicks(page, options = {}) {
|
|
655
|
+
const {
|
|
656
|
+
clicks = CONTENT_CLICK.CLICK_COUNT,
|
|
657
|
+
preDelay = CONTENT_CLICK.PRE_CLICK_DELAY,
|
|
658
|
+
interClickMin = CONTENT_CLICK.INTER_CLICK_MIN,
|
|
659
|
+
interClickMax = CONTENT_CLICK.INTER_CLICK_MAX,
|
|
660
|
+
forceDebug = false
|
|
661
|
+
} = options;
|
|
662
|
+
|
|
663
|
+
try {
|
|
664
|
+
if (page.isClosed()) return;
|
|
665
|
+
|
|
666
|
+
const viewport = await getCachedViewport(page);
|
|
667
|
+
const inset = CONTENT_CLICK.VIEWPORT_INSET;
|
|
668
|
+
const minX = Math.floor(viewport.width * inset);
|
|
669
|
+
const maxX = Math.floor(viewport.width * (1 - inset));
|
|
670
|
+
const minY = Math.floor(viewport.height * inset);
|
|
671
|
+
const maxY = Math.floor(viewport.height * (1 - inset));
|
|
672
|
+
|
|
673
|
+
// Wait for ad scripts to register their listeners
|
|
674
|
+
await fastTimeout(preDelay);
|
|
675
|
+
|
|
676
|
+
let lastX = minX + Math.floor(Math.random() * (maxX - minX));
|
|
677
|
+
let lastY = minY + Math.floor(Math.random() * (maxY - minY));
|
|
678
|
+
|
|
679
|
+
for (let i = 0; i < clicks; i++) {
|
|
680
|
+
if (page.isClosed()) break;
|
|
681
|
+
|
|
682
|
+
// Random position in content zone
|
|
683
|
+
const targetX = minX + Math.floor(Math.random() * (maxX - minX));
|
|
684
|
+
const targetY = minY + Math.floor(Math.random() * (maxY - minY));
|
|
685
|
+
|
|
686
|
+
// Natural mouse approach (few steps, no need for elaborate curves)
|
|
687
|
+
await humanLikeMouseMove(page, lastX, lastY, targetX, targetY, {
|
|
688
|
+
steps: CONTENT_CLICK.MOUSE_APPROACH_STEPS,
|
|
689
|
+
curve: 0.03 + Math.random() * 0.04,
|
|
690
|
+
jitter: 1
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Brief human-like pause, then click
|
|
694
|
+
await fastTimeout(TIMING.CLICK_PAUSE_MIN + Math.random() * (TIMING.CLICK_PAUSE_MAX - TIMING.CLICK_PAUSE_MIN));
|
|
695
|
+
await page.mouse.click(targetX, targetY);
|
|
696
|
+
|
|
697
|
+
if (forceDebug) {
|
|
698
|
+
console.log(`[interaction] Content click ${i + 1}/${clicks} at (${targetX}, ${targetY})`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
lastX = targetX;
|
|
702
|
+
lastY = targetY;
|
|
703
|
+
|
|
704
|
+
// Inter-click gap (skip after last click)
|
|
705
|
+
if (i < clicks - 1) {
|
|
706
|
+
await fastTimeout(interClickMin + Math.random() * (interClickMax - interClickMin));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
} catch (err) {
|
|
710
|
+
// Content clicks are supplementary — never break the scan
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
609
714
|
/**
|
|
610
715
|
* Simulates realistic typing behavior with human characteristics
|
|
611
716
|
*
|
|
@@ -876,12 +981,22 @@ async function performPageInteraction(page, currentUrl, options = {}, forceDebug
|
|
|
876
981
|
}
|
|
877
982
|
}
|
|
878
983
|
|
|
879
|
-
//
|
|
984
|
+
// Click interaction — two strategies for maximum ad script coverage
|
|
985
|
+
// 1. Content area clicks: triggers document-level onclick handlers
|
|
986
|
+
// (Monetag, similar popunder SDKs that listen on document)
|
|
987
|
+
// 2. Element clicks: interacts with specific UI elements
|
|
988
|
+
// (ad scripts that attach to specific clickable elements)
|
|
880
989
|
if (includeElementClicks) {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
990
|
+
if (checkTimeout()) return; // Emergency timeout check
|
|
991
|
+
// Primary: content area clicks for document-level onclick handlers
|
|
992
|
+
await performContentClicks(page, { forceDebug });
|
|
993
|
+
// Secondary: targeted element clicks (fast, 1 attempt only)
|
|
994
|
+
if (!checkTimeout()) {
|
|
995
|
+
await interactWithElements(page, {
|
|
996
|
+
maxAttempts: 1,
|
|
997
|
+
avoidDestructive: true
|
|
998
|
+
});
|
|
999
|
+
}
|
|
885
1000
|
}
|
|
886
1001
|
|
|
887
1002
|
// Periodic memory cleanup during interaction
|
|
@@ -1105,6 +1220,7 @@ module.exports = {
|
|
|
1105
1220
|
humanLikeMouseMove,
|
|
1106
1221
|
simulateScrolling,
|
|
1107
1222
|
interactWithElements,
|
|
1223
|
+
performContentClicks,
|
|
1108
1224
|
simulateTyping,
|
|
1109
1225
|
generateRandomCoordinates
|
|
1110
1226
|
};
|
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');
|
|
@@ -39,7 +40,7 @@ const { processResults } = require('./lib/post-processing');
|
|
|
39
40
|
// Colorize various text when used
|
|
40
41
|
const { colorize, colors, messageColors, tags, formatLogMessage } = require('./lib/colorize');
|
|
41
42
|
// Enhanced mouse interaction and page simulation
|
|
42
|
-
const { performPageInteraction, createInteractionConfig } = require('./lib/interaction');
|
|
43
|
+
const { performPageInteraction, createInteractionConfig, performContentClicks, humanLikeMouseMove } = require('./lib/interaction');
|
|
43
44
|
// Domain detection cache for performance optimization
|
|
44
45
|
const { createGlobalHelpers, getTotalDomainsSkipped, getDetectedDomainsCount } = require('./lib/domain-cache');
|
|
45
46
|
const { createSmartCache } = require('./lib/smart-cache'); // Smart cache system
|
|
@@ -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',
|
|
@@ -1447,7 +1472,6 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1447
1472
|
'--disable-features=SafeBrowsing',
|
|
1448
1473
|
'--disable-dev-shm-usage',
|
|
1449
1474
|
'--disable-sync',
|
|
1450
|
-
'--disable-gpu', // WebGL null-context handled by fingerprint.js Proxy mock
|
|
1451
1475
|
'--mute-audio',
|
|
1452
1476
|
'--disable-translate',
|
|
1453
1477
|
'--window-size=1920,1080',
|
|
@@ -1467,6 +1491,12 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1467
1491
|
'--disable-background-timer-throttling',
|
|
1468
1492
|
'--disable-features=site-per-process', // Better for single-site scanning
|
|
1469
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
|
|
1470
1500
|
...extraArgs,
|
|
1471
1501
|
],
|
|
1472
1502
|
// Optimized timeouts for Puppeteer 23.x performance
|
|
@@ -1518,7 +1548,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1518
1548
|
// Set up cleanup on process termination
|
|
1519
1549
|
process.on('SIGINT', async () => {
|
|
1520
1550
|
if (forceDebug) console.log(formatLogMessage('debug', 'SIGINT received, performing cleanup...'));
|
|
1521
|
-
|
|
1551
|
+
flushLogBuffersSync();
|
|
1522
1552
|
if (_logFlushTimer) clearInterval(_logFlushTimer);
|
|
1523
1553
|
await performEmergencyCleanup();
|
|
1524
1554
|
process.exit(0);
|
|
@@ -1526,7 +1556,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1526
1556
|
|
|
1527
1557
|
process.on('SIGTERM', async () => {
|
|
1528
1558
|
if (forceDebug) console.log(formatLogMessage('debug', 'SIGTERM received, performing cleanup...'));
|
|
1529
|
-
|
|
1559
|
+
flushLogBuffersSync();
|
|
1530
1560
|
if (_logFlushTimer) clearInterval(_logFlushTimer);
|
|
1531
1561
|
await performEmergencyCleanup();
|
|
1532
1562
|
process.exit(0);
|
|
@@ -1557,6 +1587,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1557
1587
|
}
|
|
1558
1588
|
wgDisconnectAll(forceDebug);
|
|
1559
1589
|
ovpnDisconnectAll(forceDebug);
|
|
1590
|
+
cleanupCloudflareCache();
|
|
1560
1591
|
}
|
|
1561
1592
|
|
|
1562
1593
|
let siteCounter = 0;
|
|
@@ -3220,7 +3251,9 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3220
3251
|
siteCounter++;
|
|
3221
3252
|
|
|
3222
3253
|
// Enhanced Cloudflare handling with parallel detection
|
|
3223
|
-
|
|
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) {
|
|
3224
3257
|
try {
|
|
3225
3258
|
const parallelResult = await parallelChallengeDetection(page, forceDebug);
|
|
3226
3259
|
if (parallelResult.hasAnyChallenge && forceDebug) {
|
|
@@ -3660,6 +3693,32 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3660
3693
|
}
|
|
3661
3694
|
}
|
|
3662
3695
|
|
|
3696
|
+
// Post-reload interaction: trigger onclick ad scripts (Monetag etc.)
|
|
3697
|
+
// Each reload gives a fresh session with a new random ad domain —
|
|
3698
|
+
// without clicks the SDK never fires and we miss those domains.
|
|
3699
|
+
if (interactEnabled && !page.isClosed()) {
|
|
3700
|
+
try {
|
|
3701
|
+
// Brief wait for ad scripts to re-register after reload
|
|
3702
|
+
await fastTimeout(800);
|
|
3703
|
+
// Quick mouse moves to build movement score (Monetag tracks this)
|
|
3704
|
+
const vp = page.viewport() || { width: 1920, height: 1080 };
|
|
3705
|
+
const startX = 200 + Math.floor(Math.random() * (vp.width - 400));
|
|
3706
|
+
const startY = 200 + Math.floor(Math.random() * (vp.height - 400));
|
|
3707
|
+
await page.mouse.move(startX, startY);
|
|
3708
|
+
for (let m = 0; m < 2; m++) {
|
|
3709
|
+
const endX = 200 + Math.floor(Math.random() * (vp.width - 400));
|
|
3710
|
+
const endY = 200 + Math.floor(Math.random() * (vp.height - 400));
|
|
3711
|
+
await humanLikeMouseMove(page, startX, startY, endX, endY, { steps: 3, curve: 0.04, jitter: 1 });
|
|
3712
|
+
}
|
|
3713
|
+
// Content clicks to trigger document-level onclick handlers
|
|
3714
|
+
await performContentClicks(page, { clicks: 2, preDelay: 200, forceDebug });
|
|
3715
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Post-reload interaction completed for reload #${i}`));
|
|
3716
|
+
} catch (postReloadInteractErr) {
|
|
3717
|
+
// Non-critical — continue with remaining reloads
|
|
3718
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Post-reload interaction failed: ${postReloadInteractErr.message}`));
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3663
3722
|
// Only add delay if we're continuing with more reloads
|
|
3664
3723
|
if (i < totalReloads) {
|
|
3665
3724
|
// Reduce delay for problematic sites
|
|
@@ -4404,7 +4463,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4404
4463
|
}
|
|
4405
4464
|
|
|
4406
4465
|
// Flush any remaining buffered log entries before compression/exit
|
|
4407
|
-
|
|
4466
|
+
flushLogBuffersSync();
|
|
4408
4467
|
if (_logFlushTimer) {
|
|
4409
4468
|
clearInterval(_logFlushTimer);
|
|
4410
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": {
|