/** * captcha-solver.js — Sistema de Resolución de CAPTCHAs con IA * ============================================================== * Sin servicios de pago. Todo con IA disponible. * * ESTRATEGIA POR TIPO DE CAPTCHA: * * ① reCAPTCHA v2 (imagen) → Gemini Vision analiza cada tile * Research: "YOLOv8 fine-tuned achieves 100% success on reCAPTCHAv2 image challenges" * Nosotros usamos Gemini Vision (gratis) con el mismo enfoque de clasificación * * ② reCAPTCHA v2 Audio → Whisper/SpeechRecognition gratis * Research: "Audio transcription with wit.ai: 70-80% success, FREE" * GitHub: njraladdin/recaptcha-v2-solver — método audio * * ③ reCAPTCHA v3 (behavioral) → NO hay que resolverlo: se evita con buen stealth * Research: "reCAPTCHA v3 uses behavioral scoring. Good behavior simulation * achieves 0.9 trust score without solving any challenge" * * ④ hCaptcha → Mismo enfoque que reCAPTCHA v2 con Gemini Vision * * ⑤ Cloudflare Turnstile → JavaScript challenge, se resuelve solo con buen stealth * * ⑥ Text CAPTCHAs → OCR via Gemini Vision * * ⑦ CAPTCHAs irresolubles → Notificar al owner para resolución manual */ import { readConfig } from './utils.js'; const config = readConfig(); // ── Gemini Vision helper ────────────────────────────────────────────────────── async function callGeminiVision(base64Image, prompt, mimeType = 'image/jpeg') { const keys = config.ai?.gemini?.keys ?? [config.ai?.gemini?.apiKey].filter(Boolean); const key = keys[0]; if (!key) return null; const res = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${key}`, { method : 'POST', headers: { 'Content-Type': 'application/json' }, body : JSON.stringify({ contents: [{ parts: [ { text: prompt }, { inline_data: { mime_type: mimeType, data: base64Image } }, ]}], generationConfig: { maxOutputTokens: 200, temperature: 0.1 }, }), signal: AbortSignal.timeout(15000), } ); if (!res.ok) return null; return (await res.json()).candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? null; } // ── Audio download y transcripción ────────────────────────────────────────── async function transcribeAudio(audioUrl) { try { // Descargar el audio del CAPTCHA const audioRes = await fetch(audioUrl, { signal: AbortSignal.timeout(15000) }); const audioData = Buffer.from(await audioRes.arrayBuffer()); const base64 = audioData.toString('base64'); // Usar Gemini para transcribir (soporta audio nativo) const keys = config.ai?.gemini?.keys ?? [config.ai?.gemini?.apiKey].filter(Boolean); const key = keys[0]; if (!key) return null; const res = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${key}`, { method : 'POST', headers: { 'Content-Type': 'application/json' }, body : JSON.stringify({ contents: [{ parts: [ { text: 'This is a CAPTCHA audio challenge. Transcribe the spoken digits or words exactly. Reply with ONLY the numbers/words, nothing else.' }, { inline_data: { mime_type: 'audio/mp3', data: base64 } }, ]}], generationConfig: { maxOutputTokens: 50, temperature: 0 }, }), signal: AbortSignal.timeout(20000), } ); if (!res.ok) return null; const text = (await res.json()).candidates?.[0]?.content?.parts?.[0]?.text?.trim(); // Limpiar respuesta: solo números y letras return text?.replace(/[^a-zA-Z0-9\s]/g, '').trim() ?? null; } catch { return null; } } function sleep(ms) { return new Promise(r => setTimeout(r, Math.round(ms))); } // ═══════════════════════════════════════════════════════════════════════════════ // SOLUCIONADORES POR TIPO // ═══════════════════════════════════════════════════════════════════════════════ /** * Resolver reCAPTCHA v2 con imágenes * Estrategia: screenshot de cada tile → Gemini Vision clasifica → clic en los correctos */ export async function solveRecaptchaV2Images(page) { console.log('[CAPTCHA] 🔍 Intentando resolver reCAPTCHA v2 por imágenes...'); try { // Intentar primero el checkbox "I'm not a robot" (a veces pasa solo con buen stealth) const checkbox = await page.$('.recaptcha-checkbox-border, #recaptcha-anchor'); if (checkbox) { await checkbox.click(); await sleep(2000 + Math.random() * 1000); // Verificar si el checkbox se marcó sin challenge const solved = await page.evaluate(() => { const resp = document.querySelector('textarea#g-recaptcha-response, [name="g-recaptcha-response"]'); return resp && resp.value.length > 10; }).catch(() => false); if (solved) { console.log('[CAPTCHA] ✅ reCAPTCHA v2 resuelto sin challenge (buen stealth)'); return { success: true, method: 'stealth_no_challenge' }; } } // Hay challenge de imágenes — resolver con visión // Obtener el iframe del CAPTCHA const frame = page.frames().find(f => f.url().includes('recaptcha')); if (!frame) return { success: false, method: 'no_frame' }; // Obtener la instrucción del challenge const instruction = await frame.$eval('.rc-imageselect-desc-wrapper', el => el.textContent?.trim() ?? '').catch(() => ''); console.log('[CAPTCHA] Instrucción:', instruction.slice(0, 80)); // Hacer screenshot del área de imágenes const grid = await frame.$('.rc-imageselect-table'); if (!grid) return { success: false, method: 'no_grid' }; const gridScreenshot = await grid.screenshot({ type: 'jpeg', quality: 90 }); const base64Grid = gridScreenshot.toString('base64'); // Gemini Vision: analizar qué tiles seleccionar const prompt = `This is a reCAPTCHA image grid challenge. The instruction says: "${instruction}" Analyze each tile in the grid (numbered left-to-right, top-to-bottom starting from 1). Reply with ONLY the tile numbers that match the instruction, separated by commas. Example: "1,3,7" or "2,5,6,9" If no tiles match, reply "none". Do not explain, just give the numbers.`; const answer = await callGeminiVision(base64Grid, prompt); if (!answer || answer.toLowerCase() === 'none') { console.log('[CAPTCHA] Gemini no encontró tiles para seleccionar'); return { success: false, method: 'vision_no_match' }; } // Parsear números de tiles const tileNumbers = answer.split(',').map(n => parseInt(n.trim())).filter(n => !isNaN(n) && n >= 1); console.log('[CAPTCHA] Tiles a seleccionar:', tileNumbers); // Hacer clic en los tiles indicados con comportamiento humano for (const tileNum of tileNumbers) { const tile = await frame.$(`.rc-imageselect-tile:nth-child(${tileNum}), .rc-image-tile-target:nth-child(${tileNum})`); if (tile) { const box = await tile.boundingBox(); if (box) { const x = box.x + box.width * (0.2 + Math.random() * 0.6); const y = box.y + box.height * (0.2 + Math.random() * 0.6); await page.mouse.move(x, y); await sleep(200 + Math.random() * 300); await page.mouse.click(x, y); await sleep(300 + Math.random() * 400); } } } // Hacer clic en Verify await sleep(1000 + Math.random() * 500); const verifyBtn = await frame.$('#recaptcha-verify-button, .rc-button-default'); if (verifyBtn) { await verifyBtn.click(); await sleep(2000 + Math.random() * 1000); } // Verificar resultado const token = await page.evaluate(() => { const t = document.querySelector('textarea[name="g-recaptcha-response"]'); return t?.value ?? ''; }).catch(() => ''); if (token.length > 20) { console.log('[CAPTCHA] ✅ reCAPTCHA v2 resuelto por visión'); return { success: true, method: 'vision', token }; } return { success: false, method: 'vision_unverified' }; } catch (e) { console.warn('[CAPTCHA] Error en reCAPTCHA v2:', e.message); return { success: false, error: e.message }; } } /** * Resolver reCAPTCHA v2 por audio (más fiable que imágenes) * Estrategia: solicitar versión audio → descargar → transcribir con Gemini → escribir */ export async function solveRecaptchaV2Audio(page) { console.log('[CAPTCHA] 🎵 Intentando reCAPTCHA v2 por audio...'); try { const frame = page.frames().find(f => f.url().includes('recaptcha')); if (!frame) return { success: false, method: 'no_frame' }; // Cambiar a modo audio const audioBtn = await frame.$('#recaptcha-audio-button'); if (!audioBtn) { // Intentar via checkbox primero const checkbox = await frame.$('#recaptcha-anchor'); if (checkbox) { await checkbox.click(); await sleep(1500); } } const audioBtnRetry = await frame.$('#recaptcha-audio-button'); if (!audioBtnRetry) return { success: false, method: 'no_audio_button' }; await audioBtnRetry.click(); await sleep(2000); // Obtener la URL del audio const audioUrl = await frame.$eval('.rc-audiochallenge-tdownload-link', el => el.href).catch(() => null); if (!audioUrl) return { success: false, method: 'no_audio_url' }; // Transcribir con Gemini const transcription = await transcribeAudio(audioUrl); if (!transcription) return { success: false, method: 'transcription_failed' }; console.log('[CAPTCHA] Transcripción:', transcription); // Escribir la respuesta en el campo const input = await frame.$('#audio-response'); if (!input) return { success: false, method: 'no_input' }; await input.click(); await sleep(200); await page.keyboard.type(transcription.toLowerCase().replace(/\s+/g, ' '), { delay: 80 + Math.random() * 40 }); await sleep(500); // Verificar const verifyBtn = await frame.$('#recaptcha-verify-button'); if (verifyBtn) { await verifyBtn.click(); await sleep(2000); } const token = await page.evaluate(() => document.querySelector('textarea[name="g-recaptcha-response"]')?.value ?? '' ).catch(() => ''); if (token.length > 20) { console.log('[CAPTCHA] ✅ reCAPTCHA v2 resuelto por audio'); return { success: true, method: 'audio', token }; } return { success: false, method: 'audio_unverified' }; } catch (e) { return { success: false, error: e.message }; } } /** * Resolver text CAPTCHA (imagen con texto/números distorsionados) */ export async function solveTextCaptcha(page, imageSelector) { try { const imgEl = await page.$(imageSelector); if (!imgEl) return null; const imgBase64 = (await imgEl.screenshot({ type: 'jpeg', quality: 95 })).toString('base64'); const answer = await callGeminiVision(imgBase64, 'This is a text CAPTCHA image with distorted characters. Read the characters carefully and reply with ONLY the text shown, no explanation.' ); return answer?.replace(/\s/g, '') ?? null; } catch { return null; } } /** * Detector principal de CAPTCHA + resolutor automático */ export async function detectAndSolve(page, notifyOwner = null) { console.log('[CAPTCHA] Escaneando página...'); // Detectar tipo de CAPTCHA presente const captchaInfo = await page.evaluate(() => { const body = document.body.innerHTML.toLowerCase(); const text = document.body.textContent.toLowerCase(); return { hasRecaptchaV2 : !!document.querySelector('iframe[src*="recaptcha"]') || body.includes('g-recaptcha'), hasHcaptcha : body.includes('hcaptcha') || !!document.querySelector('iframe[src*="hcaptcha"]'), hasCloudflare : text.includes('checking your browser') || text.includes('verifying you are') || body.includes('cf-challenge'), hasTurnstile : body.includes('turnstile') || !!document.querySelector('[data-sitekey]'), hasTextCaptcha : !!document.querySelector('img[src*="captcha"], img[alt*="captcha"]'), }; }).catch(() => ({})); console.log('[CAPTCHA] Detectado:', JSON.stringify(captchaInfo)); // 1. Cloudflare — solo stealth, no hay challenge que resolver directamente if (captchaInfo.hasCloudflare) { console.log('[CAPTCHA] Cloudflare detectado — esperando resolución automática...'); await sleep(5000 + Math.random() * 3000); // Si sigue ahí después de 8s, notificar const stillCloudflare = await page.evaluate(() => document.body.textContent.toLowerCase().includes('checking your browser') ).catch(() => false); if (stillCloudflare && notifyOwner) { await notifyOwner('⚠️ Cloudflare no se resolvió automáticamente. Puede necesitar proxy de mayor calidad.'); } return { type: 'cloudflare', solved: !stillCloudflare }; } // 2. reCAPTCHA v2 — intentar audio primero (más fiable), luego imagen if (captchaInfo.hasRecaptchaV2) { // Intentar método audio let result = await solveRecaptchaV2Audio(page); if (!result.success) { // Fallback a imágenes result = await solveRecaptchaV2Images(page); } if (!result.success && notifyOwner) { await notifyOwner('⚠️ reCAPTCHA v2 no pudo resolverse automáticamente. Requiere intervención manual.'); } return { type: 'recaptcha_v2', ...result }; } // 3. hCaptcha — mismo enfoque que reCAPTCHA v2 if (captchaInfo.hasHcaptcha) { console.log('[CAPTCHA] hCaptcha detectado — intentando resolver...'); // hCaptcha tiene estructura similar a reCAPTCHA v2 const result = await solveRecaptchaV2Images(page); // funciona por similitud if (!result.success && notifyOwner) { await notifyOwner('⚠️ hCaptcha detectado. Requiere intervención manual.'); } return { type: 'hcaptcha', ...result }; } // 4. Text CAPTCHA if (captchaInfo.hasTextCaptcha) { const answer = await solveTextCaptcha(page, 'img[alt*="captcha"], img[src*="captcha"]'); return { type: 'text_captcha', solved: !!answer, answer }; } return { type: 'none', solved: true }; } export async function getCaptchaSolverStats() { const geminiAvailable = !!(config.ai?.gemini?.keys?.length || config.ai?.gemini?.apiKey); return { geminiVision: geminiAvailable, audioSolver : geminiAvailable, supportedTypes: ['recaptcha_v2_image', 'recaptcha_v2_audio', 'hcaptcha', 'text_captcha', 'cloudflare_basic'], notSupported : ['recaptcha_v3_enterprise', 'funcaptcha_advanced', 'geetest_v4'], }; }