Files
2026-07-03 20:12:20 +08:00

302 lines
9.8 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.
/**
* 付费 Captcha API 兜底:CapSolver / 2Captcha 统一 Turnstile 求解接口
*/
const {
CAPTCHA_PROVIDER,
CAPTCHA_API_KEY,
CAPTCHA_MAX_WAIT_MS,
} = require('./constants');
const { sanitizePageUrlForCfSolver } = require('./cf-detector');
/** AntiCloudflareTask 所需静态代理(ip:port:user:pass),未配置则不可用 */
const CAPTCHA_PROXY = (process.env.CAPTCHA_PROXY || '').trim();
/**
* @param {string} url
* @param {Record<string, string>} [headers]
* @returns {Promise<any>}
*/
async function httpJsonPost(url, body, headers = {}) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body: JSON.stringify(body),
});
const text = await res.text();
try {
return JSON.parse(text);
} catch (_) {
throw new Error(`Captcha API 返回非 JSON: ${text.slice(0, 200)}`);
}
}
/**
* @param {string} url
* @returns {Promise<any>}
*/
async function httpJsonGet(url) {
const res = await fetch(url);
const text = await res.text();
try {
return JSON.parse(text);
} catch (_) {
throw new Error(`Captcha API 返回非 JSON: ${text.slice(0, 200)}`);
}
}
/**
* CapSolver Turnstile 求解
* @param {{ pageUrl: string, sitekey: string, apiKey: string }} params
* @returns {Promise<string>}
*/
async function solveViaCapSolver({ pageUrl, sitekey, apiKey }) {
const create = await httpJsonPost('https://api.capsolver.com/createTask', {
clientKey: apiKey,
task: {
type: 'AntiTurnstileTaskProxyLess',
websiteURL: pageUrl,
websiteKey: sitekey,
},
});
if (create.errorId !== 0 || !create.taskId) {
throw new Error(`CapSolver createTask 失败: ${create.errorDescription || create.errorCode || 'unknown'}`);
}
const deadline = Date.now() + CAPTCHA_MAX_WAIT_MS;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 3000));
const result = await httpJsonPost('https://api.capsolver.com/getTaskResult', {
clientKey: apiKey,
taskId: create.taskId,
});
if (result.status === 'ready' && result.solution && result.solution.token) {
return result.solution.token;
}
if (result.errorId !== 0) {
throw new Error(`CapSolver getTaskResult 失败: ${result.errorDescription || result.errorCode}`);
}
}
throw new Error('CapSolver 求解超时');
}
/**
* 2Captcha Turnstile 求解
* @param {{ pageUrl: string, sitekey: string, apiKey: string }} params
* @returns {Promise<string>}
*/
async function solveVia2Captcha({ pageUrl, sitekey, apiKey }) {
const params = new URLSearchParams({
key: apiKey,
method: 'turnstile',
sitekey,
pageurl: pageUrl,
json: '1',
});
const create = await httpJsonGet(`https://2captcha.com/in.php?${params.toString()}`);
if (create.status !== 1 || !create.request) {
throw new Error(`2Captcha in.php 失败: ${create.request || create.error_text || 'unknown'}`);
}
const taskId = create.request;
const deadline = Date.now() + CAPTCHA_MAX_WAIT_MS;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 5000));
const result = await httpJsonGet(
`https://2captcha.com/res.php?key=${encodeURIComponent(apiKey)}&action=get&id=${encodeURIComponent(taskId)}&json=1`
);
if (result.status === 1 && result.request) {
return result.request;
}
if (result.request && result.request !== 'CAPCHA_NOT_READY') {
throw new Error(`2Captcha res.php 失败: ${result.request}`);
}
}
throw new Error('2Captcha 求解超时');
}
/**
* CapSolver Managed Challenge(返回 cf_clearance cookie,需 CAPTCHA_PROXY
* @param {{ pageUrl: string, apiKey: string, proxy: string, html?: string, userAgent?: string }} params
* @returns {Promise<{ cookies: Record<string, string>, userAgent?: string }>}
*/
async function solveViaCapSolverCloudflare({ pageUrl, apiKey, proxy, html, userAgent }) {
const task = {
type: 'AntiCloudflareTask',
websiteURL: pageUrl,
proxy,
};
if (html) {
task.html = html.slice(0, 500000);
}
if (userAgent) {
task.userAgent = userAgent;
}
const create = await httpJsonPost('https://api.capsolver.com/createTask', {
clientKey: apiKey,
task,
});
if (create.errorId !== 0 || !create.taskId) {
throw new Error(`CapSolver AntiCloudflare createTask 失败: ${create.errorDescription || create.errorCode || 'unknown'}`);
}
const deadline = Date.now() + CAPTCHA_MAX_WAIT_MS;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 3000));
const result = await httpJsonPost('https://api.capsolver.com/getTaskResult', {
clientKey: apiKey,
taskId: create.taskId,
});
if (result.status === 'ready' && result.solution) {
const cookies = result.solution.cookies || {};
if (typeof cookies === 'string') {
return { cookies: { cf_clearance: cookies }, userAgent: result.solution.userAgent };
}
if (result.solution.token && !cookies.cf_clearance) {
return {
cookies: { cf_clearance: result.solution.token },
userAgent: result.solution.userAgent,
};
}
return {
cookies,
userAgent: result.solution.userAgent,
};
}
if (result.errorId !== 0) {
throw new Error(`CapSolver AntiCloudflare getTaskResult 失败: ${result.errorDescription || result.errorCode}`);
}
}
throw new Error('CapSolver AntiCloudflare 求解超时');
}
/**
* 统一 Cloudflare Managed Challenge 求解
* @param {{ pageUrl: string, provider?: string, apiKey?: string, proxy?: string, html?: string, userAgent?: string }} params
* @returns {Promise<{ cookies: Record<string, string>, userAgent?: string, provider: string }>}
*/
async function solveCloudflare({ pageUrl, provider, apiKey, proxy, html, userAgent }) {
const resolvedProvider = (provider || CAPTCHA_PROVIDER).toLowerCase();
const resolvedKey = apiKey || CAPTCHA_API_KEY;
const resolvedProxy = (proxy || CAPTCHA_PROXY).trim();
const cleanPageUrl = sanitizePageUrlForCfSolver(pageUrl);
if (!resolvedKey) {
throw new Error('未配置 CAPTCHA_API_KEY,无法使用 AntiCloudflareTask');
}
if (!resolvedProxy) {
throw new Error('AntiCloudflareTask 需要配置 CAPTCHA_PROXY(静态代理 ip:port:user:pass');
}
if (resolvedProvider !== 'capsolver') {
throw new Error('AntiCloudflareTask 当前仅支持 CapSolver');
}
const { cookies, userAgent: solvedUa } = await solveViaCapSolverCloudflare({
pageUrl: cleanPageUrl,
apiKey: resolvedKey,
proxy: resolvedProxy,
html,
userAgent,
});
if (!cookies || !cookies.cf_clearance) {
throw new Error('CapSolver AntiCloudflare 未返回 cf_clearance');
}
return { cookies, userAgent: solvedUa || userAgent, provider: resolvedProvider };
}
/**
* 将 CapSolver AntiCloudflare 返回的 cookie / UA 写入页面并 reload
* @param {import('puppeteer').Page} page
* @param {string} cleanUrl
* @param {{ cookies: Record<string, string>, userAgent?: string }} solution
*/
async function applyCfClearanceSolution(page, cleanUrl, solution) {
if (solution.userAgent) {
await page.setUserAgent(solution.userAgent).catch(() => {});
}
let host = '';
let path = '/';
try {
const parsed = new URL(cleanUrl);
host = parsed.hostname;
path = parsed.pathname || '/';
} catch (_) {
host = '';
}
for (const [name, value] of Object.entries(solution.cookies || {})) {
if (!value) {
continue;
}
const cookieDomain = host.startsWith('.') ? host : `.${host}`;
await page.setCookie({
name,
value: String(value),
domain: cookieDomain,
path: '/',
secure: true,
sameSite: 'None',
}).catch(() => {});
}
console.log(`[CF] cf_clearance 已注入,goto 业务页 url=${cleanUrl}`);
await page.goto(cleanUrl, {
waitUntil: 'domcontentloaded',
timeout: 60000,
}).catch(() => {});
await page.waitForNetworkIdle({ idleTime: 500, timeout: 30000 }).catch(() => {
console.warn('[CF] cf_clearance goto 后 networkidle 超时,继续后续流程');
});
}
/**
* 统一 Turnstile 求解入口
* @param {{ pageUrl: string, sitekey: string, provider?: string, apiKey?: string }} params
* @returns {Promise<{ token: string, provider: string }>}
*/
async function solveTurnstile({ pageUrl, sitekey, provider, apiKey }) {
const resolvedProvider = (provider || CAPTCHA_PROVIDER).toLowerCase();
const resolvedKey = apiKey || CAPTCHA_API_KEY;
const cleanPageUrl = sanitizePageUrlForCfSolver(pageUrl);
if (!sitekey) {
throw new Error('无法提取 Turnstile sitekey');
}
if (!resolvedKey) {
throw new Error('未配置 CAPTCHA_API_KEY,无法使用付费 Captcha 兜底');
}
let token;
if (resolvedProvider === '2captcha') {
token = await solveVia2Captcha({ pageUrl: cleanPageUrl, sitekey, apiKey: resolvedKey });
} else {
token = await solveViaCapSolver({ pageUrl: cleanPageUrl, sitekey, apiKey: resolvedKey });
}
return { token, provider: resolvedProvider };
}
/**
* Captcha API 是否已配置
* @returns {boolean}
*/
function isCaptchaConfigured() {
return CAPTCHA_API_KEY.length > 0;
}
module.exports = {
solveTurnstile,
solveCloudflare,
applyCfClearanceSolution,
isCaptchaConfigured,
};