Files

174 lines
6.0 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.
/**
* CapSolver / 2Captcha 过盾兜底:Turnstile token 注入 或 AntiCloudflareTask cf_clearance
*
* A2C 等业务域为 CF Managed Challenge(非独立 Turnstile 挂件),须优先 AntiCloudflareTask。
*/
const { sanitizePageUrlForCfSolver } = require('./cf-detector');
const {
injectTurnstileToken,
recoverAfterTokenInject,
extractTurnstileSitekey,
} = require('./turnstile-handler');
const {
solveTurnstile,
solveCloudflare,
applyCfClearanceSolution,
} = require('./captcha-solver');
const { getSitekeyCapture } = require('./sitekey-capture');
/** @returns {boolean} */
function hasCaptchaProxy() {
return (process.env.CAPTCHA_PROXY || '').trim() !== '';
}
/**
* 是否应使用 CapSolver AntiCloudflareTaskManaged Challenge / 5s 盾)
* @param {object|null|undefined} antiBot
*/
function shouldPreferManagedCloudflareSolver(antiBot) {
if (!antiBot || antiBot.cfCloudflareSolver === false) {
return false;
}
if (antiBot.cfChallengeMode === 'managed') {
return hasCaptchaProxy();
}
if (antiBot.cfCloudflareSolver === true && hasCaptchaProxy()) {
return true;
}
return false;
}
/**
* 解析可用于 AntiTurnstileTask 的 sitekey(排除 A2C 配置的 challenge sitekey
* @param {import('puppeteer').Page} page
* @param {object} antiBot
* @param {number} waitMs
* @returns {Promise<string|null>}
*/
async function resolveTurnstileWidgetSitekey(page, antiBot, waitMs) {
if (antiBot.cfChallengeMode === 'managed') {
return null;
}
const capture = getSitekeyCapture(page);
if (capture) {
const fromCapture = await capture.resolveForSolver(page, antiBot, waitMs);
if (fromCapture && antiBot.cfChallengeMode !== 'managed') {
const configured = String(antiBot.turnstileSitekey || '').trim();
if (configured && fromCapture === configured && antiBot.cfCloudflareSolver) {
return null;
}
return fromCapture;
}
}
const dynamic = await extractTurnstileSitekey(page);
if (dynamic) {
return dynamic;
}
const configured = String(antiBot?.turnstileSitekey || '').trim();
if (configured && !antiBot.cfCloudflareSolver) {
return configured;
}
return null;
}
/**
* CapSolver AntiCloudflareTask 求解并注入 cf_clearance
* @param {import('puppeteer').Page} page
* @param {object} params
*/
async function runManagedCloudflareSolver(page, params) {
const { cleanSolverUrl, waitForUrlSettled, timeoutMs } = params;
if (!hasCaptchaProxy()) {
throw new Error('AntiCloudflareTask 需要配置 CAPTCHA_PROXY(静态代理 ip:port:user:pass');
}
console.log('[CF] A2C/Managed Challenge:使用 CapSolver AntiCloudflareTask');
const html = await page.content().catch(() => '');
const userAgent = await page.evaluate(() => navigator.userAgent).catch(() => '');
const { cookies, userAgent: solvedUa, provider } = await solveCloudflare({
pageUrl: cleanSolverUrl,
html,
userAgent,
});
console.log(`[CF] CapSolver AntiCloudflareTask (${provider}) 返回 cf_clearance`);
await applyCfClearanceSolution(page, cleanSolverUrl, { cookies, userAgent: solvedUa || userAgent });
if (typeof waitForUrlSettled === 'function') {
await waitForUrlSettled(page).catch(() => {});
}
return { mode: 'cloudflare', provider };
}
/**
* @param {import('puppeteer').Page} page
* @param {object} options
* @param {object} options.antiBot
* @param {string} [options.navUrl]
* @param {(page: import('puppeteer').Page) => Promise<string>} [options.waitForUrlSettled]
* @param {number} [options.timeoutMs]
* @returns {Promise<{ mode: 'turnstile'|'cloudflare', provider: string }>}
*/
async function runCaptchaApiFallback(page, options) {
const {
antiBot,
navUrl = '',
waitForUrlSettled,
timeoutMs = 60000,
} = options;
const cleanSolverUrl = sanitizePageUrlForCfSolver(navUrl || page.url());
if (shouldPreferManagedCloudflareSolver(antiBot)) {
console.log(`[CF] Captcha API 求解 pageUrl=${cleanSolverUrl} mode=managed_challenge`);
return runManagedCloudflareSolver(page, { cleanSolverUrl, waitForUrlSettled, timeoutMs });
}
const sitekey = await resolveTurnstileWidgetSitekey(
page,
antiBot,
Math.min(timeoutMs, 25000)
);
console.log(`[CF] Captcha API 求解 pageUrl=${cleanSolverUrl} sitekey=${sitekey ? 'ok' : 'missing'}`);
if (sitekey) {
try {
const { token, provider } = await solveTurnstile({ pageUrl: cleanSolverUrl, sitekey });
console.log(`[CF] Captcha API Turnstile (${provider}) 返回 token,注入页面`);
await injectTurnstileToken(page, token);
await recoverAfterTokenInject(
page,
cleanSolverUrl,
waitForUrlSettled,
Math.min(timeoutMs, 45000)
);
return { mode: 'turnstile', provider };
} catch (turnstileErr) {
const msg = (turnstileErr?.message || '').toLowerCase();
const isChallengeMismatch = msg.includes('challenge, not turnstile')
|| msg.includes('not turnstile');
if (isChallengeMismatch && antiBot.cfCloudflareSolver !== false && hasCaptchaProxy()) {
console.warn(`[CF] Turnstile 任务失败(${turnstileErr.message}),降级 AntiCloudflareTask`);
return runManagedCloudflareSolver(page, { cleanSolverUrl, waitForUrlSettled, timeoutMs });
}
throw turnstileErr;
}
}
if (antiBot?.cfCloudflareSolver !== false && hasCaptchaProxy()) {
return runManagedCloudflareSolver(page, { cleanSolverUrl, waitForUrlSettled, timeoutMs });
}
throw new Error('无法过盾:请配置 CAPTCHA_PROXYA2C Managed Challenge)或提供有效 Turnstile sitekey');
}
module.exports = {
runCaptchaApiFallback,
shouldPreferManagedCloudflareSolver,
hasCaptchaProxy,
};