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