/** * Cloudflare / Turnstile 挑战页检测 * * antiBot.cfClearanceRequired === false(A2C 业务域):仅真实 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} */ 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} */ 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, };