174 lines
6.0 KiB
JavaScript
174 lines
6.0 KiB
JavaScript
/**
|
||
* 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 AntiCloudflareTask(Managed 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_PROXY(A2C Managed Challenge)或提供有效 Turnstile sitekey');
|
||
}
|
||
|
||
module.exports = {
|
||
runCaptchaApiFallback,
|
||
shouldPreferManagedCloudflareSolver,
|
||
hasCaptchaProxy,
|
||
};
|