import { spawnSync } from 'child_process'; import fs from 'fs'; import path from 'path'; import os from 'os'; import { Readable } from 'stream'; import vosk from 'vosk-koffi'; import wav from 'wav'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); vosk.setLogLevel(-1); const MODEL_DIR = path.resolve(__dirname, "model"); const model = new vosk.Model(MODEL_DIR); const SOURCE_FILE = "sound.mp3"; const OUT_FILE = "out.wav"; const MAIN_FRAME = "iframe[title='reCAPTCHA']"; const BFRAME = "iframe[src*='google.com/recaptcha'][src*='bframe']"; const CHALLENGE = "body > div > div"; class Mutex { constructor(name = "Mutex") { this.name = name; this._locked = false; this._queue = []; } async lock(tag) { if (this._locked) { if (tag) { console.log(`[${this.name}] ${tag} waiting`); } return new Promise((resolve) => { this._queue.push(() => { this._lock(tag); resolve(); }); }); } this._lock(tag); } unlock(tag) { if (this._locked) { if (tag) { console.log(`[${this.name}] ${tag} unlocked`); } this._locked = false; this._queue.shift()?.(); } } _lock(tag) { this._locked = true; if (tag) { console.log(`[${this.name}] ${tag} locked`); } } } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function createDir() { const dir = path.resolve(os.tmpdir(), "reSOLVER-" + Math.random().toString().slice(2)); if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true }); } fs.mkdirSync(dir, { recursive: true }); return dir; } function convert(dir, ffmpeg = "ffmpeg") { const args = [ "-loglevel", "error", "-i", SOURCE_FILE, "-acodec", "pcm_s16le", "-ac", "1", "-ar", "16000", OUT_FILE, ]; spawnSync(ffmpeg, args, { cwd: dir }); } function recognize(dir) { return new Promise((resolve) => { const stream = fs.createReadStream(path.resolve(dir, OUT_FILE), { highWaterMark: 4096 }); const reader = new wav.Reader(); const readable = new Readable().wrap(reader); reader.on("format", async ({ audioFormat, sampleRate, channels }) => { if (audioFormat != 1 || channels != 1) { throw new Error("Audio file must be WAV with mono PCM."); } const rec = new vosk.Recognizer({ model, sampleRate }); rec.setMaxAlternatives(10); rec.setWords(true); rec.setPartialWords(true); for await (const data of readable) { const end_of_speech = rec.acceptWaveform(data); if (end_of_speech) { const result = rec .result() .alternatives.sort((a, b) => b.confidence - a.confidence)[0].text; stream.close(() => resolve(result)); } } rec.free(); }); stream.pipe(reader); }); } async function getText(res, ffmpeg = "ffmpeg") { const tempDir = createDir(); fs.writeFileSync(path.resolve(tempDir, SOURCE_FILE), await res.body()); convert(tempDir, ffmpeg); const result = await recognize(tempDir); fs.rmSync(tempDir, { recursive: true }); return result; } async function getToken(page) { let token = null; try { token = await page.evaluate(() => { const textarea = document.querySelector('textarea[name="g-recaptcha-response"]'); if (textarea && textarea.value) { return textarea.value; } const textareaById = document.getElementById('g-recaptcha-response'); if (textareaById && textareaById.value) { return textareaById.value; } const allTextareas = document.querySelectorAll('textarea'); for (const ta of allTextareas) { if (ta.name === 'g-recaptcha-response' || ta.id === 'g-recaptcha-response') { if (ta.value) return ta.value; } } return null; }); } catch (error) { console.log("Error getting token:", error.message); } return token; } export async function solve(page, { delay = 64, wait = 5000, retry = 3, ffmpeg = "ffmpeg" } = {}) { console.log("Starting reCAPTCHA solver..."); try { await page.waitForSelector('iframe', { state: "attached", timeout: wait }); console.log("Found iframes on page"); } catch (error) { console.error("Error finding iframes:", error.message); throw new Error("No reCAPTCHA detected"); } let invisible = false; let mainFrame = null; try { await page.waitForSelector(MAIN_FRAME, { state: "attached", timeout: wait }); const iframe = await page.$(MAIN_FRAME); if (iframe === null) { throw new Error("Could not find reCAPTCHA iframe"); } mainFrame = await iframe.contentFrame(); if (mainFrame === null) { throw new Error("Could not find reCAPTCHA iframe content"); } invisible = (await mainFrame.$("div.rc-anchor-invisible")) ? true : false; console.log("Invisible reCAPTCHA:", invisible); if (!invisible) { const label = await mainFrame.$("#recaptcha-anchor-label"); if (label === null) { throw new Error("Could not find reCAPTCHA label"); } console.log("Clicking reCAPTCHA checkbox..."); await label.click(); await sleep(3000); let token = await getToken(page); if (token) { console.log("✓ Token generated without challenge:", token.substring(0, 50) + "..."); return { success: true, token }; } console.log("No immediate token, checking for challenge..."); } } catch (error) { console.error("Error handling main frame:", error.message); } let b_iframe = null; const bframeSelectors = [ "iframe[src*='/recaptcha/api2/bframe']", "iframe[src*='/recaptcha/enterprise/bframe']", "iframe[src*='bframe']" ]; await sleep(2000); for (const selector of bframeSelectors) { try { b_iframe = await page.$(selector); if (b_iframe) { console.log(`Found bframe with selector: ${selector}`); break; } } catch { continue; } } if (b_iframe === null) { console.log("No challenge bframe found, waiting for token..."); for (let i = 0; i < 15; i++) { await sleep(1000); const token = await getToken(page); if (token) { console.log("✓ Token found after waiting:", token.substring(0, 50) + "..."); return { success: true, token }; } console.log(`Waiting for token... (${i + 1}/15)`); } console.log("No token found after waiting"); return { success: true, token: null }; } const bframe = await b_iframe.contentFrame(); if (bframe === null) { throw new Error("Could not find reCAPTCHA popup iframe content"); } await sleep(1000); const challenge = await bframe.$(CHALLENGE); if (challenge === null) { console.log("No challenge element in bframe, waiting for token..."); for (let i = 0; i < 10; i++) { await sleep(1000); const token = await getToken(page); if (token) { console.log("✓ Token found:", token.substring(0, 50) + "..."); return { success: true, token }; } } return { success: true, token: null }; } const required = await challenge.evaluate( (elm) => !elm.classList.contains("rc-footer"), ); console.log("Audio/Image challenge required:", required); if (required === false) { console.log("No challenge required, waiting for token..."); for (let i = 0; i < 10; i++) { await sleep(1000); const token = await getToken(page); if (token) { console.log("✓ Token found:", token.substring(0, 50) + "..."); return { success: true, token }; } } return { success: true, token: null }; } await bframe.waitForSelector("#recaptcha-audio-button", { timeout: wait }); const audio_button = await bframe.$("#recaptcha-audio-button"); if (audio_button === null) { throw new Error("Could not find reCAPTCHA audio button"); } const mutex = new Mutex(); await mutex.lock("init"); let passed = false; let answer = Promise.resolve(""); const listener = async (res) => { if (res.headers()["content-type"] === "audio/mp3") { console.log(`Got audio from ${res.url()}`); answer = new Promise((resolve) => { getText(res, ffmpeg) .then(resolve) .catch(() => undefined); }); mutex.unlock("get sound"); } else if ( res.url().startsWith("https://www.google.com/recaptcha/api2/userverify") || res.url().startsWith("https://www.google.com/recaptcha/enterprise/userverify") ) { const raw = (await res.body()).toString().replace(")]}'\n", ""); const json = JSON.parse(raw); passed = json[2] === 1; mutex.unlock("verified"); } }; page.on("response", listener); await audio_button.click(); let tried = 0; while (passed === false) { if (tried++ >= retry) { throw new Error("Could not solve reCAPTCHA"); } await Promise.race([ mutex.lock("ready"), sleep(wait).then(() => { throw new Error("No Audio Found"); }), ]); await bframe.waitForSelector("#audio-source", { state: "attached", timeout: wait }); await bframe.waitForSelector("#audio-response", { timeout: wait }); console.log("Recognized text:", await answer); const input = await bframe.$("#audio-response"); if (input === null) { throw new Error("Could not find reCAPTCHA audio input"); } await input.type(await answer, { delay }); const button = await bframe.$("#recaptcha-verify-button"); if (button === null) { throw new Error("Could not find reCAPTCHA verify button"); } await button.click(); await mutex.lock("done"); console.log("Passed audio challenge:", passed); } page.off("response", listener); console.log("Audio challenge solved, waiting for token..."); await sleep(2000); for (let i = 0; i < 15; i++) { const token = await getToken(page); if (token) { console.log("✓ Token found after audio challenge:", token.substring(0, 50) + "..."); return { success: true, token }; } await sleep(1000); console.log(`Waiting for token after audio... (${i + 1}/15)`); } const finalToken = await getToken(page); console.log("Final token:", finalToken ? `${finalToken.substring(0, 50)}... (length: ${finalToken.length})` : "not found"); return { success: true, token: finalToken }; }