Files
newsletter_spammer/src/captcha.js
T

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