2026-06-20 04:47:34 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Cloudflare / Turnstile 挑战页检测
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-06-29 04:54:41 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 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');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 04:47:34 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @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
|
|
|
|
|
|
* @returns {Promise<CloudflareState>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function detectCloudflare(page) {
|
|
|
|
|
|
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'),
|
|
|
|
|
|
};
|
|
|
|
|
|
}).catch(() => ({
|
|
|
|
|
|
hasTurnstileInput: false,
|
|
|
|
|
|
hasTurnstileWidget: false,
|
|
|
|
|
|
hasChallengeRunning: false,
|
|
|
|
|
|
titleHasJustAMoment: false,
|
|
|
|
|
|
bodyHasJustAMoment: false,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2026-06-29 04:54:41 +08:00
|
|
|
|
const urlChallenge = urlIndicatesCfChallenge(url);
|
2026-06-20 04:47:34 +08:00
|
|
|
|
|
|
|
|
|
|
let challengeType = null;
|
2026-06-29 04:54:41 +08:00
|
|
|
|
if (domSignals.hasTurnstileInput || domSignals.hasTurnstileWidget || url.includes('__cf_chl')) {
|
2026-06-20 04:47:34 +08:00
|
|
|
|
challengeType = 'turnstile';
|
|
|
|
|
|
} else if (urlChallenge || domSignals.titleHasJustAMoment || domSignals.bodyHasJustAMoment) {
|
|
|
|
|
|
challengeType = 'js_challenge';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const blocked = !hasCfClearance && (
|
|
|
|
|
|
urlChallenge
|
|
|
|
|
|
|| domSignals.titleHasJustAMoment
|
|
|
|
|
|
|| domSignals.bodyHasJustAMoment
|
|
|
|
|
|
|| domSignals.hasTurnstileInput
|
|
|
|
|
|
|| domSignals.hasTurnstileWidget
|
|
|
|
|
|
|| domSignals.hasChallengeRunning
|
|
|
|
|
|
|| title.includes('Just a moment')
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
2026-06-29 04:54:41 +08:00
|
|
|
|
urlIndicatesCfChallenge,
|
2026-06-20 04:47:34 +08:00
|
|
|
|
};
|