@fanboynz/network-scanner 2.0.50 → 2.0.52
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/README.md +81 -0
- package/lib/cloudflare.js +217 -176
- package/lib/fingerprint.js +22 -1
- package/lib/proxy.js +279 -0
- package/nwss.js +158 -25
- package/package.json +2 -2
package/lib/cloudflare.js
CHANGED
|
@@ -58,11 +58,76 @@ const FAST_TIMEOUTS = {
|
|
|
58
58
|
ELEMENT_INTERACTION_DELAY: 250, // Fast element interactions
|
|
59
59
|
SELECTOR_WAIT: 3000, // Fast selector waits
|
|
60
60
|
TURNSTILE_OPERATION: 6000, // Fast Turnstile operations
|
|
61
|
-
JS_CHALLENGE:
|
|
61
|
+
JS_CHALLENGE: 10000, // Fast JS challenge completion
|
|
62
62
|
CHALLENGE_SOLVING: 30000, // Fast overall challenge solving
|
|
63
63
|
CHALLENGE_COMPLETION: 8000 // Fast completion check
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Finds and clicks an element inside shadow DOM trees via page.evaluate
|
|
68
|
+
* Returns {found, clicked, x, y} - coordinates allow fallback mouse.click
|
|
69
|
+
*/
|
|
70
|
+
async function clickInShadowDOM(context, selectors, forceDebug = false, waitMs = 1500) {
|
|
71
|
+
// Try Puppeteer's pierce/ selector first � handles CLOSED shadow roots via CDP
|
|
72
|
+
for (const selector of selectors) {
|
|
73
|
+
try {
|
|
74
|
+
// Wait for element to appear (handles delayed rendering)
|
|
75
|
+
const start = Date.now();
|
|
76
|
+
const element = await context.waitForSelector(`pierce/${selector}`, { timeout: waitMs });
|
|
77
|
+
if (element) {
|
|
78
|
+
const box = await element.boundingBox();
|
|
79
|
+
if (box && box.width > 0 && box.height > 0) {
|
|
80
|
+
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
|
+
await element.click();
|
|
82
|
+
await element.dispose();
|
|
83
|
+
return { found: true, clicked: true, selector, x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
84
|
+
}
|
|
85
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `pierce/${selector} found but not visible (0x0)`));
|
|
86
|
+
await element.dispose();
|
|
87
|
+
// Element found but not visible
|
|
88
|
+
return { found: true, clicked: false, selector, x: 0, y: 0 };
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `pierce/${selector} timeout after ${waitMs}ms`));
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fallback: manual traversal for open shadow roots
|
|
97
|
+
const result = await context.evaluate((sels) => {
|
|
98
|
+
function deepQuery(root, selector) {
|
|
99
|
+
// Try direct query first
|
|
100
|
+
const el = root.querySelector(selector);
|
|
101
|
+
if (el) return el;
|
|
102
|
+
|
|
103
|
+
// Traverse shadow roots
|
|
104
|
+
const allElements = root.querySelectorAll('*');
|
|
105
|
+
for (const node of allElements) {
|
|
106
|
+
if (node.shadowRoot) {
|
|
107
|
+
const found = deepQuery(node.shadowRoot, selector);
|
|
108
|
+
if (found) return found;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const selector of sels) {
|
|
115
|
+
const el = deepQuery(document, selector);
|
|
116
|
+
if (el) {
|
|
117
|
+
const rect = el.getBoundingClientRect();
|
|
118
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
119
|
+
el.click();
|
|
120
|
+
return { found: true, clicked: true, selector, x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
121
|
+
}
|
|
122
|
+
return { found: true, clicked: false, selector, x: 0, y: 0 };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { found: false, clicked: false, selector: null, x: 0, y: 0 };
|
|
126
|
+
}, selectors);
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
66
131
|
/**
|
|
67
132
|
* Error categories for better handling
|
|
68
133
|
*/
|
|
@@ -306,12 +371,12 @@ function categorizeError(error) {
|
|
|
306
371
|
/**
|
|
307
372
|
* Implements exponential backoff delay
|
|
308
373
|
*/
|
|
309
|
-
|
|
374
|
+
function getRetryDelay(attempt) {
|
|
310
375
|
const delay = Math.min(
|
|
311
376
|
RETRY_CONFIG.baseDelay * Math.pow(RETRY_CONFIG.backoffMultiplier, attempt - 1),
|
|
312
377
|
RETRY_CONFIG.maxDelay
|
|
313
378
|
);
|
|
314
|
-
return
|
|
379
|
+
return delay;
|
|
315
380
|
}
|
|
316
381
|
|
|
317
382
|
/**
|
|
@@ -341,32 +406,6 @@ async function safePageEvaluate(page, func, timeout = TIMEOUTS.PAGE_EVALUATION_S
|
|
|
341
406
|
throw new Error('Page URL access failed - likely detached');
|
|
342
407
|
}
|
|
343
408
|
|
|
344
|
-
// Quick execution context validation with timeout
|
|
345
|
-
const contextValid = await Promise.race([
|
|
346
|
-
page.evaluate(() => {
|
|
347
|
-
try {
|
|
348
|
-
// Quick context validation
|
|
349
|
-
if (typeof window === 'undefined' || !document) {
|
|
350
|
-
return false;
|
|
351
|
-
}
|
|
352
|
-
// Check if document is ready for interaction
|
|
353
|
-
if (document.readyState === 'uninitialized') {
|
|
354
|
-
return false;
|
|
355
|
-
}
|
|
356
|
-
return true;
|
|
357
|
-
} catch (e) {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
}),
|
|
361
|
-
new Promise((_, reject) => {
|
|
362
|
-
setTimeout(() => reject(new Error('Context validation timeout')), 3500);
|
|
363
|
-
})
|
|
364
|
-
]).catch(() => false);
|
|
365
|
-
|
|
366
|
-
if (!contextValid) {
|
|
367
|
-
throw new Error('Page execution context is invalid');
|
|
368
|
-
}
|
|
369
|
-
|
|
370
409
|
const result = await Promise.race([
|
|
371
410
|
page.evaluate(func),
|
|
372
411
|
new Promise((_, reject) => {
|
|
@@ -418,7 +457,7 @@ async function safePageEvaluate(page, func, timeout = TIMEOUTS.PAGE_EVALUATION_S
|
|
|
418
457
|
}
|
|
419
458
|
|
|
420
459
|
// Wait before retrying with exponential backoff
|
|
421
|
-
await getRetryDelay(attempt);
|
|
460
|
+
await new Promise(resolve => setTimeout(resolve, getRetryDelay(attempt)));
|
|
422
461
|
}
|
|
423
462
|
}
|
|
424
463
|
|
|
@@ -440,15 +479,18 @@ async function safePageEvaluate(page, func, timeout = TIMEOUTS.PAGE_EVALUATION_S
|
|
|
440
479
|
* Safe element clicking with timeout protection
|
|
441
480
|
*/
|
|
442
481
|
async function safeClick(page, selector, timeout = TIMEOUTS.CLICK_TIMEOUT) {
|
|
482
|
+
let timeoutId;
|
|
443
483
|
try {
|
|
444
484
|
return await Promise.race([
|
|
445
485
|
page.click(selector, { timeout: timeout }),
|
|
446
486
|
new Promise((_, reject) => {
|
|
447
|
-
setTimeout(() => reject(new Error('Click timeout')), timeout + TIMEOUTS.CLICK_TIMEOUT_BUFFER);
|
|
487
|
+
timeoutId = setTimeout(() => reject(new Error('Click timeout')), timeout + TIMEOUTS.CLICK_TIMEOUT_BUFFER);
|
|
448
488
|
})
|
|
449
489
|
]);
|
|
450
490
|
} catch (error) {
|
|
451
491
|
throw new Error(`Click failed: ${error.message}`);
|
|
492
|
+
} finally {
|
|
493
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
452
494
|
}
|
|
453
495
|
}
|
|
454
496
|
|
|
@@ -456,16 +498,18 @@ async function safeClick(page, selector, timeout = TIMEOUTS.CLICK_TIMEOUT) {
|
|
|
456
498
|
* Safe navigation waiting with timeout protection
|
|
457
499
|
*/
|
|
458
500
|
async function safeWaitForNavigation(page, timeout = TIMEOUTS.NAVIGATION_TIMEOUT) {
|
|
501
|
+
let timeoutId;
|
|
459
502
|
try {
|
|
460
503
|
return await Promise.race([
|
|
461
504
|
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: timeout }),
|
|
462
505
|
new Promise((_, reject) => {
|
|
463
|
-
setTimeout(() => reject(new Error('Navigation timeout')), timeout + TIMEOUTS.NAVIGATION_TIMEOUT_BUFFER);
|
|
506
|
+
timeoutId = setTimeout(() => reject(new Error('Navigation timeout')), timeout + TIMEOUTS.NAVIGATION_TIMEOUT_BUFFER);
|
|
464
507
|
})
|
|
465
508
|
]);
|
|
466
509
|
} catch (error) {
|
|
467
510
|
console.warn(formatLogMessage('cloudflare', `Navigation wait failed: ${error.message}`));
|
|
468
|
-
|
|
511
|
+
} finally {
|
|
512
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
469
513
|
}
|
|
470
514
|
}
|
|
471
515
|
|
|
@@ -563,7 +607,14 @@ async function quickCloudflareDetection(page, forceDebug = false) {
|
|
|
563
607
|
*/
|
|
564
608
|
async function analyzeCloudflareChallenge(page) {
|
|
565
609
|
try {
|
|
566
|
-
|
|
610
|
+
// CDP-level frame check � bypasses closed shadow roots
|
|
611
|
+
const frames = page.frames();
|
|
612
|
+
const hasChallengeFrame = frames.some(f => {
|
|
613
|
+
const url = f.url();
|
|
614
|
+
return url.includes('challenges.cloudflare.com') || url.includes('/cdn-cgi/challenge-platform/');
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const result = await safePageEvaluate(page, () => {
|
|
567
618
|
const title = document.title || '';
|
|
568
619
|
const bodyText = document.body ? document.body.textContent : '';
|
|
569
620
|
|
|
@@ -635,6 +686,15 @@ async function analyzeCloudflareChallenge(page) {
|
|
|
635
686
|
bodySnippet: bodyText.substring(0, 200)
|
|
636
687
|
};
|
|
637
688
|
}, TIMEOUTS.PAGE_EVALUATION);
|
|
689
|
+
|
|
690
|
+
// Merge CDP frame detection � catches iframes behind closed shadow roots
|
|
691
|
+
if (hasChallengeFrame && !result.hasTurnstileIframe) {
|
|
692
|
+
result.hasTurnstileIframe = true;
|
|
693
|
+
result.isTurnstile = true;
|
|
694
|
+
result.isChallengePresent = true;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return result;
|
|
638
698
|
} catch (error) {
|
|
639
699
|
return {
|
|
640
700
|
isChallengePresent: false,
|
|
@@ -842,7 +902,7 @@ async function handleVerificationChallengeWithRetries(page, currentUrl, siteConf
|
|
|
842
902
|
|
|
843
903
|
// If this wasn't the last attempt, wait before retrying
|
|
844
904
|
if (attempt < retryConfig.maxAttempts) {
|
|
845
|
-
const delay =
|
|
905
|
+
const delay = getRetryDelay(attempt);
|
|
846
906
|
if (forceDebug) {
|
|
847
907
|
console.log(formatLogMessage('cloudflare', `Challenge attempt ${attempt} failed, retrying in ${delay}ms: ${result.error}`));
|
|
848
908
|
}
|
|
@@ -884,7 +944,7 @@ async function handleVerificationChallengeWithRetries(page, currentUrl, siteConf
|
|
|
884
944
|
|
|
885
945
|
// Wait before retrying with exponential backoff
|
|
886
946
|
if (attempt < retryConfig.maxAttempts) {
|
|
887
|
-
await getRetryDelay(attempt);
|
|
947
|
+
await new Promise(resolve => setTimeout(resolve, getRetryDelay(attempt)));
|
|
888
948
|
}
|
|
889
949
|
}
|
|
890
950
|
}
|
|
@@ -925,7 +985,7 @@ async function handlePhishingWarningWithRetries(page, currentUrl, siteConfig, fo
|
|
|
925
985
|
|
|
926
986
|
// If this wasn't the last attempt, wait before retrying
|
|
927
987
|
if (attempt < retryConfig.maxAttempts) {
|
|
928
|
-
const delay =
|
|
988
|
+
const delay = getRetryDelay(attempt);
|
|
929
989
|
if (forceDebug) {
|
|
930
990
|
console.log(formatLogMessage('cloudflare', `Phishing warning attempt ${attempt} failed, retrying in ${delay}ms: ${result.error}`));
|
|
931
991
|
}
|
|
@@ -955,7 +1015,7 @@ async function handlePhishingWarningWithRetries(page, currentUrl, siteConfig, fo
|
|
|
955
1015
|
|
|
956
1016
|
// Wait before retrying with exponential backoff
|
|
957
1017
|
if (attempt < retryConfig.maxAttempts) {
|
|
958
|
-
await getRetryDelay(attempt);
|
|
1018
|
+
await new Promise(resolve => setTimeout(resolve, getRetryDelay(attempt)));
|
|
959
1019
|
}
|
|
960
1020
|
}
|
|
961
1021
|
}
|
|
@@ -1026,6 +1086,23 @@ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug
|
|
|
1026
1086
|
|
|
1027
1087
|
const jsResult = await waitForJSChallengeCompletion(page, forceDebug);
|
|
1028
1088
|
if (jsResult.success) {
|
|
1089
|
+
// Wait for redirect after challenge completion
|
|
1090
|
+
try {
|
|
1091
|
+
const startUrl = await page.url();
|
|
1092
|
+
await page.waitForFunction(
|
|
1093
|
+
(origUrl) => {
|
|
1094
|
+
const bodyText = document.body?.textContent || '';
|
|
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`));
|
|
1105
|
+
}
|
|
1029
1106
|
result.success = true;
|
|
1030
1107
|
result.method = 'js_challenge_wait';
|
|
1031
1108
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `JS challenge completed successfully for ${currentUrl}`));
|
|
@@ -1034,6 +1111,8 @@ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug
|
|
|
1034
1111
|
} catch (jsError) {
|
|
1035
1112
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `JS challenge wait failed for ${currentUrl}: ${jsError.message}`));
|
|
1036
1113
|
}
|
|
1114
|
+
} else if (forceDebug) {
|
|
1115
|
+
console.log(formatLogMessage('cloudflare', `Skipping JS challenge method (not detected)`));
|
|
1037
1116
|
}
|
|
1038
1117
|
|
|
1039
1118
|
// Method 2: Handle Turnstile challenges (interactive)
|
|
@@ -1051,6 +1130,8 @@ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug
|
|
|
1051
1130
|
} catch (turnstileError) {
|
|
1052
1131
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `Turnstile method failed for ${currentUrl}: ${turnstileError.message}`));
|
|
1053
1132
|
}
|
|
1133
|
+
} else if (forceDebug) {
|
|
1134
|
+
console.log(formatLogMessage('cloudflare', `Skipping Turnstile method (not detected)`));
|
|
1054
1135
|
}
|
|
1055
1136
|
|
|
1056
1137
|
// Method 3: Legacy checkbox interaction (fallback)
|
|
@@ -1068,10 +1149,23 @@ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug
|
|
|
1068
1149
|
} catch (legacyError) {
|
|
1069
1150
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `Legacy checkbox method failed for ${currentUrl}: ${legacyError.message}`));
|
|
1070
1151
|
}
|
|
1152
|
+
} else if (forceDebug) {
|
|
1153
|
+
console.log(formatLogMessage('cloudflare', `Skipping legacy checkbox method (not detected)`));
|
|
1071
1154
|
}
|
|
1072
1155
|
|
|
1073
1156
|
if (!result.success) {
|
|
1074
1157
|
result.error = result.error || 'All challenge bypass methods failed';
|
|
1158
|
+
if (forceDebug) {
|
|
1159
|
+
try {
|
|
1160
|
+
const postState = await page.evaluate(() => ({
|
|
1161
|
+
title: document.title,
|
|
1162
|
+
url: window.location.href,
|
|
1163
|
+
body: (document.body?.textContent || '').substring(0, 300)
|
|
1164
|
+
}));
|
|
1165
|
+
console.log(formatLogMessage('cloudflare', `Post-attempt page state: title="${postState.title}" url=${postState.url}`));
|
|
1166
|
+
console.log(formatLogMessage('cloudflare', `Post-attempt body: ${postState.body}`));
|
|
1167
|
+
} catch (_) {}
|
|
1168
|
+
}
|
|
1075
1169
|
}
|
|
1076
1170
|
|
|
1077
1171
|
return result;
|
|
@@ -1089,88 +1183,57 @@ async function handleEmbeddedIframeChallenge(page, forceDebug = false) {
|
|
|
1089
1183
|
try {
|
|
1090
1184
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `Checking for embedded iframe challenges`));
|
|
1091
1185
|
|
|
1092
|
-
//
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
'
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
];
|
|
1099
|
-
|
|
1100
|
-
// Wait for iframe to appear
|
|
1101
|
-
let iframeFound = false;
|
|
1102
|
-
for (const selector of iframeSelectors) {
|
|
1103
|
-
try {
|
|
1104
|
-
await Promise.race([
|
|
1105
|
-
page.waitForSelector(selector, { timeout: FAST_TIMEOUTS.SELECTOR_WAIT }),
|
|
1106
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), FAST_TIMEOUTS.SELECTOR_WAIT + 1000))
|
|
1107
|
-
]);
|
|
1108
|
-
iframeFound = true;
|
|
1109
|
-
if (forceDebug) console.log(formatLogMessage('cloudflare', `Found iframe: ${selector}`));
|
|
1110
|
-
break;
|
|
1111
|
-
} catch (e) {
|
|
1112
|
-
continue;
|
|
1186
|
+
// Use CDP-level frame detection � bypasses closed shadow roots
|
|
1187
|
+
const frames = page.frames();
|
|
1188
|
+
if (forceDebug) {
|
|
1189
|
+
console.log(formatLogMessage('cloudflare', `Available frames (${frames.length}):`));
|
|
1190
|
+
for (const f of frames) {
|
|
1191
|
+
console.log(formatLogMessage('cloudflare', ` ${f.url()}`));
|
|
1113
1192
|
}
|
|
1114
1193
|
}
|
|
1115
|
-
|
|
1116
|
-
if (!iframeFound) {
|
|
1117
|
-
result.error = 'No embedded iframe found';
|
|
1118
|
-
return result;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
// Find challenge frame using existing frame detection logic
|
|
1122
|
-
const frames = await page.frames();
|
|
1123
1194
|
const challengeFrame = frames.find(frame => {
|
|
1124
1195
|
const frameUrl = frame.url();
|
|
1125
1196
|
return frameUrl.includes('challenges.cloudflare.com') ||
|
|
1197
|
+
frameUrl.includes('/cdn-cgi/challenge-platform/') ||
|
|
1126
1198
|
frameUrl.includes('/turnstile/if/') ||
|
|
1127
|
-
frameUrl.includes('captcha-delivery.com') ||
|
|
1128
|
-
frameUrl.includes('/challenge-platform/') ||
|
|
1129
1199
|
frameUrl.includes('turnstile');
|
|
1130
1200
|
});
|
|
1131
1201
|
|
|
1132
1202
|
if (!challengeFrame) {
|
|
1133
|
-
result.error = '
|
|
1203
|
+
result.error = 'No challenge frame found via CDP';
|
|
1134
1204
|
return result;
|
|
1135
1205
|
}
|
|
1136
1206
|
|
|
1137
1207
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `Interacting with iframe: ${challengeFrame.url()}`));
|
|
1138
1208
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1209
|
+
await waitForTimeout(page, 500);
|
|
1210
|
+
|
|
1211
|
+
let checkboxInteractionSuccess = false;
|
|
1212
|
+
try {
|
|
1213
|
+
const shadowResult = await clickInShadowDOM(challengeFrame, [
|
|
1141
1214
|
'input[type="checkbox"]',
|
|
1142
1215
|
'.ctp-checkbox',
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
try {
|
|
1151
|
-
await Promise.race([
|
|
1152
|
-
challengeFrame.waitForSelector(selector, { timeout: FAST_TIMEOUTS.SELECTOR_WAIT }),
|
|
1153
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), FAST_TIMEOUTS.SELECTOR_WAIT + 1000))
|
|
1154
|
-
]);
|
|
1155
|
-
|
|
1156
|
-
await waitForTimeout(page, FAST_TIMEOUTS.ELEMENT_INTERACTION_DELAY);
|
|
1157
|
-
await challengeFrame.click(selector);
|
|
1158
|
-
|
|
1159
|
-
if (forceDebug) console.log(formatLogMessage('cloudflare', `Clicked iframe element: ${selector}`));
|
|
1216
|
+
'.ctp-checkbox-label',
|
|
1217
|
+
'[role="checkbox"]',
|
|
1218
|
+
'label.cb-lb',
|
|
1219
|
+
'label'
|
|
1220
|
+
], forceDebug);
|
|
1221
|
+
|
|
1222
|
+
if (shadowResult.clicked) {
|
|
1160
1223
|
checkboxInteractionSuccess = true;
|
|
1161
|
-
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1224
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `Shadow DOM click succeeded: ${shadowResult.selector}`));
|
|
1225
|
+
} else if (shadowResult.found && shadowResult.x > 0) {
|
|
1226
|
+
await page.mouse.click(shadowResult.x, shadowResult.y);
|
|
1227
|
+
checkboxInteractionSuccess = true;
|
|
1228
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `Shadow DOM mouse fallback at (${shadowResult.x}, ${shadowResult.y})`));
|
|
1164
1229
|
}
|
|
1230
|
+
} catch (shadowErr) {
|
|
1231
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `Shadow DOM click failed: ${shadowErr.message}`));
|
|
1165
1232
|
}
|
|
1166
1233
|
|
|
1167
|
-
// Try alternative interaction only if standard selectors failed
|
|
1168
1234
|
if (!checkboxInteractionSuccess) {
|
|
1169
|
-
if (forceDebug) console.log(formatLogMessage('cloudflare', `Checkbox interactions failed, trying container fallback`));
|
|
1170
|
-
await waitForTimeout(page, 1000);
|
|
1171
1235
|
|
|
1172
1236
|
try {
|
|
1173
|
-
// Try clicking on the iframe container itself as fallback
|
|
1174
1237
|
const iframeElement = await page.$('iframe[src*="challenges.cloudflare.com"]');
|
|
1175
1238
|
if (iframeElement) {
|
|
1176
1239
|
await iframeElement.click();
|
|
@@ -1179,8 +1242,6 @@ async function handleEmbeddedIframeChallenge(page, forceDebug = false) {
|
|
|
1179
1242
|
} catch (containerClickError) {
|
|
1180
1243
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `Container click failed: ${containerClickError.message}`));
|
|
1181
1244
|
}
|
|
1182
|
-
} else {
|
|
1183
|
-
if (forceDebug) console.log(formatLogMessage('cloudflare', `Checkbox interaction successful, skipping container fallback`));
|
|
1184
1245
|
}
|
|
1185
1246
|
|
|
1186
1247
|
// Reuse existing completion check pattern with error handling
|
|
@@ -1237,8 +1298,10 @@ async function waitForJSChallengeCompletion(page, forceDebug = false) {
|
|
|
1237
1298
|
await Promise.race([
|
|
1238
1299
|
page.waitForFunction(
|
|
1239
1300
|
() => {
|
|
1240
|
-
|
|
1241
|
-
|
|
1301
|
+
const bodyText = document.body.textContent;
|
|
1302
|
+
if (bodyText.includes('Verification successful')) return true;
|
|
1303
|
+
return !bodyText.includes('Checking your browser') &&
|
|
1304
|
+
!bodyText.includes('Please wait while we verify') &&
|
|
1242
1305
|
!document.querySelector('.cf-challenge-running') &&
|
|
1243
1306
|
!document.querySelector('[data-cf-challenge]');
|
|
1244
1307
|
},
|
|
@@ -1322,28 +1385,26 @@ async function handleTurnstileChallenge(page, forceDebug = false) {
|
|
|
1322
1385
|
console.log(formatLogMessage('cloudflare', `Found Turnstile iframe with URL: ${turnstileFrame.url()}`));
|
|
1323
1386
|
}
|
|
1324
1387
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
if (forceDebug) console.log(formatLogMessage('cloudflare', `
|
|
1342
|
-
break;
|
|
1343
|
-
} catch (e) {
|
|
1344
|
-
if (forceDebug) console.log(formatLogMessage('cloudflare', `Checkbox selector ${selector} not found or failed to click`));
|
|
1345
|
-
continue;
|
|
1388
|
+
await waitForTimeout(page, FAST_TIMEOUTS.ELEMENT_INTERACTION_DELAY);
|
|
1389
|
+
|
|
1390
|
+
try {
|
|
1391
|
+
const shadowResult = await clickInShadowDOM(turnstileFrame, [
|
|
1392
|
+
'input[type="checkbox"]',
|
|
1393
|
+
'.ctp-checkbox',
|
|
1394
|
+
'.ctp-checkbox-label',
|
|
1395
|
+
'[role="checkbox"]',
|
|
1396
|
+
'label.cb-lb',
|
|
1397
|
+
'label'
|
|
1398
|
+
], forceDebug);
|
|
1399
|
+
|
|
1400
|
+
if (shadowResult.clicked) {
|
|
1401
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `Turnstile shadow DOM click succeeded: ${shadowResult.selector}`));
|
|
1402
|
+
} else if (shadowResult.found && shadowResult.x > 0) {
|
|
1403
|
+
await page.mouse.click(shadowResult.x, shadowResult.y);
|
|
1404
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `Turnstile shadow DOM mouse fallback at (${shadowResult.x}, ${shadowResult.y})`));
|
|
1346
1405
|
}
|
|
1406
|
+
} catch (shadowErr) {
|
|
1407
|
+
if (forceDebug) console.log(formatLogMessage('cloudflare', `Shadow DOM fallback failed: ${shadowErr.message}`));
|
|
1347
1408
|
}
|
|
1348
1409
|
|
|
1349
1410
|
// Wait for Turnstile completion with reduced timeout
|
|
@@ -1531,7 +1592,11 @@ async function checkChallengeCompletion(page) {
|
|
|
1531
1592
|
* }
|
|
1532
1593
|
*/
|
|
1533
1594
|
async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDebug = false) {
|
|
1534
|
-
|
|
1595
|
+
const cfDebug = forceDebug || siteConfig.cloudflare_bypass === 'debug' || siteConfig.cloudflare_phish === 'debug';
|
|
1596
|
+
const cfBypassEnabled = siteConfig.cloudflare_bypass === true || siteConfig.cloudflare_bypass === 'debug';
|
|
1597
|
+
const cfPhishEnabled = siteConfig.cloudflare_phish === true || siteConfig.cloudflare_phish === 'debug';
|
|
1598
|
+
|
|
1599
|
+
if (cfDebug) {
|
|
1535
1600
|
console.log(formatLogMessage('cloudflare', `Using Cloudflare module v${CLOUDFLARE_MODULE_VERSION} for ${currentUrl}`));
|
|
1536
1601
|
}
|
|
1537
1602
|
|
|
@@ -1561,7 +1626,7 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
|
|
|
1561
1626
|
// Sets attempted: false, success: true for both protection types
|
|
1562
1627
|
|
|
1563
1628
|
// Only proceed if we have indicators OR explicit config enables Cloudflare handling
|
|
1564
|
-
if (!quickDetection.hasIndicators && !
|
|
1629
|
+
if (!quickDetection.hasIndicators && !cfPhishEnabled && !cfBypassEnabled) {
|
|
1565
1630
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `No Cloudflare indicators found and no explicit config, skipping protection handling for ${currentUrl}`));
|
|
1566
1631
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `Quick detection details: title="${quickDetection.title}", bodySnippet="${quickDetection.bodySnippet}"`));
|
|
1567
1632
|
return {
|
|
@@ -1586,7 +1651,7 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
|
|
|
1586
1651
|
try {
|
|
1587
1652
|
// Adaptive timeout based on detection results and explicit config
|
|
1588
1653
|
let adaptiveTimeout;
|
|
1589
|
-
if (
|
|
1654
|
+
if (cfPhishEnabled || cfBypassEnabled) {
|
|
1590
1655
|
// Explicit config - give more time
|
|
1591
1656
|
adaptiveTimeout = quickDetection.hasIndicators ? TIMEOUTS.ADAPTIVE_TIMEOUT_WITH_INDICATORS : TIMEOUTS.ADAPTIVE_TIMEOUT_WITHOUT_INDICATORS;
|
|
1592
1657
|
} else {
|
|
@@ -1599,7 +1664,7 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
|
|
|
1599
1664
|
}
|
|
1600
1665
|
|
|
1601
1666
|
return await Promise.race([
|
|
1602
|
-
performCloudflareHandling(page, currentUrl, siteConfig,
|
|
1667
|
+
performCloudflareHandling(page, currentUrl, siteConfig, cfDebug),
|
|
1603
1668
|
new Promise((resolve) => {
|
|
1604
1669
|
setTimeout(() => {
|
|
1605
1670
|
console.warn(formatLogMessage('cloudflare', `Adaptive timeout (${adaptiveTimeout}ms) for ${currentUrl} - continuing with scan`));
|
|
@@ -1631,6 +1696,9 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
|
|
|
1631
1696
|
* @returns {Promise<Object>} Same structure as handleCloudflareProtection()
|
|
1632
1697
|
*/
|
|
1633
1698
|
async function performCloudflareHandling(page, currentUrl, siteConfig, forceDebug = false) {
|
|
1699
|
+
const cfBypassEnabled = siteConfig.cloudflare_bypass === true || siteConfig.cloudflare_bypass === 'debug';
|
|
1700
|
+
const cfPhishEnabled = siteConfig.cloudflare_phish === true || siteConfig.cloudflare_phish === 'debug';
|
|
1701
|
+
|
|
1634
1702
|
const result = {
|
|
1635
1703
|
phishingWarning: { attempted: false, success: false },
|
|
1636
1704
|
verificationChallenge: { attempted: false, success: false },
|
|
@@ -1643,7 +1711,7 @@ async function performCloudflareHandling(page, currentUrl, siteConfig, forceDebu
|
|
|
1643
1711
|
// Handle phishing warnings first - updates result.phishingWarning
|
|
1644
1712
|
// Only runs if siteConfig.cloudflare_phish === true
|
|
1645
1713
|
// Handle phishing warnings if enabled
|
|
1646
|
-
if (
|
|
1714
|
+
if (cfPhishEnabled) {
|
|
1647
1715
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `Phishing warning bypass enabled for ${currentUrl}`));
|
|
1648
1716
|
|
|
1649
1717
|
const phishingResult = await handlePhishingWarningWithRetries(page, currentUrl, siteConfig, forceDebug);
|
|
@@ -1678,7 +1746,7 @@ async function performCloudflareHandling(page, currentUrl, siteConfig, forceDebu
|
|
|
1678
1746
|
// Only runs if siteConfig.cloudflare_bypass === true
|
|
1679
1747
|
// Sets requiresHuman: true if CAPTCHA detected (no bypass attempted)
|
|
1680
1748
|
// Handle verification challenges if enabled
|
|
1681
|
-
if (
|
|
1749
|
+
if (cfBypassEnabled) {
|
|
1682
1750
|
if (forceDebug) console.log(formatLogMessage('cloudflare', `Challenge bypass enabled for ${currentUrl}`));
|
|
1683
1751
|
|
|
1684
1752
|
const challengeResult = await handleVerificationChallengeWithRetries(page, currentUrl, siteConfig, forceDebug);
|
|
@@ -1731,55 +1799,28 @@ async function performCloudflareHandling(page, currentUrl, siteConfig, forceDebu
|
|
|
1731
1799
|
* Performs parallel detection of multiple challenge types for better performance
|
|
1732
1800
|
*/
|
|
1733
1801
|
async function parallelChallengeDetection(page, forceDebug = false) {
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
detected: document.querySelector('
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
};
|
|
1757
|
-
}).catch(err => ({ type: 'turnstile', detected: false, error: err.message }))
|
|
1758
|
-
);
|
|
1759
|
-
|
|
1760
|
-
// Check for phishing warning
|
|
1761
|
-
detectionPromises.push(
|
|
1762
|
-
page.evaluate(() => {
|
|
1763
|
-
return {
|
|
1764
|
-
type: 'phishing',
|
|
1765
|
-
detected: document.body?.textContent?.includes('This website has been reported for potential phishing') ||
|
|
1766
|
-
document.querySelector('a[href*="continue"]') !== null
|
|
1767
|
-
};
|
|
1768
|
-
}).catch(err => ({ type: 'phishing', detected: false, error: err.message }))
|
|
1769
|
-
);
|
|
1770
|
-
|
|
1771
|
-
// Check for managed challenge
|
|
1772
|
-
detectionPromises.push(
|
|
1773
|
-
page.evaluate(() => {
|
|
1774
|
-
return {
|
|
1775
|
-
type: 'managed',
|
|
1776
|
-
detected: document.querySelector('.cf-managed-challenge') !== null ||
|
|
1777
|
-
document.querySelector('[data-cf-managed]') !== null
|
|
1778
|
-
};
|
|
1779
|
-
}).catch(err => ({ type: 'managed', detected: false, error: err.message }))
|
|
1780
|
-
);
|
|
1781
|
-
|
|
1782
|
-
const results = await Promise.all(detectionPromises);
|
|
1802
|
+
let results;
|
|
1803
|
+
try {
|
|
1804
|
+
results = await page.evaluate(() => {
|
|
1805
|
+
const bodyText = document.body?.textContent || '';
|
|
1806
|
+
return [
|
|
1807
|
+
{ type: 'js', detected: document.querySelector('script[src*="/cdn-cgi/challenge-platform/"]') !== null ||
|
|
1808
|
+
bodyText.includes('Checking your browser') || bodyText.includes('Please wait while we verify') },
|
|
1809
|
+
{ type: 'turnstile', detected: document.querySelector('.cf-turnstile') !== null ||
|
|
1810
|
+
document.querySelector('iframe[src*="challenges.cloudflare.com"]') !== null ||
|
|
1811
|
+
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 },
|
|
1814
|
+
{ type: 'managed', detected: document.querySelector('.cf-managed-challenge') !== null ||
|
|
1815
|
+
document.querySelector('[data-cf-managed]') !== null }
|
|
1816
|
+
];
|
|
1817
|
+
});
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
results = [
|
|
1820
|
+
{ type: 'js', detected: false }, { type: 'turnstile', detected: false },
|
|
1821
|
+
{ type: 'phishing', detected: false }, { type: 'managed', detected: false }
|
|
1822
|
+
];
|
|
1823
|
+
}
|
|
1783
1824
|
|
|
1784
1825
|
const detectedChallenges = results.filter(r => r.detected).map(r => r.type);
|
|
1785
1826
|
|
package/lib/fingerprint.js
CHANGED
|
@@ -843,7 +843,28 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
843
843
|
}
|
|
844
844
|
}, 'WebGL spoofing');
|
|
845
845
|
|
|
846
|
-
//
|
|
846
|
+
// WebGL null-context safety net � prevents ad script crashes in headless
|
|
847
|
+
safeExecute(() => {
|
|
848
|
+
const originalGetContext = HTMLCanvasElement.prototype.getContext;
|
|
849
|
+
HTMLCanvasElement.prototype.getContext = function(type, attrs) {
|
|
850
|
+
const ctx = originalGetContext.call(this, type, attrs);
|
|
851
|
+
if (ctx !== null || (type !== 'webgl' && type !== 'experimental-webgl' && type !== 'webgl2')) return ctx;
|
|
852
|
+
// Return minimal mock so scripts calling getShaderPrecisionFormat etc. don't crash
|
|
853
|
+
const noop = () => {};
|
|
854
|
+
return new Proxy({}, {
|
|
855
|
+
get(_, prop) {
|
|
856
|
+
if (prop === 'getShaderPrecisionFormat') return () => ({ rangeMin: 127, rangeMax: 127, precision: 23 });
|
|
857
|
+
if (prop === 'getParameter') return (p) => ({ 37445: 'Intel Inc.', 37446: 'Intel(R) UHD Graphics 630' }[p] || 0);
|
|
858
|
+
if (prop === 'getSupportedExtensions') return () => [];
|
|
859
|
+
if (prop === 'getExtension') return () => null;
|
|
860
|
+
if (prop === 'canvas') return this;
|
|
861
|
+
if (prop === 'drawingBufferWidth') return 1920;
|
|
862
|
+
if (prop === 'drawingBufferHeight') return 1080;
|
|
863
|
+
return noop;
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
};
|
|
867
|
+
}, 'WebGL null-context safety net'); // Permissions API spoofing
|
|
847
868
|
//
|
|
848
869
|
safeExecute(() => {
|
|
849
870
|
if (navigator.permissions?.query) {
|