Files

302 lines
9.8 KiB
JavaScript
Raw Permalink Normal View History

2026-06-20 04:47:34 +08:00
/**
* 付费 Captcha API 兜底:CapSolver / 2Captcha 统一 Turnstile 求解接口
*/
const {
CAPTCHA_PROVIDER,
CAPTCHA_API_KEY,
CAPTCHA_MAX_WAIT_MS,
} = require('./constants');
2026-07-03 20:12:20 +08:00
const { sanitizePageUrlForCfSolver } = require('./cf-detector');
/** AntiCloudflareTask 所需静态代理(ip:port:user:pass),未配置则不可用 */
const CAPTCHA_PROXY = (process.env.CAPTCHA_PROXY || '').trim();
2026-06-20 04:47:34 +08:00
/**
* @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 求解超时');
}
2026-07-03 20:12:20 +08:00
/**
* 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 超时,继续后续流程');
});
}
2026-06-20 04:47:34 +08:00
/**
* 统一 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;
2026-07-03 20:12:20 +08:00
const cleanPageUrl = sanitizePageUrlForCfSolver(pageUrl);
2026-06-20 04:47:34 +08:00
if (!sitekey) {
throw new Error('无法提取 Turnstile sitekey');
}
if (!resolvedKey) {
throw new Error('未配置 CAPTCHA_API_KEY,无法使用付费 Captcha 兜底');
}
let token;
if (resolvedProvider === '2captcha') {
2026-07-03 20:12:20 +08:00
token = await solveVia2Captcha({ pageUrl: cleanPageUrl, sitekey, apiKey: resolvedKey });
2026-06-20 04:47:34 +08:00
} else {
2026-07-03 20:12:20 +08:00
token = await solveViaCapSolver({ pageUrl: cleanPageUrl, sitekey, apiKey: resolvedKey });
2026-06-20 04:47:34 +08:00
}
return { token, provider: resolvedProvider };
}
/**
* Captcha API 是否已配置
* @returns {boolean}
*/
function isCaptchaConfigured() {
return CAPTCHA_API_KEY.length > 0;
}
module.exports = {
solveTurnstile,
2026-07-03 20:12:20 +08:00
solveCloudflare,
applyCfClearanceSolution,
2026-06-20 04:47:34 +08:00
isCaptchaConfigured,
};