165 lines
5.1 KiB
JavaScript
Executable File
165 lines
5.1 KiB
JavaScript
Executable File
/**
|
||
* 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<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,
|
||
};
|