recaptcha-v2-solver
Version:
ReCaptcha v2 bypass solution using three methods: Audio transcription, Visual (Gemini), and 2Captcha
1,204 lines (1,009 loc) • 42.3 kB
JavaScript
const puppeteerExtra = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const os = require('os');
const fs = require('fs').promises;
const path = require('path');
const { GoogleGenerativeAI } = require('@google/generative-ai');
const createLogger = require('../utils/logger');
// Initialize with default level, will be updated when the main function is called
let logger = createLogger({ level: 'info' });
// Setup puppeteer with stealth plugin
puppeteerExtra.use(StealthPlugin());
class ResultTracker {
constructor() {
this.results = [];
this.startTime = Date.now();
this.maxResults = 500;
this.firstProcessingTime = null;
}
addResult(result) {
if (!this.firstProcessingTime) {
this.firstProcessingTime = Date.now();
}
this.results.push({
success: result.token ? true : false, // Success is based on getting a token
timestamp: Date.now()
});
if (this.results.length > this.maxResults) {
this.results.shift();
}
// Automatically print stats after adding a result
this.printStats();
}
getStats() {
if (this.results.length === 0) return null;
const successCount = this.results.filter(r => r.success).length;
const successRate = (successCount / this.results.length) * 100;
let avgTimePerToken = 0;
if (successCount > 0) {
const totalElapsedSeconds = (Date.now() - this.startTime) / 1000;
avgTimePerToken = totalElapsedSeconds / successCount;
}
return {
successRate: successRate.toFixed(2),
avgTimePerToken: avgTimePerToken.toFixed(2),
totalAttempts: this.results.length,
successfulTokens: successCount
};
}
printStats() {
const stats = this.getStats();
if (!stats) return;
logger.info(`Stats: Success Rate: ${stats.successRate}% | Avg Time/Token: ${stats.avgTimePerToken}s | Total Attempts: ${stats.totalAttempts} | Successful Tokens: ${stats.successfulTokens}`);
}
}
class CaptchaWatcher {
constructor() {
this.page = null;
this.isWatching = false;
this.currentChallengeText = null;
this.currentCaptchaFrame = null;
this.currentChallengeFrame = null;
this.currentImageUrls = null;
this.currentCheckbox = null;
this.callbacks = {
onChallengeOpen: () => { },
onCaptchaReady: () => { },
onChallengeChange: () => { },
onTilesReady: () => { },
onTokenFound: () => { }
};
}
setPage(page) {
this.page = page;
this.currentChallengeText = null;
this.startWatching();
return this;
}
async startWatching() {
if (!this.page) {
logger.error('No page set. Call setPage(page) first');
return;
}
if (this.isWatching) {
logger.info('Watcher is already running');
return;
}
this.isWatching = true;
this._pollForCaptchaReady();
this._pollForChallengeFrame();
this._pollForToken();
}
async stopWatching() {
this.isWatching = false;
}
onChallengeOpen(callback) {
this.callbacks.onChallengeOpen = callback;
}
onCaptchaReady(callback) {
this.callbacks.onCaptchaReady = callback;
}
onChallengeChange(callback) {
this.callbacks.onChallengeChange = callback;
}
onTilesReady(callback) {
this.callbacks.onTilesReady = callback;
}
onTokenFound(callback) {
this.callbacks.onTokenFound = callback;
}
async _pollForCaptchaReady() {
try {
while (this.isWatching) {
const frames = await this.page.frames();
const captchaFrame = frames.find(frame => frame.url().includes('api2/anchor'));
if (captchaFrame) {
let checkCount = 0;
while (this.isWatching && checkCount < 50) {
const captchaInfo = await captchaFrame.evaluate(() => {
const checkbox = document.querySelector('.recaptcha-checkbox-border');
if (!checkbox) return null;
const rect = checkbox.getBoundingClientRect();
const style = window.getComputedStyle(checkbox);
const isVisible = rect.width > 0 &&
rect.height > 0 &&
style.visibility !== 'hidden' &&
style.display !== 'none';
const isClickable = !checkbox.disabled &&
!checkbox.getAttribute('disabled') &&
isVisible;
if (isClickable) {
return {
timestamp: new Date().toISOString(),
element: 'checkbox',
status: 'ready'
};
}
return null;
});
if (captchaInfo) {
this.currentCaptchaFrame = captchaFrame;
this.currentCheckbox = await captchaFrame.$('.recaptcha-checkbox-border');
this.callbacks.onCaptchaReady({
...captchaInfo,
frame: captchaFrame
});
return;
}
await new Promise(resolve => setTimeout(resolve, 100));
checkCount++;
}
}
await new Promise(resolve => setTimeout(resolve, 100));
}
} catch (error) {
console.error('Error in captcha ready polling:', error);
}
}
async _pollForChallengeFrame() {
try {
while (this.isWatching) {
const frames = await this.page.frames();
const challengeFrame = frames.find(frame => frame.url().includes('api2/bframe'));
if (challengeFrame) {
let checkCount = 0;
while (this.isWatching && checkCount < 50) {
const challengeInfo = await challengeFrame.evaluate(() => {
const checkTilesLoaded = () => {
const element = document.querySelector('.rc-imageselect-challenge');
if (!element) return false;
const tiles = element.querySelectorAll('.rc-imageselect-tile');
if (!tiles.length) return false;
const anyTilesInTransition = Array.from(tiles).some(tile =>
tile.classList.contains('rc-imageselect-dynamic-selected')
);
if (anyTilesInTransition) {
console.log('Tiles are still in transition');
return false;
}
return true;
};
const getImageUrls = () => {
const images = document.querySelectorAll('.rc-image-tile-33, .rc-image-tile-44');
return Array.from(images).map(img => {
const style = window.getComputedStyle(img);
const backgroundImage = style.backgroundImage || '';
const src = img.src || '';
return backgroundImage.replace(/url\(['"]?(.*?)['"]?\)/, '$1') || src;
}).filter(Boolean);
};
const payload = document.querySelector('.rc-imageselect-payload');
const desc = document.querySelector('.rc-imageselect-desc, .rc-imageselect-desc-no-canonical');
const table = document.querySelector('.rc-imageselect-table-33, .rc-imageselect-table-44');
if (payload && desc && table) {
const tilesReady = checkTilesLoaded();
if (tilesReady) {
const text = desc.textContent.trim();
const hasCorrectFormat = text.includes('Select all images with');
let promptText = '';
const strongElement = desc.querySelector('strong');
if (strongElement) {
promptText = strongElement.textContent.trim();
} else {
const match = text.match(/Select all images with (.*?)(?:$|\.|\n)/i);
if (match) {
promptText = match[1].trim();
}
}
const imageUrls = getImageUrls();
return {
text: text,
type: desc.className,
gridType: table.className,
imageCount: table.querySelectorAll('img').length,
timestamp: new Date().toISOString(),
hasCorrectFormat: hasCorrectFormat,
isDynamic: text.includes('Click verify once there are none left'),
mainText: text,
promptText: promptText,
tilesReady: true,
imageUrls: imageUrls
};
}
}
return null;
});
if (challengeInfo) {
this.currentChallengeFrame = challengeFrame;
this.callbacks.onTilesReady({
...challengeInfo,
frame: challengeFrame
});
const hasChanged = this._checkIfChallengeChanged(challengeInfo);
if (!this.currentChallengeText) {
this._updateCurrentChallenge(challengeInfo);
this.callbacks.onChallengeOpen({
...challengeInfo,
frame: challengeFrame
});
}
else if (hasChanged) {
this._updateCurrentChallenge(challengeInfo);
this.callbacks.onChallengeChange({
...challengeInfo,
frame: challengeFrame
});
}
}
await new Promise(resolve => setTimeout(resolve, 100));
checkCount++;
}
}
await new Promise(resolve => setTimeout(resolve, 100));
}
} catch (error) {
console.error('Error in challenge frame polling:', error);
}
}
_checkIfChallengeChanged(newChallengeInfo) {
if (this.currentChallengeText !== newChallengeInfo.text) {
return true;
}
if (!this.currentImageUrls || !newChallengeInfo.imageUrls) {
return true;
}
if (this.currentImageUrls.length !== newChallengeInfo.imageUrls.length) {
return true;
}
return this.currentImageUrls.some((url, index) =>
url !== newChallengeInfo.imageUrls[index]
);
}
_updateCurrentChallenge(challengeInfo) {
this.currentChallengeText = challengeInfo.text;
this.currentImageUrls = challengeInfo.imageUrls;
}
async _pollForToken() {
try {
while (this.isWatching) {
try {
if (!this.page || !this.page.isClosed()) {
const token = await this.page.evaluate(() => {
const textarea = document.querySelector('textarea[name="g-recaptcha-response"]');
return textarea ? textarea.value : null;
});
if (token) {
this.callbacks.onTokenFound({
timestamp: new Date().toISOString(),
token: token
});
return;
}
}
} catch (evalError) {
if (!evalError.message.includes('Execution context was destroyed')) {
console.error('Token polling evaluation error:', evalError);
}
}
await new Promise(resolve => setTimeout(resolve, 100));
}
} catch (error) {
console.error('Error in token polling:', error);
}
}
getCaptchaFrame() {
return this.currentCaptchaFrame;
}
getChallengeFrame() {
return this.currentChallengeFrame;
}
getCheckbox() {
return this.currentCheckbox;
}
cleanup() {
this.stopWatching();
this.page = null;
this.currentCaptchaFrame = null;
this.currentChallengeFrame = null;
this.currentCheckbox = null;
this.currentChallengeText = null;
this.currentImageUrls = null;
}
}
// Browser management functions
async function launchBrowser(userDataDir, proxyConfig = null, browserConfig) {
const randomProfile = Math.floor(Math.random() * 4) + 1;
const browser = await puppeteerExtra.launch({
headless: browserConfig.headless,
executablePath: browserConfig.executablePath,
userDataDir: userDataDir,
protocolTimeout: 30000,
args: [
'--no-sandbox',
'--disable-gpu',
'--enable-webgl',
'--window-size=1920,1080',
'--disable-dev-shm-usage',
'--disable-setuid-sandbox',
'--no-first-run',
'--no-default-browser-check',
'--password-store=basic',
'--disable-blink-features=AutomationControlled',
'--disable-features=IsolateOrigins,site-per-process',
'--lang=en',
'--disable-web-security',
'--flag-switches-begin --disable-site-isolation-trials --flag-switches-end',
`--profile-directory=Profile ${randomProfile}`,
proxyConfig ? `--proxy-server=${proxyConfig.host}:${proxyConfig.port}` : ''
].filter(Boolean),
ignoreDefaultArgs: ['--enable-automation', '--enable-blink-features=AutomationControlled'],
defaultViewport: null,
});
// Update page configuration
browser.on('targetcreated', async (target) => {
const page = await target.page();
if (page) {
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
delete navigator.__proto__.webdriver;
});
const randomUserAgent = browserConfig.userAgents[Math.floor(Math.random() * browserConfig.userAgents.length)];
await page.setUserAgent(randomUserAgent);
await page.setDefaultTimeout(30000);
await page.setDefaultNavigationTimeout(30000);
// Set proxy authentication if provided
if (proxyConfig?.username && proxyConfig?.password) {
await page.authenticate({
username: proxyConfig.username,
password: proxyConfig.password
});
}
}
});
return browser;
}
async function closeBrowser(browser) {
try {
await browser.close();
} catch (error) {
logger.error('Error closing browser:', error);
}
}
// Add function to analyze image with Gemini
async function analyzeWithGemini(screenshotPath, prompt, gridType, geminiConfig) {
try {
logger.info(`Original prompt: ${prompt}`);
const mainPrompt = prompt.split('Click verify once there are none left')[0].trim()
.replace(/\.$/, '');
logger.info(`Processed prompt: ${mainPrompt}`);
const genAI = new GoogleGenerativeAI(geminiConfig.apiKey);
// Use the model from config or fall back to default
const model = genAI.getGenerativeModel({
model: geminiConfig.model || 'gemini-1.5-flash',
generationConfig: {
temperature: 0.1,
topP: 0.95,
topK: 40,
}
});
// Read screenshot file
const imageData = await fs.readFile(screenshotPath);
const imageBase64 = imageData.toString('base64');
// Construct grid description based on type
const gridDesc = gridType === "4x4" ?
`Row 4: [1,1] - [1,2] - [1,3] - [1,4]
Row 3: [2,1] - [2,2] - [2,3] - [2,4]
Row 2: [3,1] - [3,2] - [3,3] - [3,4]
Row 1: [4,1] - [4,2] - [4,3] - [4,4]` :
`Row 3: [1,1] - [1,2] - [1,3]
Row 2: [2,1] - [2,2] - [2,3]
Row 1: [3,1] - [3,2] - [3,3]`;
const finalPrompt = `For each tile in the grid, check if it contains a VISIBLE -- ${mainPrompt.toUpperCase()} -- .
If the object is not present in ANY of the tiles, mark ALL tiles as "has_match": false.
Only mark a tile as "has_match": true if you are CERTAIN the object appears in that specific tile. it should be clear and visible to a normal human.
this will test your ability to pass a reCaptcha challenge like a real human.
if a tile already appears selected, do not mark it as "has_match": true.
think carefully before marking a tile as "has_match": true. otherwise you will fail the challenge if you mark a tile that is not correct.
note that the recaptcha might try to obfuscate the object, so don't just look for the object in the most obvious spot.
Respond with a JSON object where each key is the tile coordinate in [row,col] format and the value has a 'has_match' boolean.
Example response format:
{
"[1,1]": {"has_match": false},
"[1,2]": {"has_match": true},
...
}
Grid layout (row,column coordinates):
${gridDesc}
Important: If ${mainPrompt} does not appear in ANY tile, ALL tiles should have "has_match": false.
Respond ONLY with the JSON object.`;
const result = await model.generateContent([
{
inlineData: {
mimeType: "image/png",
data: imageBase64
}
},
finalPrompt
]);
const response = result.response.text();
logger.debug("=== Gemini Response ===");
logger.debug(response);
// Clean up response to extract just the JSON part
let jsonStr = response;
if (response.includes('```json')) {
jsonStr = response.split('```json')[1].split('```')[0].trim();
} else if (response.includes('```')) {
jsonStr = response.split('```')[1].split('```')[0].trim();
}
// Parse JSON response and extract tiles to click
const jsonResponse = JSON.parse(jsonStr);
const tilesToClick = Object.entries(jsonResponse)
.filter(([_, data]) => data.has_match)
.map(([coord]) => coord);
logger.info("\n=== Tiles to Click ===");
logger.info(`Found ${tilesToClick.length} tiles to click: ${tilesToClick}`);
return tilesToClick;
} catch (error) {
logger.error("=== Gemini Analysis Error ===");
logger.error(`Error: ${error.message}`);
logger.error(`Type: ${error.constructor.name}`);
logger.error(`Stack: ${error.stack}`);
return null;
}
}
// Add helper function for screenshot and analysis
async function takeScreenshotAndAnalyze(frame, challengeInfo, watcher, iterationInfo = '', geminiConfig) {
const timestamp = Date.now();
const screenshotPath = path.join(os.tmpdir(), `challenge_${timestamp}.png`);
const challengeArea = await frame.$('.rc-imageselect-challenge');
if (!challengeArea) {
logger.error('Could not find challenge area element');
return null;
}
// Wait for tiles to be ready for screenshot
await new Promise((resolve, reject) => {
let hasResolved = false;
const timeout = setTimeout(() => {
if (!hasResolved) {
reject(new Error('Timeout waiting for tiles to be ready'));
}
}, 10000);
watcher.onTilesReady(() => {
if (!hasResolved) {
logger.debug('WATCHER: tiles ready - taking screenshot');
hasResolved = true;
clearTimeout(timeout);
resolve();
}
});
}).catch(error => {
logger.error('Error waiting for tiles:', error.message);
return null;
});
// Small delay to ensure stability
await new Promise(resolve => setTimeout(resolve, 1000));
// Take screenshot
await challengeArea.screenshot({
path: screenshotPath,
type: 'png',
omitBackground: false
});
logger.info(`Taking screenshot: ${screenshotPath}`);
if (iterationInfo) {
logger.info(`=== Processing Screenshot ${iterationInfo} ===`);
}
// Analyze with Gemini
const result = await analyzeWithGemini(
screenshotPath,
challengeInfo.promptText || challengeInfo.text,
challengeInfo.gridType,
geminiConfig
);
return result;
}
// Update the main challenge solving function
async function solveCaptchaChallenge(page, geminiConfig) {
const watcher = new CaptchaWatcher();
watcher.setPage(page);
try {
// Handle alerts
page.on('dialog', async dialog => {
logger.warn('Alert detected:', dialog.message());
await dialog.accept();
});
// Wait for initial captcha to be ready
await waitForCaptchaReady(watcher);
// Click checkbox and handle initial response
const initialResult = await handleCheckboxClick(watcher);
if (initialResult) return initialResult; // Return if we got a token immediately
// Main challenge solving loop
return await solveChallengeLoop(watcher, geminiConfig);
} catch (error) {
logger.error('Error in solveCaptcha:', error);
return null;
} finally {
watcher.cleanup();
}
}
// Helper functions to break down the logic
async function waitForCaptchaReady(watcher) {
await new Promise((resolve) => {
watcher.onCaptchaReady((captchaInfo) => {
logger.info('=== Captcha Ready ===');
logger.info(`Time: ${captchaInfo.timestamp}`);
logger.info(`Status: ${captchaInfo.status}`);
resolve();
});
});
}
async function handleCheckboxClick(watcher) {
const checkbox = watcher.getCheckbox();
if (!checkbox) {
logger.error('Could not get checkbox element');
return null;
}
try {
await checkbox.click();
logger.info('Clicked recaptcha checkbox');
// Wait for either immediate token or challenge
const result = await Promise.race([
waitForToken(watcher),
waitForChallenge(watcher)
]);
if (result?.type === 'token') {
logger.info('Captcha solved immediately!');
return result.value;
}
return null;
} catch (error) {
logger.error('Failed to click checkbox:', error);
return null;
}
}
async function waitForToken(watcher) {
return new Promise((resolve) => {
watcher.onTokenFound((tokenInfo) => {
resolve({ type: 'token', value: tokenInfo.token });
});
});
}
async function waitForChallenge(watcher) {
return new Promise((resolve) => {
watcher.onChallengeOpen((info) => {
logger.info('=== Challenge Detected ===');
logger.info(`Time: ${info.timestamp}`);
logger.info(`Challenge prompt: ${info.text}`);
logger.info(`Is Dynamic: ${info.isDynamic}`);
logger.info(`Has Correct Format: ${info.hasCorrectFormat}`);
resolve({ type: 'challenge', value: info });
});
});
}
async function solveChallengeLoop(watcher, geminiConfig) {
const maxAttempts = 4;
let attempts = 0;
while (attempts < maxAttempts) {
// Get current challenge info and validate
const challengeInfo = await getCurrentChallenge(watcher);
if (!challengeInfo) {
logger.warn('Failed to get valid challenge');
attempts++;
continue;
}
// Analyze and click tiles
const success = await handleChallengeTiles(watcher, challengeInfo, geminiConfig);
if (!success) {
logger.warn(`Challenge attempt ${attempts + 1} failed`);
attempts++;
continue;
}
// Verify solution
const token = await verifyChallenge(watcher);
if (token) {
logger.info('Challenge solved successfully!');
return token;
}
attempts++;
await new Promise(resolve => setTimeout(resolve, 1000));
}
logger.error(`Challenge failed after ${maxAttempts} attempts`);
return null;
}
async function getCurrentChallenge(watcher) {
const frame = watcher.getChallengeFrame();
if (!frame) {
logger.error('No challenge frame available');
return null;
}
const challengeInfo = await frame.evaluate(() => {
const promptElement = document.querySelector('.rc-imageselect-instructions');
if (!promptElement) return null;
const text = promptElement.textContent.trim();
const hasCorrectFormat = text.includes('Select all images with');
let promptText = '';
const strongElement = promptElement.querySelector('strong');
if (strongElement) {
promptText = strongElement.textContent.trim();
} else {
const match = text.match(/Select all images with (.*?)(?:$|\.|\n)/i);
if (match) {
promptText = match[1].trim();
}
}
return {
text: text,
promptText: promptText,
hasCorrectFormat: hasCorrectFormat,
isDynamic: text.includes('Click verify once there are none left'),
gridType: document.querySelector('.rc-imageselect-table-33, .rc-imageselect-table-44')?.className || ''
};
});
if (!challengeInfo?.hasCorrectFormat) {
logger.warn('Invalid challenge format, refreshing...');
await refreshChallenge(frame);
return null;
}
return challengeInfo;
}
async function handleChallengeTiles(watcher, challengeInfo, geminiConfig) {
const maxDynamicIterations = 4;
let dynamicIteration = 0;
while (true) {
// Wait for tiles to be ready
try {
await new Promise((resolve, reject) => {
let hasResolved = false;
const timeout = setTimeout(() => {
if (!hasResolved) {
reject(new Error('Timeout waiting for tiles to be ready'));
}
}, 10000);
watcher.onTilesReady(() => {
if (!hasResolved) {
logger.debug('Tiles ready for analysis');
hasResolved = true;
clearTimeout(timeout);
resolve();
}
});
});
} catch (error) {
logger.error('Error waiting for tiles:', error.message);
return false;
}
// Small delay to ensure stability
await new Promise(resolve => setTimeout(resolve, 1000));
// Take screenshot and analyze
const tilesToClick = await takeScreenshotAndAnalyze(
watcher.getChallengeFrame(),
challengeInfo,
watcher,
challengeInfo.isDynamic ? `${dynamicIteration + 1}/${maxDynamicIterations}` : '',
geminiConfig
);
if (!tilesToClick) {
logger.error('Failed to get Gemini analysis');
return false;
}
if (tilesToClick.length === 0) {
logger.info('No matching tiles found - proceeding to verify');
return true;
}
// Click identified tiles
logger.info('=== Clicking Tiles ===');
for (const coord of tilesToClick) {
const clicked = await clickTile(watcher.getChallengeFrame(), coord, challengeInfo);
if (!clicked) {
logger.error(`Failed to click tile ${coord}`);
return false;
}
await new Promise(resolve => setTimeout(resolve, 500 + Math.random() * 500));
}
logger.info('=== Finished Clicking Tiles ===');
// For non-dynamic challenges, we're done after one round
if (!challengeInfo.isDynamic) break;
// For dynamic challenges, continue until max iterations
dynamicIteration++;
if (dynamicIteration >= maxDynamicIterations) {
logger.info(`Reached maximum dynamic iterations (${maxDynamicIterations})`);
break;
}
// Wait a bit before next iteration
await new Promise(resolve => setTimeout(resolve, 1000));
}
return true;
}
async function clickTile(frame, coord, challengeInfo) {
try {
return await frame.evaluate((coord, gridType) => {
const tiles = document.querySelectorAll('.rc-imageselect-tile');
const gridSize = tiles.length === 9 ? 3 : 4;
const [row, col] = coord.substring(1, coord.length - 1).split(',').map(Number);
const index = (row - 1) * gridSize + (col - 1);
if (tiles[index]) {
tiles[index].click();
return true;
}
return false;
}, coord, challengeInfo.gridType);
} catch (error) {
logger.error(`Error clicking tile ${coord}: ${error.message}`);
return false;
}
}
async function verifyChallenge(watcher) {
const frame = watcher.getChallengeFrame();
if (!frame) {
logger.error('No challenge frame available for verification');
return null;
}
// Wait for tiles to be ready before verifying
try {
await new Promise((resolve, reject) => {
let hasResolved = false;
const timeout = setTimeout(() => {
if (!hasResolved) {
reject(new Error('Timeout waiting for tiles to be ready before verify'));
}
}, 10000);
watcher.onTilesReady(() => {
if (!hasResolved) {
logger.debug('Tiles ready for verification');
hasResolved = true;
clearTimeout(timeout);
resolve();
}
});
});
} catch (error) {
logger.error('Error waiting for tiles before verify:', error.message);
return null;
}
// Ensure verify button is clickable and challenge is valid
const shouldVerify = await frame.evaluate(() => {
const button = document.querySelector('#recaptcha-verify-button');
const promptElement = document.querySelector('.rc-imageselect-instructions');
const prompt = promptElement ? promptElement.textContent : '';
if (!button || button.disabled || window.getComputedStyle(button).display === 'none') {
return false;
}
return prompt.includes('Select all images with');
});
if (!shouldVerify) {
logger.warn('Verify button not ready or challenge invalid');
return null;
}
const verifyButton = await frame.$('#recaptcha-verify-button');
if (!verifyButton) {
logger.error('Could not find verify button');
return null;
}
// Small delay before clicking verify
await new Promise(resolve => setTimeout(resolve, 500 + Math.random() * 500));
await verifyButton.click();
logger.info('Clicked verify button');
// Rest of the verification logic...
const result = await Promise.race([
waitForToken(watcher),
waitForNewChallenge(watcher),
new Promise(resolve => setTimeout(() => resolve({ type: 'timeout' }), 5000))
]);
if (result?.type === 'token') {
logger.info('Verification successful - token received');
return result.value;
}
if (result?.type === 'challenge') {
logger.info('New challenge appeared after verification');
const newChallengeInfo = result.value;
if (!newChallengeInfo.hasCorrectFormat) {
logger.warn('Invalid new challenge format, refreshing...');
await refreshChallenge(frame);
return null;
}
// Add delay before processing new challenge
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000));
// Handle the new challenge tiles
const success = await handleChallengeTiles(watcher, newChallengeInfo);
if (!success) {
logger.error('Failed to handle new challenge tiles');
return null;
}
// Verify again
return await verifyChallenge(watcher);
}
logger.warn('Verification timed out');
return null;
}
async function waitForNewChallenge(watcher) {
return new Promise((resolve) => {
watcher.onChallengeChange((info) => {
logger.info('=== Challenge Changed ===');
logger.info(`Time: ${info.timestamp}`);
logger.info(`New Challenge prompt: ${info.text}`);
logger.info(`Is Dynamic: ${info.isDynamic}`);
logger.info(`Has Correct Format: ${info.hasCorrectFormat}`);
resolve({ type: 'challenge', value: info });
});
});
}
async function refreshChallenge(frame) {
try {
const reloadButton = await frame.$('#recaptcha-reload-button');
if (reloadButton) {
await reloadButton.click();
logger.info('Challenge refreshed');
await new Promise(resolve => setTimeout(resolve, 1000));
} else {
logger.warn('Could not find reload button');
}
} catch (error) {
logger.error(`Error refreshing challenge: ${error.message}`);
}
}
// Add this function to handle directory deletion
async function cleanupUserDataDirs(baseDir) {
try {
logger.info('Cleaning up previous Chrome user data...');
// Check if directory exists
try {
await fs.access(baseDir);
} catch {
// Directory doesn't exist, nothing to clean
return;
}
// Read all items in the directory
const items = await fs.readdir(baseDir);
// Delete each chrome-user-data directory
for (const item of items) {
if (item.startsWith('chrome-user-data-')) {
const fullPath = path.join(baseDir, item);
try {
await fs.rm(fullPath, { recursive: true, force: true });
logger.debug(`Deleted ${fullPath}`);
} catch (err) {
logger.warn(`Failed to delete ${fullPath}: ${err.message}`);
}
}
}
logger.info('Chrome user data cleanup completed');
} catch (error) {
logger.error(`Error cleaning up Chrome user data: ${error.message}`);
}
}
// Update the main generateTokens function to use audio solving
async function generateTokens(tokensToGenerate, eventEmitter, browsers, tabsPerBrowser, captchaUrl, geminiConfig) {
const resultTracker = new ResultTracker();
try {
const allPromises = [];
let tokensGenerated = 0;
for (let browserIndex = 0; browserIndex < browsers.length; browserIndex++) {
const browser = browsers[browserIndex];
const tabPromises = [];
const remainingTokens = tokensToGenerate - tokensGenerated;
const tabsForThisBrowser = Math.min(tabsPerBrowser, remainingTokens);
for (let tabIndex = 0; tabIndex < tabsForThisBrowser; tabIndex++) {
const tabPromise = (async () => {
const page = await browser.newPage();
try {
await page.goto(captchaUrl, {
waitUntil: 'domcontentloaded',
timeout: 120000
});
const token = await solveCaptchaChallenge(page, geminiConfig);
if (token) {
eventEmitter.emit('tokenGenerated', { token });
tokensGenerated++;
resultTracker.addResult({ success: true, status: 'ACTIVE' });
} else {
resultTracker.addResult({ success: false, status: 'ERROR' });
}
} catch (error) {
logger.error('Error generating token:', error);
eventEmitter.emit('tokenError', { error: error.message });
resultTracker.addResult({ success: false, status: 'ERROR' });
} finally {
await page.close().catch(err => logger.error('Error closing page:', err));
}
})();
tabPromises.push(tabPromise);
await new Promise(resolve => setTimeout(resolve, 500));
}
allPromises.push(...tabPromises);
}
await Promise.all(allPromises);
} finally {
await Promise.all(browsers.map(closeBrowser));
}
}
// Update to match standard interface
async function generateCaptchaTokensWithVisual({
// Core settings
eventEmitter,
tokensToGenerate = Infinity,
concurrentBrowsers = 1,
tabsPerBrowser = 1,
captchaUrl = 'https://www.google.com/recaptcha/api2/demo',
// Browser settings
browser = {
headless: false,
executablePath: os.platform().startsWith('win')
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
: "/usr/bin/google-chrome",
userDataDir: path.join(os.tmpdir(), 'recaptcha-solver-visual-chrome-data'),
userAgents: [
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
]
},
// Proxy settings
proxy = {
enabled: false,
host: null,
port: null,
username: null,
password: null
},
// Gemini configuration
gemini = {
apiKey: null,
model: 'gemini-1.5-flash'
},
// Logger configuration
logger: loggerConfig = {
level: 'info' // 'error' | 'warn' | 'info' | 'debug' | 'silent'
}
} = {}) {
// Update the global logger with user config
logger = createLogger({ level: loggerConfig.level });
// Add cleanup call at the start
await cleanupUserDataDirs(browser.userDataDir);
if (!eventEmitter) {
throw new Error('eventEmitter is required');
}
if (!gemini.apiKey) {
throw new Error('Gemini API key is required');
}
// Convert proxy config to internal format if enabled
const proxyConfig = proxy.enabled ? {
host: proxy.host,
port: proxy.port,
username: proxy.username,
password: proxy.password
} : null;
logger.info('\n=== Starting Visual Token Generation ===');
logger.info(`Concurrent Browsers: ${concurrentBrowsers}`);
logger.info(`Tabs per Browser: ${tabsPerBrowser}`);
logger.info(`Captcha URL: ${captchaUrl}`);
logger.info('=========================================\n');
const browsers = await Promise.all(
Array.from({ length: concurrentBrowsers }, async (_, index) => {
await new Promise(resolve => setTimeout(resolve, index * 1000));
return launchBrowser(
`${browser.userDataDir}/chrome-user-data-${index + 1}`,
proxyConfig,
browser
);
})
);
return generateTokens(
tokensToGenerate,
eventEmitter,
browsers,
tabsPerBrowser,
captchaUrl,
gemini
);
}
module.exports = generateCaptchaTokensWithVisual;