NAME commited on
Commit ·
63c5b6b
1
Parent(s): 6f8cb92
Forgets
Browse files- Dockerfile +43 -0
- README.md +3 -4
- package.json +23 -0
- postinstall.js +122 -0
- server.js +201 -0
- solver.js +377 -0
Dockerfile
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:18-bullseye
|
| 2 |
+
|
| 3 |
+
RUN apt-get update && apt-get install -y \
|
| 4 |
+
ffmpeg \
|
| 5 |
+
chromium \
|
| 6 |
+
chromium-driver \
|
| 7 |
+
fonts-liberation \
|
| 8 |
+
libasound2 \
|
| 9 |
+
libatk-bridge2.0-0 \
|
| 10 |
+
libatk1.0-0 \
|
| 11 |
+
libatspi2.0-0 \
|
| 12 |
+
libcups2 \
|
| 13 |
+
libdbus-1-3 \
|
| 14 |
+
libdrm2 \
|
| 15 |
+
libgbm1 \
|
| 16 |
+
libgtk-3-0 \
|
| 17 |
+
libnspr4 \
|
| 18 |
+
libnss3 \
|
| 19 |
+
libwayland-client0 \
|
| 20 |
+
libxcomposite1 \
|
| 21 |
+
libxdamage1 \
|
| 22 |
+
libxfixes3 \
|
| 23 |
+
libxkbcommon0 \
|
| 24 |
+
libxrandr2 \
|
| 25 |
+
xdg-utils \
|
| 26 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 27 |
+
|
| 28 |
+
WORKDIR /app
|
| 29 |
+
|
| 30 |
+
COPY package*.json ./
|
| 31 |
+
COPY postinstall.js ./
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
RUN npm install
|
| 35 |
+
|
| 36 |
+
COPY . .
|
| 37 |
+
|
| 38 |
+
EXPOSE 7860
|
| 39 |
+
|
| 40 |
+
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/bin/chromium
|
| 41 |
+
ENV NODE_ENV=production
|
| 42 |
+
|
| 43 |
+
CMD ["node", "server.js"]
|
README.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
| 1 |
---
|
| 2 |
title: V2
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
-
short_description: 🙏
|
| 9 |
---
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
title: V2
|
| 3 |
+
emoji: 📉
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "recaptcha-solver-api",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "reCAPTCHA Solver API for Hugging Face",
|
| 5 |
+
"main": "server.js",
|
| 6 |
+
"type": "module",
|
| 7 |
+
"scripts": {
|
| 8 |
+
"start": "node server.js",
|
| 9 |
+
"dev": "node server.js",
|
| 10 |
+
"postinstall": "node postinstall.js"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"express": "^4.18.2",
|
| 14 |
+
"playwright-core": "^1.40.0",
|
| 15 |
+
"cors": "^2.8.5",
|
| 16 |
+
"vosk-koffi": "^1.0.8",
|
| 17 |
+
"wav": "^1.0.2",
|
| 18 |
+
"yauzl": "^2.10.0"
|
| 19 |
+
},
|
| 20 |
+
"engines": {
|
| 21 |
+
"node": ">=18.0.0"
|
| 22 |
+
}
|
| 23 |
+
}
|
postinstall.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'fs';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
import os from 'os';
|
| 4 |
+
import https from 'https';
|
| 5 |
+
import yauzl from 'yauzl';
|
| 6 |
+
import { fileURLToPath } from 'url';
|
| 7 |
+
|
| 8 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 9 |
+
const __dirname = path.dirname(__filename);
|
| 10 |
+
|
| 11 |
+
const URL = "https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip";
|
| 12 |
+
const MODEL_DIR = path.resolve(__dirname, "model");
|
| 13 |
+
|
| 14 |
+
async function main() {
|
| 15 |
+
if (!fs.existsSync(MODEL_DIR)) {
|
| 16 |
+
fs.mkdirSync(MODEL_DIR, { recursive: true });
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
if (fs.existsSync(path.resolve(MODEL_DIR, "DONE"))) {
|
| 20 |
+
console.log("Model already downloaded");
|
| 21 |
+
return;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const zip = path.resolve(os.tmpdir(), path.basename(URL));
|
| 25 |
+
await download(URL, zip);
|
| 26 |
+
console.log("Downloaded model to", zip);
|
| 27 |
+
|
| 28 |
+
await unzip(zip, MODEL_DIR);
|
| 29 |
+
fs.unlinkSync(zip);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function download(url, to, redirect = 0) {
|
| 33 |
+
if (redirect === 0) {
|
| 34 |
+
console.log(`Downloading ${url} to ${to}`);
|
| 35 |
+
} else {
|
| 36 |
+
console.log(`Redirecting to ${url}`);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
return new Promise((resolve, reject) => {
|
| 40 |
+
if (!fs.existsSync(path.dirname(to))) {
|
| 41 |
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
let done = true;
|
| 45 |
+
const file = fs.createWriteStream(to);
|
| 46 |
+
const request = https.get(url, (res) => {
|
| 47 |
+
if (res.statusCode === 302 && res.headers.location !== undefined) {
|
| 48 |
+
done = false;
|
| 49 |
+
file.close();
|
| 50 |
+
resolve(download(res.headers.location, to, redirect + 1));
|
| 51 |
+
return;
|
| 52 |
+
}
|
| 53 |
+
res.pipe(file);
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
file.on("finish", () => {
|
| 57 |
+
if (done) {
|
| 58 |
+
resolve(to);
|
| 59 |
+
}
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
request.on("error", (err) => {
|
| 63 |
+
fs.unlink(to, () => reject(err));
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
file.on("error", (err) => {
|
| 67 |
+
fs.unlink(to, () => reject(err));
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
request.end();
|
| 71 |
+
});
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
function unzip(zip, dest) {
|
| 75 |
+
const dir = path.basename(zip, ".zip");
|
| 76 |
+
return new Promise((resolve, reject) => {
|
| 77 |
+
yauzl.open(zip, { lazyEntries: true }, (err, zipfile) => {
|
| 78 |
+
if (err) {
|
| 79 |
+
reject(err);
|
| 80 |
+
}
|
| 81 |
+
zipfile.readEntry();
|
| 82 |
+
zipfile
|
| 83 |
+
.on("entry", (entry) => {
|
| 84 |
+
if (/\/$/.test(entry.fileName)) {
|
| 85 |
+
zipfile.readEntry();
|
| 86 |
+
} else {
|
| 87 |
+
zipfile.openReadStream(entry, (err, stream) => {
|
| 88 |
+
if (err) {
|
| 89 |
+
reject(err);
|
| 90 |
+
}
|
| 91 |
+
const f = path.resolve(dest, entry.fileName.replace(`${dir}/`, ""));
|
| 92 |
+
if (!fs.existsSync(path.dirname(f))) {
|
| 93 |
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
| 94 |
+
console.log("Created directory", path.dirname(f));
|
| 95 |
+
}
|
| 96 |
+
stream.pipe(fs.createWriteStream(f));
|
| 97 |
+
stream
|
| 98 |
+
.on("end", () => {
|
| 99 |
+
console.log("Extracted", f);
|
| 100 |
+
zipfile.readEntry();
|
| 101 |
+
})
|
| 102 |
+
.on("error", (err) => {
|
| 103 |
+
reject(err);
|
| 104 |
+
});
|
| 105 |
+
});
|
| 106 |
+
}
|
| 107 |
+
})
|
| 108 |
+
.on("error", (err) => {
|
| 109 |
+
reject(err);
|
| 110 |
+
})
|
| 111 |
+
.on("end", () => {
|
| 112 |
+
console.log("Extracted all files");
|
| 113 |
+
fs.writeFileSync(path.resolve(dest, "DONE"), "");
|
| 114 |
+
})
|
| 115 |
+
.on("close", () => {
|
| 116 |
+
resolve();
|
| 117 |
+
});
|
| 118 |
+
});
|
| 119 |
+
});
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
main().catch(console.error);
|
server.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from 'express';
|
| 2 |
+
import { chromium } from 'playwright-core';
|
| 3 |
+
import cors from 'cors';
|
| 4 |
+
import { solve } from './solver.js';
|
| 5 |
+
|
| 6 |
+
const app = express();
|
| 7 |
+
const PORT = process.env.PORT || 7860;
|
| 8 |
+
|
| 9 |
+
app.use(cors());
|
| 10 |
+
app.use(express.json());
|
| 11 |
+
|
| 12 |
+
app.get('/', (req, res) => {
|
| 13 |
+
res.json({
|
| 14 |
+
status: 'ok',
|
| 15 |
+
message: 'reCAPTCHA Solver API is running',
|
| 16 |
+
endpoints: {
|
| 17 |
+
solve: '/api/solve?url=YOUR_URL',
|
| 18 |
+
health: '/health'
|
| 19 |
+
}
|
| 20 |
+
});
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
app.get('/health', (req, res) => {
|
| 24 |
+
res.json({ status: 'healthy' });
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
app.get('/api/solve', async (req, res) => {
|
| 28 |
+
const { url } = req.query;
|
| 29 |
+
|
| 30 |
+
if (!url) {
|
| 31 |
+
return res.status(400).json({
|
| 32 |
+
error: 'Missing required parameter',
|
| 33 |
+
message: 'Please provide a URL parameter'
|
| 34 |
+
});
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
let browser;
|
| 38 |
+
const startTime = Date.now();
|
| 39 |
+
|
| 40 |
+
try {
|
| 41 |
+
console.log(`[API] Starting to solve reCAPTCHA for: ${url}`);
|
| 42 |
+
|
| 43 |
+
browser = await chromium.launch({
|
| 44 |
+
headless: true,
|
| 45 |
+
args: [
|
| 46 |
+
'--no-sandbox',
|
| 47 |
+
'--disable-setuid-sandbox',
|
| 48 |
+
'--disable-dev-shm-usage',
|
| 49 |
+
'--disable-gpu',
|
| 50 |
+
'--disable-software-rasterizer',
|
| 51 |
+
'--disable-extensions',
|
| 52 |
+
'--disable-blink-features=AutomationControlled'
|
| 53 |
+
],
|
| 54 |
+
executablePath: process.env.PLAYWRIGHT_BROWSERS_PATH || '/usr/bin/chromium'
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
const context = await browser.newContext({
|
| 58 |
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 59 |
+
viewport: { width: 1280, height: 720 }
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
const page = await context.newPage();
|
| 63 |
+
|
| 64 |
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
| 65 |
+
|
| 66 |
+
console.log('[API] Attempting to solve reCAPTCHA...');
|
| 67 |
+
|
| 68 |
+
await solve(page);
|
| 69 |
+
|
| 70 |
+
const solveTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
| 71 |
+
console.log(`[API] reCAPTCHA solved in ${solveTime}s`);
|
| 72 |
+
|
| 73 |
+
const pageUrl = page.url();
|
| 74 |
+
const pageTitle = await page.title();
|
| 75 |
+
|
| 76 |
+
const token = await page.evaluate(() => {
|
| 77 |
+
const textarea = document.querySelector('[name="g-recaptcha-response"]');
|
| 78 |
+
return textarea ? textarea.value : null;
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
await browser.close();
|
| 82 |
+
|
| 83 |
+
res.json({
|
| 84 |
+
success: true,
|
| 85 |
+
message: 'reCAPTCHA solved successfully',
|
| 86 |
+
solveTime: `${solveTime}s`,
|
| 87 |
+
url: pageUrl,
|
| 88 |
+
title: pageTitle,
|
| 89 |
+
hasToken: !!token,
|
| 90 |
+
tokenLength: token ? token.length : 0,
|
| 91 |
+
timestamp: new Date().toISOString()
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
} catch (error) {
|
| 95 |
+
console.error('[API] Error solving reCAPTCHA:', error);
|
| 96 |
+
|
| 97 |
+
if (browser) {
|
| 98 |
+
await browser.close().catch(err => console.error('Error closing browser:', err));
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
res.status(500).json({
|
| 102 |
+
success: false,
|
| 103 |
+
error: 'Failed to solve reCAPTCHA',
|
| 104 |
+
message: error.message,
|
| 105 |
+
timestamp: new Date().toISOString()
|
| 106 |
+
});
|
| 107 |
+
}
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
app.post('/api/solve', async (req, res) => {
|
| 111 |
+
const { url, submitSelector } = req.body;
|
| 112 |
+
|
| 113 |
+
if (!url) {
|
| 114 |
+
return res.status(400).json({
|
| 115 |
+
error: 'Missing required field',
|
| 116 |
+
message: 'Please provide a URL in the request body'
|
| 117 |
+
});
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
let browser;
|
| 121 |
+
const startTime = Date.now();
|
| 122 |
+
|
| 123 |
+
try {
|
| 124 |
+
console.log(`[API] Starting to solve reCAPTCHA for: ${url}`);
|
| 125 |
+
|
| 126 |
+
browser = await chromium.launch({
|
| 127 |
+
headless: true,
|
| 128 |
+
args: [
|
| 129 |
+
'--no-sandbox',
|
| 130 |
+
'--disable-setuid-sandbox',
|
| 131 |
+
'--disable-dev-shm-usage',
|
| 132 |
+
'--disable-gpu',
|
| 133 |
+
'--disable-blink-features=AutomationControlled'
|
| 134 |
+
],
|
| 135 |
+
executablePath: process.env.PLAYWRIGHT_BROWSERS_PATH || '/usr/bin/chromium'
|
| 136 |
+
});
|
| 137 |
+
|
| 138 |
+
const context = await browser.newContext({
|
| 139 |
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 140 |
+
viewport: { width: 1280, height: 720 }
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
const page = await context.newPage();
|
| 144 |
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
| 145 |
+
|
| 146 |
+
console.log('[API] Attempting to solve reCAPTCHA...');
|
| 147 |
+
await solve(page);
|
| 148 |
+
|
| 149 |
+
await page.waitForTimeout(2000);
|
| 150 |
+
|
| 151 |
+
if (submitSelector) {
|
| 152 |
+
await page.click(submitSelector);
|
| 153 |
+
await page.waitForTimeout(3000);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
const solveTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
| 157 |
+
console.log(`[API] reCAPTCHA solved in ${solveTime}s`);
|
| 158 |
+
|
| 159 |
+
const pageUrl = page.url();
|
| 160 |
+
|
| 161 |
+
await browser.close();
|
| 162 |
+
|
| 163 |
+
res.json({
|
| 164 |
+
success: true,
|
| 165 |
+
message: 'reCAPTCHA solved successfully',
|
| 166 |
+
solveTime: `${solveTime}s`,
|
| 167 |
+
url: pageUrl,
|
| 168 |
+
timestamp: new Date().toISOString()
|
| 169 |
+
});
|
| 170 |
+
|
| 171 |
+
} catch (error) {
|
| 172 |
+
console.error('[API] Error solving reCAPTCHA:', error);
|
| 173 |
+
|
| 174 |
+
if (browser) {
|
| 175 |
+
await browser.close().catch(err => console.error('Error closing browser:', err));
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
res.status(500).json({
|
| 179 |
+
success: false,
|
| 180 |
+
error: 'Failed to solve reCAPTCHA',
|
| 181 |
+
message: error.message,
|
| 182 |
+
timestamp: new Date().toISOString()
|
| 183 |
+
});
|
| 184 |
+
}
|
| 185 |
+
});
|
| 186 |
+
|
| 187 |
+
app.listen(PORT, '0.0.0.0', () => {
|
| 188 |
+
console.log(`🚀 reCAPTCHA Solver API running on port ${PORT}`);
|
| 189 |
+
console.log(`📍 Health check: http://localhost:${PORT}/health`);
|
| 190 |
+
console.log(`🔧 Solve endpoint: http://localhost:${PORT}/api/solve?url=YOUR_URL`);
|
| 191 |
+
});
|
| 192 |
+
|
| 193 |
+
process.on('SIGTERM', () => {
|
| 194 |
+
console.log('SIGTERM received, shutting down gracefully...');
|
| 195 |
+
process.exit(0);
|
| 196 |
+
});
|
| 197 |
+
|
| 198 |
+
process.on('SIGINT', () => {
|
| 199 |
+
console.log('SIGINT received, shutting down gracefully...');
|
| 200 |
+
process.exit(0);
|
| 201 |
+
});
|
solver.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { spawnSync } from 'child_process';
|
| 2 |
+
import fs from 'fs';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
import os from 'os';
|
| 5 |
+
import { Readable } from 'stream';
|
| 6 |
+
import vosk from 'vosk-koffi';
|
| 7 |
+
import wav from 'wav';
|
| 8 |
+
import { fileURLToPath } from 'url';
|
| 9 |
+
|
| 10 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 11 |
+
const __dirname = path.dirname(__filename);
|
| 12 |
+
|
| 13 |
+
vosk.setLogLevel(-1);
|
| 14 |
+
const MODEL_DIR = path.resolve(__dirname, "model");
|
| 15 |
+
const model = new vosk.Model(MODEL_DIR);
|
| 16 |
+
|
| 17 |
+
const SOURCE_FILE = "sound.mp3";
|
| 18 |
+
const OUT_FILE = "out.wav";
|
| 19 |
+
const MAIN_FRAME = "iframe[title='reCAPTCHA']";
|
| 20 |
+
const BFRAME = "iframe[src*='google.com/recaptcha'][src*='bframe']";
|
| 21 |
+
const CHALLENGE = "body > div > div";
|
| 22 |
+
|
| 23 |
+
class Mutex {
|
| 24 |
+
constructor(name = "Mutex") {
|
| 25 |
+
this.name = name;
|
| 26 |
+
this._locked = false;
|
| 27 |
+
this._queue = [];
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
async lock(tag) {
|
| 31 |
+
if (this._locked) {
|
| 32 |
+
if (tag) {
|
| 33 |
+
console.log(`[${this.name}] ${tag} waiting`);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
return new Promise((resolve) => {
|
| 37 |
+
this._queue.push(() => {
|
| 38 |
+
this._lock(tag);
|
| 39 |
+
resolve();
|
| 40 |
+
});
|
| 41 |
+
});
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
this._lock(tag);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
unlock(tag) {
|
| 48 |
+
if (this._locked) {
|
| 49 |
+
if (tag) {
|
| 50 |
+
console.log(`[${this.name}] ${tag} unlocked`);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
this._locked = false;
|
| 54 |
+
this._queue.shift()?.();
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
_lock(tag) {
|
| 59 |
+
this._locked = true;
|
| 60 |
+
|
| 61 |
+
if (tag) {
|
| 62 |
+
console.log(`[${this.name}] ${tag} locked`);
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function sleep(ms) {
|
| 68 |
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function createDir() {
|
| 72 |
+
const dir = path.resolve(os.tmpdir(), "reSOLVER-" + Math.random().toString().slice(2));
|
| 73 |
+
if (fs.existsSync(dir)) {
|
| 74 |
+
fs.rmSync(dir, { recursive: true });
|
| 75 |
+
}
|
| 76 |
+
fs.mkdirSync(dir, { recursive: true });
|
| 77 |
+
return dir;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function convert(dir, ffmpeg = "ffmpeg") {
|
| 81 |
+
const args = [
|
| 82 |
+
"-loglevel",
|
| 83 |
+
"error",
|
| 84 |
+
"-i",
|
| 85 |
+
SOURCE_FILE,
|
| 86 |
+
"-acodec",
|
| 87 |
+
"pcm_s16le",
|
| 88 |
+
"-ac",
|
| 89 |
+
"1",
|
| 90 |
+
"-ar",
|
| 91 |
+
"16000",
|
| 92 |
+
OUT_FILE,
|
| 93 |
+
];
|
| 94 |
+
|
| 95 |
+
spawnSync(ffmpeg, args, { cwd: dir });
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
function recognize(dir) {
|
| 99 |
+
return new Promise((resolve) => {
|
| 100 |
+
const stream = fs.createReadStream(path.resolve(dir, OUT_FILE), { highWaterMark: 4096 });
|
| 101 |
+
|
| 102 |
+
const reader = new wav.Reader();
|
| 103 |
+
const readable = new Readable().wrap(reader);
|
| 104 |
+
reader.on("format", async ({ audioFormat, sampleRate, channels }) => {
|
| 105 |
+
if (audioFormat != 1 || channels != 1) {
|
| 106 |
+
throw new Error("Audio file must be WAV with mono PCM.");
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
const rec = new vosk.Recognizer({ model, sampleRate });
|
| 110 |
+
rec.setMaxAlternatives(10);
|
| 111 |
+
rec.setWords(true);
|
| 112 |
+
rec.setPartialWords(true);
|
| 113 |
+
|
| 114 |
+
for await (const data of readable) {
|
| 115 |
+
const end_of_speech = rec.acceptWaveform(data);
|
| 116 |
+
if (end_of_speech) {
|
| 117 |
+
const result = rec
|
| 118 |
+
.result()
|
| 119 |
+
.alternatives.sort((a, b) => b.confidence - a.confidence)[0].text;
|
| 120 |
+
stream.close(() => resolve(result));
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
rec.free();
|
| 125 |
+
});
|
| 126 |
+
|
| 127 |
+
stream.pipe(reader);
|
| 128 |
+
});
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
async function getText(res, ffmpeg = "ffmpeg") {
|
| 132 |
+
const tempDir = createDir();
|
| 133 |
+
|
| 134 |
+
fs.writeFileSync(path.resolve(tempDir, SOURCE_FILE), await res.body());
|
| 135 |
+
convert(tempDir, ffmpeg);
|
| 136 |
+
const result = await recognize(tempDir);
|
| 137 |
+
|
| 138 |
+
fs.rmSync(tempDir, { recursive: true });
|
| 139 |
+
return result;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
export async function solve(page, { delay = 64, wait = 5000, retry = 3, ffmpeg = "ffmpeg" } = {}) {
|
| 143 |
+
console.log("Starting reCAPTCHA solver...");
|
| 144 |
+
|
| 145 |
+
try {
|
| 146 |
+
await page.waitForSelector('iframe', { state: "attached", timeout: wait });
|
| 147 |
+
console.log("Found iframes on page");
|
| 148 |
+
|
| 149 |
+
const allIframes = await page.$$('iframe');
|
| 150 |
+
console.log(`Total iframes found: ${allIframes.length}`);
|
| 151 |
+
|
| 152 |
+
for (let i = 0; i < allIframes.length; i++) {
|
| 153 |
+
const src = await allIframes[i].getAttribute('src');
|
| 154 |
+
const title = await allIframes[i].getAttribute('title');
|
| 155 |
+
console.log(`Iframe ${i}: src=${src}, title=${title}`);
|
| 156 |
+
}
|
| 157 |
+
} catch (error) {
|
| 158 |
+
console.error("Error finding iframes:", error.message);
|
| 159 |
+
throw new Error("No reCAPTCHA detected");
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
let invisible = false;
|
| 163 |
+
|
| 164 |
+
let b_iframe = null;
|
| 165 |
+
const bframeSelectors = [
|
| 166 |
+
"iframe[src*='/recaptcha/api2/bframe']",
|
| 167 |
+
"iframe[src*='/recaptcha/enterprise/bframe']",
|
| 168 |
+
"iframe[src*='bframe']"
|
| 169 |
+
];
|
| 170 |
+
|
| 171 |
+
for (const selector of bframeSelectors) {
|
| 172 |
+
try {
|
| 173 |
+
await page.waitForSelector(selector, { state: "attached", timeout: 2000 });
|
| 174 |
+
b_iframe = await page.$(selector);
|
| 175 |
+
if (b_iframe) {
|
| 176 |
+
console.log(`Found bframe with selector: ${selector}`);
|
| 177 |
+
break;
|
| 178 |
+
}
|
| 179 |
+
} catch {
|
| 180 |
+
continue;
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
if (b_iframe === null) {
|
| 185 |
+
console.log("Bframe not found yet, checking main frame...");
|
| 186 |
+
|
| 187 |
+
try {
|
| 188 |
+
await page.waitForSelector(MAIN_FRAME, { state: "attached", timeout: wait });
|
| 189 |
+
const iframe = await page.$(MAIN_FRAME);
|
| 190 |
+
|
| 191 |
+
if (iframe === null) {
|
| 192 |
+
throw new Error("Could not find reCAPTCHA iframe");
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
const box_page = await iframe.contentFrame();
|
| 196 |
+
if (box_page === null) {
|
| 197 |
+
throw new Error("Could not find reCAPTCHA iframe content");
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
invisible = (await box_page.$("div.rc-anchor-invisible")) ? true : false;
|
| 201 |
+
console.log("invisible:", invisible);
|
| 202 |
+
|
| 203 |
+
if (invisible === true) {
|
| 204 |
+
return false;
|
| 205 |
+
} else {
|
| 206 |
+
const label = await box_page.$("#recaptcha-anchor-label");
|
| 207 |
+
if (label === null) {
|
| 208 |
+
throw new Error("Could not find reCAPTCHA label");
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
console.log("Clicking reCAPTCHA checkbox...");
|
| 212 |
+
await label.click();
|
| 213 |
+
|
| 214 |
+
await sleep(2000);
|
| 215 |
+
|
| 216 |
+
for (const selector of bframeSelectors) {
|
| 217 |
+
b_iframe = await page.$(selector);
|
| 218 |
+
if (b_iframe) {
|
| 219 |
+
console.log(`Found bframe after click: ${selector}`);
|
| 220 |
+
break;
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
} catch (error) {
|
| 225 |
+
console.error("Error handling main frame:", error.message);
|
| 226 |
+
throw new Error("Could not find reCAPTCHA popup iframe");
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
if (b_iframe === null) {
|
| 231 |
+
throw new Error("Could not find reCAPTCHA popup iframe after all attempts");
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
const bframe = await b_iframe.contentFrame();
|
| 235 |
+
if (bframe === null) {
|
| 236 |
+
throw new Error("Could not find reCAPTCHA popup iframe content");
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
await sleep(1000);
|
| 240 |
+
|
| 241 |
+
const challenge = await bframe.$(CHALLENGE);
|
| 242 |
+
if (challenge === null) {
|
| 243 |
+
console.log("No challenge found yet, this might be OK");
|
| 244 |
+
return false;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
const required = await challenge.evaluate(
|
| 248 |
+
(elm) => !elm.classList.contains("rc-footer"),
|
| 249 |
+
);
|
| 250 |
+
console.log("action required:", required);
|
| 251 |
+
|
| 252 |
+
if (required === false) {
|
| 253 |
+
return false;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
await bframe.waitForSelector("#recaptcha-audio-button", { timeout: wait });
|
| 257 |
+
const audio_button = await bframe.$("#recaptcha-audio-button");
|
| 258 |
+
if (audio_button === null) {
|
| 259 |
+
throw new Error("Could not find reCAPTCHA audio button");
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
const mutex = new Mutex();
|
| 263 |
+
await mutex.lock("init");
|
| 264 |
+
let passed = false;
|
| 265 |
+
let answer = Promise.resolve("");
|
| 266 |
+
const listener = async (res) => {
|
| 267 |
+
if (res.headers()["content-type"] === "audio/mp3") {
|
| 268 |
+
console.log(`got audio from ${res.url()}`);
|
| 269 |
+
answer = new Promise((resolve) => {
|
| 270 |
+
getText(res, ffmpeg)
|
| 271 |
+
.then(resolve)
|
| 272 |
+
.catch(() => undefined);
|
| 273 |
+
});
|
| 274 |
+
mutex.unlock("get sound");
|
| 275 |
+
} else if (
|
| 276 |
+
res.url().startsWith("https://www.google.com/recaptcha/api2/userverify") ||
|
| 277 |
+
res.url().startsWith("https://www.google.com/recaptcha/enterprise/userverify")
|
| 278 |
+
) {
|
| 279 |
+
const raw = (await res.body()).toString().replace(")]}'\n", "");
|
| 280 |
+
const json = JSON.parse(raw);
|
| 281 |
+
passed = json[2] === 1;
|
| 282 |
+
mutex.unlock("verified");
|
| 283 |
+
}
|
| 284 |
+
};
|
| 285 |
+
page.on("response", listener);
|
| 286 |
+
|
| 287 |
+
await audio_button.click();
|
| 288 |
+
|
| 289 |
+
let tried = 0;
|
| 290 |
+
while (passed === false) {
|
| 291 |
+
if (tried++ >= retry) {
|
| 292 |
+
throw new Error("Could not solve reCAPTCHA");
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
await Promise.race([
|
| 296 |
+
mutex.lock("ready"),
|
| 297 |
+
sleep(wait).then(() => {
|
| 298 |
+
throw new Error("No Audio Found");
|
| 299 |
+
}),
|
| 300 |
+
]);
|
| 301 |
+
await bframe.waitForSelector("#audio-source", { state: "attached", timeout: wait });
|
| 302 |
+
await bframe.waitForSelector("#audio-response", { timeout: wait });
|
| 303 |
+
|
| 304 |
+
console.log("recognized:", await answer);
|
| 305 |
+
|
| 306 |
+
const input = await bframe.$("#audio-response");
|
| 307 |
+
if (input === null) {
|
| 308 |
+
throw new Error("Could not find reCAPTCHA audio input");
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
await input.type(await answer, { delay });
|
| 312 |
+
|
| 313 |
+
const button = await bframe.$("#recaptcha-verify-button");
|
| 314 |
+
if (button === null) {
|
| 315 |
+
throw new Error("Could not find reCAPTCHA verify button");
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
await button.click();
|
| 319 |
+
await mutex.lock("done");
|
| 320 |
+
console.log("passed:", passed);
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
page.off("response", listener);
|
| 324 |
+
|
| 325 |
+
await sleep(1000);
|
| 326 |
+
|
| 327 |
+
let token = null;
|
| 328 |
+
|
| 329 |
+
try {
|
| 330 |
+
const iframe = await page.$(MAIN_FRAME);
|
| 331 |
+
if (iframe) {
|
| 332 |
+
const box_page = await iframe.contentFrame();
|
| 333 |
+
if (box_page) {
|
| 334 |
+
const textarea = await box_page.$('#g-recaptcha-response');
|
| 335 |
+
if (textarea) {
|
| 336 |
+
token = await textarea.evaluate(el => el.value);
|
| 337 |
+
console.log("Token found in textarea:", token?.substring(0, 50) + "...");
|
| 338 |
+
}
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
} catch (error) {
|
| 342 |
+
console.log("Error extracting token from iframe:", error.message);
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
if (!token) {
|
| 346 |
+
try {
|
| 347 |
+
const textarea = await page.$('#g-recaptcha-response');
|
| 348 |
+
if (textarea) {
|
| 349 |
+
token = await textarea.evaluate(el => el.value);
|
| 350 |
+
console.log("Token found in main page:", token?.substring(0, 50) + "...");
|
| 351 |
+
}
|
| 352 |
+
} catch (error) {
|
| 353 |
+
console.log("Error extracting token from main page:", error.message);
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
if (!token) {
|
| 358 |
+
try {
|
| 359 |
+
const allTextareas = await page.$('textarea');
|
| 360 |
+
for (const textarea of allTextareas) {
|
| 361 |
+
const name = await textarea.getAttribute('name');
|
| 362 |
+
const id = await textarea.getAttribute('id');
|
| 363 |
+
if (name === 'g-recaptcha-response' || id === 'g-recaptcha-response') {
|
| 364 |
+
token = await textarea.evaluate(el => el.value);
|
| 365 |
+
console.log("Token found in textarea by attribute:", token?.substring(0, 50) + "...");
|
| 366 |
+
break;
|
| 367 |
+
}
|
| 368 |
+
}
|
| 369 |
+
} catch (error) {
|
| 370 |
+
console.log("Error searching all textareas:", error.message);
|
| 371 |
+
}
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
console.log("Final token:", token ? `${token.substring(0, 50)}... (length: ${token.length})` : "not found");
|
| 375 |
+
|
| 376 |
+
return { success: true, token };
|
| 377 |
+
}
|