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,
|
|||
|
|
};
|