Files
2026-07-03 20:12:20 +08:00

165 lines
5.1 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Cloudflare / Turnstile 挑战页检测
*
* antiBot.cfClearanceRequired === falseA2C 业务域):仅真实 CF 中间页算 blocked
* 不因页面引用 turnstile.js 或缺少 cf_clearance cookie 误判。
*/
/**
* URL 是否处于 CF Managed Challenge 中间态(Whatshub 短链常见 __cf_chl_rt_tk
* @param {string} url
*/
function urlIndicatesCfChallenge(url) {
if (!url || typeof url !== 'string') {
return false;
}
return url.includes('__cf_chl_rt_tk')
|| url.includes('__cf_chl_tk')
|| url.includes('__cf_chl_f_tk')
|| url.includes('/cdn-cgi/challenge-platform')
|| url.includes('/cdn-cgi/challenge');
}
/** CF 挑战态查询参数(CapSolver websiteURL 需剔除) */
const CF_CHALLENGE_QUERY_KEYS = [
'__cf_chl_rt_tk',
'__cf_chl_tk',
'__cf_chl_f_tk',
'__cf_chl_captcha_tk__',
];
/**
* 去掉 URL 中 CF 挑战临时参数,供 CapSolver / reload 使用
* @param {string} url
* @returns {string}
*/
function sanitizePageUrlForCfSolver(url) {
if (!url || typeof url !== 'string') {
return url || '';
}
try {
const parsed = new URL(url);
for (const key of CF_CHALLENGE_QUERY_KEYS) {
parsed.searchParams.delete(key);
}
return parsed.toString();
} catch (_) {
return url.split('?')[0] || url;
}
}
/**
* 是否要求 cf_clearance 才视为过盾完成(A2C user.a2c.chat 为 false
* @param {object|null|undefined} antiBot
*/
function isCfClearanceRequired(antiBot) {
return antiBot?.cfClearanceRequired !== false;
}
/**
* @typedef {Object} CloudflareState
* @property {boolean} blocked 是否处于 CF 挑战中
* @property {string|null} challengeType turnstile|js_challenge|unknown|null
* @property {boolean} hasCfClearance 是否已有 cf_clearance cookie
* @property {string} url 当前页面 URL
* @property {string} title 页面 title
*/
/**
* 检测页面是否被 Cloudflare 拦截
* @param {import('puppeteer').Page} page
* @param {object|null} [antiBot]
* @returns {Promise<CloudflareState>}
*/
async function detectCloudflare(page, antiBot = null) {
const url = page.url();
const title = await page.title().catch(() => '');
const cookies = await page.cookies().catch(() => []);
const hasCfClearance = cookies.some((c) => c.name === 'cf_clearance');
const domSignals = await page.evaluate(() => {
const hasTurnstileInput = !!document.querySelector('[name="cf-turnstile-response"]');
const hasTurnstileWidget = !!document.querySelector('.cf-turnstile, [data-sitekey]');
const hasChallengeRunning = !!document.querySelector('#challenge-running, #cf-challenge-running');
const titleText = document.title || '';
const bodyText = (document.body && document.body.innerText) ? document.body.innerText.slice(0, 500) : '';
return {
hasTurnstileInput,
hasTurnstileWidget,
hasChallengeRunning,
titleHasJustAMoment: titleText.includes('Just a moment'),
bodyHasJustAMoment: bodyText.includes('Just a moment')
|| bodyText.includes('Checking your browser')
|| bodyText.includes('Performing security verification'),
};
}).catch(() => ({
hasTurnstileInput: false,
hasTurnstileWidget: false,
hasChallengeRunning: false,
titleHasJustAMoment: false,
bodyHasJustAMoment: false,
}));
const urlChallenge = urlIndicatesCfChallenge(url);
const realChallenge = urlChallenge
|| domSignals.titleHasJustAMoment
|| domSignals.bodyHasJustAMoment
|| domSignals.hasChallengeRunning
|| title.includes('Just a moment')
|| title.includes('Performing security verification');
const turnstileSignals = domSignals.hasTurnstileInput || domSignals.hasTurnstileWidget;
let challengeType = null;
if (turnstileSignals || url.includes('__cf_chl')) {
challengeType = 'turnstile';
} else if (realChallenge) {
challengeType = 'js_challenge';
}
const requireClearance = isCfClearanceRequired(antiBot);
let blocked;
if (requireClearance) {
blocked = !hasCfClearance && (realChallenge || turnstileSignals);
} else {
blocked = realChallenge || (turnstileSignals && urlChallenge);
}
return {
blocked,
challengeType,
hasCfClearance,
url,
title,
};
}
/**
* 检测 Turnstile 是否已通过
* @param {import('puppeteer').Page} page
* @returns {Promise<boolean>}
*/
async function isTurnstileSolved(page) {
const cookies = await page.cookies().catch(() => []);
if (cookies.some((c) => c.name === 'cf_clearance')) {
return true;
}
const tokenLen = await page.evaluate(() => {
const input = document.querySelector('[name="cf-turnstile-response"]');
return input && input.value ? input.value.length : 0;
}).catch(() => 0);
return tokenLen > 20;
}
module.exports = {
detectCloudflare,
isTurnstileSolved,
urlIndicatesCfChallenge,
isCfClearanceRequired,
sanitizePageUrlForCfSolver,
};