291 lines
11 KiB
JavaScript
291 lines
11 KiB
JavaScript
const Tesseract = require('tesseract.js');
|
|
const https = require('https');
|
|
const http = require('http');
|
|
|
|
const TwoCaptcha = require('2captcha');
|
|
const solver = new TwoCaptcha.Solver('9ed0ef51badf9a017ac50aea413d8001');
|
|
|
|
const CAPTCHA_PATTERNS = {
|
|
recaptcha: {
|
|
selectors: ['.g-recaptcha', '[data-sitekey]', 'iframe[src*="recaptcha"]', '#recaptcha'],
|
|
name: 'reCAPTCHA'
|
|
},
|
|
hcaptcha: {
|
|
selectors: ['.h-captcha', '[data-hcaptcha-sitekey]', 'iframe[src*="hcaptcha"]', '#hcaptcha'],
|
|
name: 'hCaptcha'
|
|
},
|
|
textCaptcha: {
|
|
selectors: ['input[name*="captcha"]', 'input[id*="captcha"]', '.captcha-input', '#captcha'],
|
|
name: 'Text CAPTCHA'
|
|
},
|
|
imageCaptcha: {
|
|
selectors: ['img[src*="captcha"]', '.captcha-image', '#captcha-image', 'img[alt*="captcha" i]'],
|
|
name: 'Image CAPTCHA'
|
|
},
|
|
mathCaptcha: {
|
|
selectors: ['.math-captcha', '[name*="math"]', 'input[placeholder*="math" i]'],
|
|
name: 'Math CAPTCHA'
|
|
}
|
|
};
|
|
|
|
function fetchImageAsBuffer(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const client = url.startsWith('https') ? https : http;
|
|
client.get(url, (res) => {
|
|
const chunks = [];
|
|
res.on('data', (chunk) => chunks.push(chunk));
|
|
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
res.on('error', reject);
|
|
}).on('error', reject);
|
|
});
|
|
}
|
|
|
|
async function detectCaptcha(page) {
|
|
const captchaInfo = await page.evaluate((patterns) => {
|
|
const results = [];
|
|
|
|
for (const [type, config] of Object.entries(patterns)) {
|
|
for (const selector of config.selectors) {
|
|
const elements = document.querySelectorAll(selector);
|
|
if (elements.length > 0) {
|
|
for (const el of elements) {
|
|
const style = window.getComputedStyle(el);
|
|
const rect = el.getBoundingClientRect();
|
|
const isVisible = style.display !== 'none' &&
|
|
style.visibility !== 'hidden' &&
|
|
style.opacity !== '0' &&
|
|
rect.width > 0 &&
|
|
rect.height > 0;
|
|
|
|
results.push({
|
|
type: type,
|
|
name: config.name,
|
|
selector: selector,
|
|
count: elements.length,
|
|
isVisible: isVisible
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}, CAPTCHA_PATTERNS);
|
|
|
|
return captchaInfo;
|
|
}
|
|
|
|
async function solveSimpleCaptcha(page) {
|
|
const mathSolution = await page.evaluate(() => {
|
|
const mathText = document.querySelector('.math-captcha, .captcha-question, [class*="math"]');
|
|
if (mathText) {
|
|
const text = mathText.textContent || '';
|
|
const match = text.match(/(\d+)\s*([+\-*/])\s*(\d+)/);
|
|
if (match) {
|
|
const [_, a, op, b] = match;
|
|
let result;
|
|
switch(op) {
|
|
case '+': result = parseInt(a) + parseInt(b); break;
|
|
case '-': result = parseInt(a) - parseInt(b); break;
|
|
case '*': result = parseInt(a) * parseInt(b); break;
|
|
case '/': result = Math.floor(parseInt(a) / parseInt(b)); break;
|
|
}
|
|
return { type: 'math', answer: result, text: text };
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
|
|
if (mathSolution) {
|
|
return mathSolution;
|
|
}
|
|
|
|
const imageCaptchaInfo = await page.evaluate(() => {
|
|
const selectors = [
|
|
'img[src*="captcha"]',
|
|
'.captcha-image img',
|
|
'#captcha-image',
|
|
'img[alt*="captcha" i]',
|
|
'img[class*="captcha" i]'
|
|
];
|
|
|
|
for (const selector of selectors) {
|
|
const img = document.querySelector(selector);
|
|
if (img && img.src) {
|
|
return { src: img.src, selector: selector };
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
|
|
if (imageCaptchaInfo) {
|
|
console.log(' Attempting 2Captcha image solving...');
|
|
console.log(` CAPTCHA image selector: ${imageCaptchaInfo.selector}`);
|
|
|
|
// Wait for CAPTCHA image to fully load before capturing
|
|
console.log(' Waiting for CAPTCHA image to load...');
|
|
let imageLoaded = false;
|
|
const maxWaitMs = 5000;
|
|
const checkIntervalMs = 200;
|
|
let waitedMs = 0;
|
|
|
|
while (!imageLoaded && waitedMs < maxWaitMs) {
|
|
imageLoaded = await page.evaluate((selector) => {
|
|
const el = document.querySelector(selector);
|
|
return el && el.naturalWidth > 0 && el.naturalHeight > 0;
|
|
}, imageCaptchaInfo.selector).catch(() => false);
|
|
|
|
if (!imageLoaded) {
|
|
await new Promise(r => setTimeout(r, checkIntervalMs));
|
|
waitedMs += checkIntervalMs;
|
|
}
|
|
}
|
|
|
|
if (imageLoaded) {
|
|
console.log(` CAPTCHA image loaded successfully (waited ${waitedMs}ms)`);
|
|
} else {
|
|
console.log(' WARNING: CAPTCHA may not have fully loaded, proceeding anyway...');
|
|
}
|
|
|
|
// Additional small delay to ensure rendering is complete
|
|
await new Promise(r => setTimeout(r, 300));
|
|
|
|
try {
|
|
let imageData;
|
|
|
|
// Get CAPTCHA element bounding box
|
|
const captchaInfo = await page.evaluate((selector) => {
|
|
const img = document.querySelector(selector);
|
|
if (!img) return null;
|
|
const rect = img.getBoundingClientRect();
|
|
return {
|
|
src: img.src,
|
|
x: rect.x,
|
|
y: rect.y,
|
|
width: rect.width,
|
|
height: rect.height
|
|
};
|
|
}, imageCaptchaInfo.selector);
|
|
|
|
if (captchaInfo && captchaInfo.width > 0) {
|
|
console.log(' CAPTCHA at:', captchaInfo.x, captchaInfo.y, captchaInfo.width, 'x', captchaInfo.height);
|
|
|
|
// Try to get element screenshot directly
|
|
try {
|
|
const captchaElement = await page.$(imageCaptchaInfo.selector);
|
|
if (captchaElement) {
|
|
const elementScreenshot = await captchaElement.screenshot({ type: 'png' });
|
|
if (Buffer.isBuffer(elementScreenshot)) {
|
|
imageData = elementScreenshot.toString('base64');
|
|
} else {
|
|
imageData = Buffer.from(elementScreenshot).toString('base64');
|
|
}
|
|
console.log(' Element screenshot captured, size:', imageData.length, 'bytes (base64)');
|
|
}
|
|
} catch (elemError) {
|
|
console.log(' Element screenshot failed:', elemError.message);
|
|
|
|
// Fallback: capture full page and crop
|
|
const fullScreenshot = await page.screenshot({ type: 'png', encoding: 'binary' });
|
|
const screenshotBuffer = Buffer.from(fullScreenshot, 'binary');
|
|
const png = require('pngjs').PNG.sync.read(screenshotBuffer);
|
|
|
|
const cropX = Math.max(0, Math.floor(captchaInfo.x));
|
|
const cropY = Math.max(0, Math.floor(captchaInfo.y));
|
|
const cropW = Math.floor(captchaInfo.width);
|
|
const cropH = Math.floor(captchaInfo.height);
|
|
|
|
console.log(' Cropping to:', cropX, cropY, cropW, cropH);
|
|
|
|
const cropped = new require('pngjs').PNG({ width: cropW, height: cropH });
|
|
|
|
for (let y = 0; y < cropH; y++) {
|
|
for (let x = 0; x < cropW; x++) {
|
|
const srcIdx = ((cropY + y) * png.width + (cropX + x)) * 4;
|
|
const dstIdx = (y * cropW + x) * 4;
|
|
cropped.data[dstIdx] = png.data[srcIdx];
|
|
cropped.data[dstIdx + 1] = png.data[srcIdx + 1];
|
|
cropped.data[dstIdx + 2] = png.data[srcIdx + 2];
|
|
cropped.data[dstIdx + 3] = png.data[srcIdx + 3];
|
|
}
|
|
}
|
|
|
|
imageData = require('pngjs').PNG.sync.write(cropped).toString('base64');
|
|
console.log(' Cropped image size:', imageData.length, 'bytes (base64)');
|
|
}
|
|
} else if (captchaInfo && imageCaptchaInfo.src.startsWith('data:')) {
|
|
const base64 = imageCaptchaInfo.src.replace(/^data:image\/\w+;base64,/, '');
|
|
imageData = base64;
|
|
}
|
|
|
|
console.log(' Sending to 2Captcha (human workers)...');
|
|
const result = await solver.imageCaptcha(imageData, {
|
|
numeric: 0,
|
|
minLength: 1,
|
|
maxLength: 10,
|
|
phrase: 0,
|
|
caseSensitive: 1,
|
|
calc: 0,
|
|
lang: 'en'
|
|
});
|
|
|
|
console.log(` 2Captcha solved: "${result.data}"`);
|
|
return {
|
|
type: 'image',
|
|
answer: result.data.trim(),
|
|
originalText: result.data,
|
|
service: '2captcha',
|
|
imageSrc: imageCaptchaInfo.src
|
|
};
|
|
|
|
} catch (error) {
|
|
console.log(` 2Captcha failed: ${error.message}`);
|
|
|
|
console.log(' Falling back to Tesseract OCR...');
|
|
try {
|
|
const { data: { text, confidence } } = await Tesseract.recognize(
|
|
imageCaptchaInfo.src,
|
|
'eng',
|
|
{ logger: m => {} }
|
|
);
|
|
|
|
console.log(` OCR raw output: "${text}"`);
|
|
console.log(` OCR confidence: ${confidence}%`);
|
|
|
|
const cleanedText = text.replace(/[^a-zA-Z0-9]/g, '').trim();
|
|
|
|
if (cleanedText && cleanedText.length >= 3) {
|
|
console.log(` OCR fallback read: "${cleanedText}"`);
|
|
return {
|
|
type: 'image',
|
|
answer: cleanedText,
|
|
originalText: text,
|
|
confidence: confidence,
|
|
service: 'tesseract-fallback',
|
|
imageSrc: imageCaptchaInfo.src
|
|
};
|
|
}
|
|
} catch (ocrError) {
|
|
console.log(` OCR fallback also failed: ${ocrError.message}`);
|
|
}
|
|
|
|
return {
|
|
type: 'image',
|
|
answer: null,
|
|
message: `Both 2Captcha and OCR failed: ${error.message}`,
|
|
imageSrc: imageCaptchaInfo.src
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
module.exports = {
|
|
CAPTCHA_PATTERNS,
|
|
detectCaptcha,
|
|
solveSimpleCaptcha,
|
|
fetchImageAsBuffer
|
|
};
|