230 lines
7.8 KiB
JavaScript
230 lines
7.8 KiB
JavaScript
/**
|
||
* Cloudflare Turnstile 验证处理器
|
||
* 通用的Cloudflare人机验证处理,支持全自动和半自动模式
|
||
*/
|
||
|
||
const logger = require('../../../shared/logger');
|
||
|
||
class CloudflareHandler {
|
||
/**
|
||
* @param {Page} page - Puppeteer page对象
|
||
* @param {HumanBehavior} human - 人类行为模拟对象
|
||
* @param {string} siteName - 网站名称(用于日志)
|
||
* @param {string} mode - 验证模式: 'auto' 或 'manual' (默认 'manual')
|
||
* @param {Function} customCheck - 自定义检测函数,返回Promise<boolean>,检测验证是否完成
|
||
*/
|
||
constructor(page, human, siteName, mode = 'manual', customCheck = null) {
|
||
this.page = page;
|
||
this.human = human;
|
||
this.siteName = siteName;
|
||
this.mode = mode; // 'auto' 或 'manual'
|
||
this.customCheck = customCheck; // 自定义检测函数
|
||
}
|
||
|
||
/**
|
||
* 处理Cloudflare Turnstile验证
|
||
* @returns {Promise<string>} 'passed' | 'failed' | 'manual'
|
||
*/
|
||
async handle() {
|
||
logger.info(this.siteName, '[Cloudflare] 检查人机验证...');
|
||
|
||
await this.human.randomDelay(2000, 3000);
|
||
|
||
// 检查页面内容,判断是否有验证
|
||
const pageText = await this.page.evaluate(() => document.body.textContent || '');
|
||
|
||
if (!pageText.includes('verify') && !pageText.includes('human')) {
|
||
logger.success(this.siteName, '[Cloudflare] 无验证,自动通过');
|
||
return 'passed';
|
||
}
|
||
|
||
logger.info(this.siteName, '[Cloudflare] 发现验证页面');
|
||
|
||
// 情况1:等待自动验证(某些情况下会自动通过)
|
||
logger.info(this.siteName, '[Cloudflare] 等待自动验证(5秒)...');
|
||
await this.human.randomDelay(5000, 6000);
|
||
|
||
const afterWait = await this.page.evaluate(() => document.body.textContent || '');
|
||
if (!afterWait.includes('verify') && !afterWait.includes('human')) {
|
||
logger.success(this.siteName, '[Cloudflare] 自动验证通过');
|
||
return 'passed';
|
||
}
|
||
|
||
// 情况2:需要点击验证框
|
||
if (this.mode === 'auto') {
|
||
return await this.autoHandle();
|
||
} else {
|
||
return await this.manualHandle();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 自动处理模式(实验性)
|
||
*/
|
||
async autoHandle() {
|
||
logger.info(this.siteName, '[Cloudflare] 自动模式:尝试点击验证框...');
|
||
|
||
try {
|
||
// 查找Turnstile iframe
|
||
const frames = await this.page.frames();
|
||
let turnstileFrame = null;
|
||
|
||
for (const frame of frames) {
|
||
const url = frame.url();
|
||
if (url.includes('challenges.cloudflare.com') || url.includes('turnstile')) {
|
||
turnstileFrame = frame;
|
||
logger.success(this.siteName, `[Cloudflare] 找到iframe: ${url.substring(0, 80)}...`);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!turnstileFrame) {
|
||
logger.warn(this.siteName, '[Cloudflare] 未找到Turnstile iframe');
|
||
return 'failed';
|
||
}
|
||
|
||
// 等待iframe加载
|
||
await turnstileFrame.waitForSelector('body', { timeout: 5000 });
|
||
await this.human.randomDelay(8000, 8000);
|
||
|
||
// 查找并点击checkbox
|
||
const checkboxElement = await turnstileFrame.$('label.cb-lb');
|
||
if (!checkboxElement) {
|
||
logger.error(this.siteName, '[Cloudflare] 未找到验证框');
|
||
return 'failed';
|
||
}
|
||
|
||
// 使用真实鼠标点击
|
||
const box = await checkboxElement.boundingBox();
|
||
if (box) {
|
||
const clickX = box.x + box.width / 2;
|
||
const clickY = box.y + box.height / 2;
|
||
|
||
await this.page.mouse.move(clickX - 50, clickY - 50);
|
||
await this.human.randomDelay(100, 300);
|
||
await this.page.mouse.move(clickX, clickY, { steps: 10 });
|
||
await this.human.randomDelay(200, 400);
|
||
await this.page.mouse.click(clickX, clickY);
|
||
|
||
logger.success(this.siteName, '[Cloudflare] 已点击验证框');
|
||
}
|
||
|
||
// 等待验证处理
|
||
await this.human.randomDelay(8000, 12000);
|
||
|
||
// 检查是否通过
|
||
const finalCheck = await this.page.evaluate(() => document.body.textContent || '');
|
||
if (!finalCheck.includes('verify') && !finalCheck.includes('human')) {
|
||
logger.success(this.siteName, '[Cloudflare] ✓ 验证通过');
|
||
return 'passed';
|
||
}
|
||
|
||
return 'failed';
|
||
} catch (error) {
|
||
logger.error(this.siteName, `[Cloudflare] 自动处理失败: ${error.message}`);
|
||
return 'failed';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 半自动处理模式(推荐)
|
||
*/
|
||
async manualHandle() {
|
||
logger.info(this.siteName, '[Cloudflare] 半自动模式:等待用户手动点击...');
|
||
|
||
try {
|
||
// 查找Turnstile iframe
|
||
const frames = await this.page.frames();
|
||
let turnstileFrame = null;
|
||
|
||
for (const frame of frames) {
|
||
const url = frame.url();
|
||
if (url.includes('challenges.cloudflare.com') || url.includes('turnstile')) {
|
||
turnstileFrame = frame;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (turnstileFrame) {
|
||
await turnstileFrame.waitForSelector('body', { timeout: 5000 });
|
||
}
|
||
|
||
// 显示提示
|
||
logger.warn(this.siteName, '='.repeat(60));
|
||
logger.warn(this.siteName, '[Cloudflare] 🖱️ 请手动点击验证框!');
|
||
logger.warn(this.siteName, '[Cloudflare] 程序将自动检测验证完成(超时10分钟)...');
|
||
logger.warn(this.siteName, '='.repeat(60));
|
||
|
||
// 轮询检查验证是否完成(最多10分钟)
|
||
let verified = false;
|
||
let attempts = 0;
|
||
const maxAttempts = 600; // 10分钟 = 600秒
|
||
|
||
while (!verified && attempts < maxAttempts) {
|
||
attempts++;
|
||
await this.human.randomDelay(1000, 1000);
|
||
|
||
// 方法1: 检查页面内容
|
||
const pageText = await this.page.evaluate(() => document.body.textContent || '');
|
||
const hasVerifyText = pageText.includes('verify') || pageText.includes('human');
|
||
|
||
// 方法2: 检查URL是否改变
|
||
const currentUrl = this.page.url();
|
||
const urlChanged = !currentUrl.includes('register');
|
||
|
||
// 方法3: 检查iframe内成功标志
|
||
let iframeSuccess = false;
|
||
if (turnstileFrame) {
|
||
try {
|
||
iframeSuccess = await turnstileFrame.evaluate(() => {
|
||
const successDiv = document.querySelector('#success');
|
||
return successDiv && successDiv.style.display !== 'none';
|
||
});
|
||
} catch (e) {
|
||
// iframe可能已消失
|
||
}
|
||
}
|
||
|
||
// 方法4: 自定义检测(由网站自己定义)
|
||
let customCheckPassed = false;
|
||
if (this.customCheck) {
|
||
try {
|
||
customCheckPassed = await this.customCheck();
|
||
} catch (e) {
|
||
// 忽略错误
|
||
}
|
||
}
|
||
|
||
// 每15秒输出一次状态
|
||
if (attempts % 15 === 0) {
|
||
const minutes = Math.floor(attempts / 60);
|
||
const seconds = attempts % 60;
|
||
logger.info(this.siteName, `[Cloudflare] 等待中... (${minutes}分${seconds}秒/${Math.floor(maxAttempts/60)}分钟) - custom=${customCheckPassed}, iframe=${iframeSuccess}`);
|
||
}
|
||
|
||
// 判断验证完成
|
||
if (customCheckPassed || iframeSuccess || (!hasVerifyText && attempts > 3) || urlChanged) {
|
||
verified = true;
|
||
logger.success(this.siteName, '[Cloudflare] ✓✓✓ 检测到验证完成!');
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!verified) {
|
||
logger.error(this.siteName, '[Cloudflare] ✗ 10分钟超时');
|
||
return 'failed';
|
||
}
|
||
|
||
// 验证完成后等待页面稳定
|
||
await this.human.randomDelay(2000, 3000);
|
||
return 'passed';
|
||
|
||
} catch (error) {
|
||
logger.error(this.siteName, `[Cloudflare] 半自动处理失败: ${error.message}`);
|
||
return 'failed';
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = CloudflareHandler;
|