| 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; |
| } |
|
|
| 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"); |
| |
| const allIframes = await page.$$('iframe'); |
| console.log(`Total iframes found: ${allIframes.length}`); |
| |
| for (let i = 0; i < allIframes.length; i++) { |
| const src = await allIframes[i].getAttribute('src'); |
| const title = await allIframes[i].getAttribute('title'); |
| console.log(`Iframe ${i}: src=${src}, title=${title}`); |
| } |
| } catch (error) { |
| console.error("Error finding iframes:", error.message); |
| throw new Error("No reCAPTCHA detected"); |
| } |
|
|
| let invisible = false; |
|
|
| let b_iframe = null; |
| const bframeSelectors = [ |
| "iframe[src*='/recaptcha/api2/bframe']", |
| "iframe[src*='/recaptcha/enterprise/bframe']", |
| "iframe[src*='bframe']" |
| ]; |
| |
| for (const selector of bframeSelectors) { |
| try { |
| await page.waitForSelector(selector, { state: "attached", timeout: 2000 }); |
| b_iframe = await page.$(selector); |
| if (b_iframe) { |
| console.log(`Found bframe with selector: ${selector}`); |
| break; |
| } |
| } catch { |
| continue; |
| } |
| } |
|
|
| if (b_iframe === null) { |
| console.log("Bframe not found yet, checking main frame..."); |
| |
| 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"); |
| } |
|
|
| const box_page = await iframe.contentFrame(); |
| if (box_page === null) { |
| throw new Error("Could not find reCAPTCHA iframe content"); |
| } |
|
|
| invisible = (await box_page.$("div.rc-anchor-invisible")) ? true : false; |
| console.log("invisible:", invisible); |
|
|
| if (invisible === true) { |
| return false; |
| } else { |
| const label = await box_page.$("#recaptcha-anchor-label"); |
| if (label === null) { |
| throw new Error("Could not find reCAPTCHA label"); |
| } |
|
|
| console.log("Clicking reCAPTCHA checkbox..."); |
| await label.click(); |
| |
| await sleep(2000); |
| |
| for (const selector of bframeSelectors) { |
| b_iframe = await page.$(selector); |
| if (b_iframe) { |
| console.log(`Found bframe after click: ${selector}`); |
| break; |
| } |
| } |
| } |
| } catch (error) { |
| console.error("Error handling main frame:", error.message); |
| throw new Error("Could not find reCAPTCHA popup iframe"); |
| } |
| } |
|
|
| if (b_iframe === null) { |
| throw new Error("Could not find reCAPTCHA popup iframe after all attempts"); |
| } |
|
|
| 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 found yet, this might be OK"); |
| return false; |
| } |
|
|
| const required = await challenge.evaluate( |
| (elm) => !elm.classList.contains("rc-footer"), |
| ); |
| console.log("action required:", required); |
|
|
| if (required === false) { |
| return false; |
| } |
|
|
| 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:", 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:", passed); |
| } |
|
|
| page.off("response", listener); |
|
|
| await sleep(1000); |
|
|
| let token = null; |
| |
| try { |
| const iframe = await page.$(MAIN_FRAME); |
| if (iframe) { |
| const box_page = await iframe.contentFrame(); |
| if (box_page) { |
| const textarea = await box_page.$('#g-recaptcha-response'); |
| if (textarea) { |
| token = await textarea.evaluate(el => el.value); |
| console.log("Token found in textarea:", token?.substring(0, 50) + "..."); |
| } |
| } |
| } |
| } catch (error) { |
| console.log("Error extracting token from iframe:", error.message); |
| } |
|
|
| if (!token) { |
| try { |
| const textarea = await page.$('#g-recaptcha-response'); |
| if (textarea) { |
| token = await textarea.evaluate(el => el.value); |
| console.log("Token found in main page:", token?.substring(0, 50) + "..."); |
| } |
| } catch (error) { |
| console.log("Error extracting token from main page:", error.message); |
| } |
| } |
|
|
| if (!token) { |
| try { |
| const allTextareas = await page.$('textarea'); |
| for (const textarea of allTextareas) { |
| const name = await textarea.getAttribute('name'); |
| const id = await textarea.getAttribute('id'); |
| if (name === 'g-recaptcha-response' || id === 'g-recaptcha-response') { |
| token = await textarea.evaluate(el => el.value); |
| console.log("Token found in textarea by attribute:", token?.substring(0, 50) + "..."); |
| break; |
| } |
| } |
| } catch (error) { |
| console.log("Error searching all textareas:", error.message); |
| } |
| } |
|
|
| console.log("Final token:", token ? `${token.substring(0, 50)}... (length: ${token.length})` : "not found"); |
|
|
| return { success: true, token }; |
| } |