Spaces:
Sleeping
Sleeping
| const express = require('express'); | |
| const { chromium } = require('playwright'); | |
| const tf = require('@tensorflow/tfjs-node'); | |
| const axios = require('axios'); | |
| const sharp = require('sharp'); | |
| const app = express(); | |
| app.use(express.json()); | |
| const HCAPTCHA_FRAME = "iframe[src*='hcaptcha.com/captcha']"; | |
| const HCAPTCHA_CHECKBOX = "iframe[src*='hcaptcha.com/checkbox']"; | |
| const CHALLENGE_CONTAINER = ".challenge-container"; | |
| let model = null; | |
| function log(...args) { | |
| console.log('[hCaptcha API]', new Date().toISOString(), ...args); | |
| } | |
| function delay(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| function randomDelay(min, max) { | |
| return delay(Math.floor(Math.random() * (max - min + 1)) + min); | |
| } | |
| async function loadModel() { | |
| if (!model) { | |
| try { | |
| log('Loading ML model...'); | |
| model = await tf.loadLayersModel('https://tfhub.dev/google/tfjs-model/imagenet/mobilenet_v2_100_224/classification/3/default/1', { | |
| fromTFHub: true | |
| }); | |
| log('ML model loaded successfully'); | |
| } catch (error) { | |
| log('Failed to load model, using fallback:', error.message); | |
| model = null; | |
| } | |
| } | |
| return model; | |
| } | |
| async function preprocessImage(imageBuffer) { | |
| const processedImage = await sharp(imageBuffer) | |
| .resize(224, 224) | |
| .toBuffer(); | |
| const tensor = tf.node.decodeImage(processedImage, 3) | |
| .toFloat() | |
| .div(255.0) | |
| .expandDims(0); | |
| return tensor; | |
| } | |
| async function analyzeImageWithML(imageUrl) { | |
| try { | |
| const response = await axios.get(imageUrl, { responseType: 'arraybuffer' }); | |
| const imageBuffer = Buffer.from(response.data); | |
| const tensor = await preprocessImage(imageBuffer); | |
| if (model) { | |
| const predictions = await model.predict(tensor); | |
| const predArray = await predictions.data(); | |
| const topPredictions = Array.from(predArray) | |
| .map((prob, idx) => ({ class: idx, probability: prob })) | |
| .sort((a, b) => b.probability - a.probability) | |
| .slice(0, 5); | |
| tensor.dispose(); | |
| predictions.dispose(); | |
| return topPredictions; | |
| } | |
| tensor.dispose(); | |
| return null; | |
| } catch (error) { | |
| log('ML analysis error:', error.message); | |
| return null; | |
| } | |
| } | |
| async function analyzeImageFeatures(frame, imageElement) { | |
| return await frame.evaluate((img) => { | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| ctx.drawImage(img, 0, 0); | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| let brightness = 0; | |
| let edges = 0; | |
| const colors = new Set(); | |
| let redDominance = 0; | |
| let blueDominance = 0; | |
| let greenDominance = 0; | |
| for (let i = 0; i < imageData.data.length; i += 4) { | |
| const r = imageData.data[i]; | |
| const g = imageData.data[i + 1]; | |
| const b = imageData.data[i + 2]; | |
| brightness += (r + g + b) / 3; | |
| if (r > g && r > b) redDominance++; | |
| if (b > r && b > g) blueDominance++; | |
| if (g > r && g > b) greenDominance++; | |
| const colorKey = `${Math.floor(r/50)*50},${Math.floor(g/50)*50},${Math.floor(b/50)*50}`; | |
| colors.add(colorKey); | |
| if (i + canvas.width * 4 < imageData.data.length) { | |
| if (Math.abs(r - imageData.data[i + canvas.width * 4]) > 30) edges++; | |
| } | |
| } | |
| const totalPixels = imageData.data.length / 4; | |
| return { | |
| brightness: brightness / totalPixels, | |
| edges: edges, | |
| colorDiversity: colors.size, | |
| redDominance: redDominance / totalPixels, | |
| blueDominance: blueDominance / totalPixels, | |
| greenDominance: greenDominance / totalPixels | |
| }; | |
| }, imageElement); | |
| } | |
| async function classifyImage(imageUrl, challengeText, features) { | |
| const mlPredictions = await analyzeImageWithML(imageUrl); | |
| let score = 0; | |
| const challenge = challengeText.toLowerCase(); | |
| if (mlPredictions && mlPredictions.length > 0) { | |
| const topClass = mlPredictions[0].class; | |
| if (challenge.includes('vehicle') || challenge.includes('car') || challenge.includes('bus') || challenge.includes('truck')) { | |
| if ((topClass >= 400 && topClass <= 900) || features.edges > 1000) { | |
| score += 3; | |
| } | |
| } else if (challenge.includes('animal') || challenge.includes('cat') || challenge.includes('dog') || challenge.includes('bird')) { | |
| if ((topClass >= 0 && topClass <= 400) || features.colorDiversity > 80) { | |
| score += 3; | |
| } | |
| } else if (challenge.includes('plant') || challenge.includes('tree') || challenge.includes('flower')) { | |
| if (features.greenDominance > 0.3 || (topClass >= 900 && topClass <= 1000)) { | |
| score += 3; | |
| } | |
| } else if (challenge.includes('sky') || challenge.includes('cloud')) { | |
| if (features.blueDominance > 0.4 || features.brightness > 150) { | |
| score += 3; | |
| } | |
| } else if (challenge.includes('water') || challenge.includes('ocean') || challenge.includes('lake')) { | |
| if (features.blueDominance > 0.3 || features.brightness > 100) { | |
| score += 3; | |
| } | |
| } | |
| } | |
| if (challenge.includes('vehicle') || challenge.includes('car') || challenge.includes('bus')) { | |
| if (features.edges > 1200) score += 2; | |
| if (features.colorDiversity > 70) score += 1; | |
| } else if (challenge.includes('animal') || challenge.includes('cat') || challenge.includes('dog')) { | |
| if (features.colorDiversity > 90) score += 2; | |
| if (features.edges > 800) score += 1; | |
| } else if (challenge.includes('plant') || challenge.includes('tree')) { | |
| if (features.greenDominance > 0.25) score += 2; | |
| if (features.colorDiversity > 60) score += 1; | |
| } else if (challenge.includes('sky') || challenge.includes('water')) { | |
| if (features.brightness > 140) score += 2; | |
| if (features.blueDominance > 0.3) score += 1; | |
| } else if (challenge.includes('building') || challenge.includes('house')) { | |
| if (features.edges > 1500) score += 2; | |
| if (features.colorDiversity < 70) score += 1; | |
| } | |
| return score; | |
| } | |
| async function findPuzzleMatch(frame) { | |
| log("Analyzing puzzle piece position with ML"); | |
| const puzzleBox = await frame.$eval('.challenge-image', el => { | |
| const rect = el.getBoundingClientRect(); | |
| return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; | |
| }); | |
| const pieceBox = await frame.$eval('.draggable-piece', el => { | |
| const rect = el.getBoundingClientRect(); | |
| return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; | |
| }); | |
| const gapX = await frame.evaluate(() => { | |
| const canvas = document.querySelector('.challenge-image'); | |
| const ctx = canvas.getContext('2d'); | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| let darkestX = 0; | |
| let minBrightness = 999; | |
| let maxEdgeCount = 0; | |
| let edgeX = 0; | |
| for (let x = 0; x < canvas.width; x += 3) { | |
| let columnBrightness = 0; | |
| let edgeCount = 0; | |
| for (let y = 0; y < canvas.height; y++) { | |
| const i = (y * canvas.width + x) * 4; | |
| const brightness = (imageData.data[i] + imageData.data[i+1] + imageData.data[i+2]) / 3; | |
| columnBrightness += brightness; | |
| if (y > 0) { | |
| const prevI = ((y-1) * canvas.width + x) * 4; | |
| const diff = Math.abs(brightness - (imageData.data[prevI] + imageData.data[prevI+1] + imageData.data[prevI+2]) / 3); | |
| if (diff > 40) edgeCount++; | |
| } | |
| } | |
| columnBrightness /= canvas.height; | |
| if (columnBrightness < minBrightness) { | |
| minBrightness = columnBrightness; | |
| darkestX = x; | |
| } | |
| if (edgeCount > maxEdgeCount) { | |
| maxEdgeCount = edgeCount; | |
| edgeX = x; | |
| } | |
| } | |
| return (darkestX + edgeX) / 2; | |
| }); | |
| return { | |
| targetX: puzzleBox.x + gapX, | |
| targetY: puzzleBox.y + puzzleBox.height / 2, | |
| startX: pieceBox.x + pieceBox.width / 2, | |
| startY: pieceBox.y + pieceBox.height / 2 | |
| }; | |
| } | |
| async function humanMouseMove(page, startX, startY, endX, endY) { | |
| await page.mouse.move(startX, startY); | |
| await randomDelay(50, 150); | |
| await page.mouse.down(); | |
| await randomDelay(100, 200); | |
| const steps = 15 + Math.floor(Math.random() * 10); | |
| const controlX = (startX + endX) / 2 + (Math.random() - 0.5) * 50; | |
| const controlY = (startY + endY) / 2 + (Math.random() - 0.5) * 50; | |
| for (let i = 1; i <= steps; i++) { | |
| const t = i / steps; | |
| const x = Math.pow(1 - t, 2) * startX + 2 * (1 - t) * t * controlX + Math.pow(t, 2) * endX; | |
| const y = Math.pow(1 - t, 2) * startY + 2 * (1 - t) * t * controlY + Math.pow(t, 2) * endY; | |
| await page.mouse.move(x + (Math.random() - 0.5) * 2, y + (Math.random() - 0.5) * 2); | |
| await randomDelay(30, 80); | |
| } | |
| await randomDelay(150, 300); | |
| await page.mouse.up(); | |
| await randomDelay(200, 400); | |
| } | |
| async function solveDragDropChallenge(page, challengeFrame) { | |
| log("Solving drag & drop challenge"); | |
| const position = await findPuzzleMatch(challengeFrame); | |
| await humanMouseMove(page, position.startX, position.startY, position.targetX, position.targetY); | |
| await randomDelay(500, 1000); | |
| return true; | |
| } | |
| async function solveImageChallenge(challengeFrame, challengeText) { | |
| log("Solving image selection challenge with ML:", challengeText); | |
| const images = await challengeFrame.$$('.task-image'); | |
| log(`Found ${images.length} images`); | |
| const imageData = []; | |
| for (let i = 0; i < images.length; i++) { | |
| const img = images[i]; | |
| const features = await analyzeImageFeatures(challengeFrame, img); | |
| const imageUrl = await img.evaluate(el => el.src); | |
| const score = await classifyImage(imageUrl, challengeText, features); | |
| imageData.push({ index: i, score, features }); | |
| await randomDelay(100, 200); | |
| } | |
| imageData.sort((a, b) => b.score - a.score); | |
| const threshold = imageData[0].score * 0.7; | |
| const selectedIndices = imageData | |
| .filter(data => data.score >= threshold) | |
| .map(data => data.index); | |
| if (selectedIndices.length === 0) { | |
| const randomCount = Math.min(3, Math.floor(Math.random() * 3) + 1); | |
| for (let i = 0; i < randomCount; i++) { | |
| selectedIndices.push(imageData[i].index); | |
| } | |
| } | |
| log(`Selecting ${selectedIndices.length} images based on ML:`, selectedIndices); | |
| for (const index of selectedIndices) { | |
| await randomDelay(300, 700); | |
| await images[index].click(); | |
| await randomDelay(200, 400); | |
| } | |
| await randomDelay(500, 1000); | |
| const submitButton = await challengeFrame.$('.button-submit'); | |
| if (submitButton) { | |
| await submitButton.click(); | |
| await randomDelay(1500, 2500); | |
| } | |
| return true; | |
| } | |
| async function solveCaptcha(url, options = {}) { | |
| const { | |
| waitTimeout = 30000, | |
| maxRetries = 3 | |
| } = options; | |
| await loadModel(); | |
| const browser = await chromium.launch({ | |
| headless: true, | |
| args: [ | |
| '--no-sandbox', | |
| '--disable-setuid-sandbox', | |
| '--disable-dev-shm-usage', | |
| '--disable-accelerated-2d-canvas', | |
| '--disable-gpu' | |
| ] | |
| }); | |
| const context = await browser.newContext({ | |
| viewport: { width: 1280, height: 720 }, | |
| userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' | |
| }); | |
| const page = await context.newPage(); | |
| try { | |
| log(`Navigating to: ${url}`); | |
| await page.goto(url, { waitUntil: 'networkidle', timeout: waitTimeout }); | |
| await page.waitForSelector(HCAPTCHA_CHECKBOX, { state: "attached", timeout: waitTimeout }); | |
| const checkboxElement = await page.$(HCAPTCHA_CHECKBOX); | |
| if (!checkboxElement) { | |
| throw new Error("Could not find hCaptcha checkbox iframe"); | |
| } | |
| const checkboxFrame = await checkboxElement.contentFrame(); | |
| if (!checkboxFrame) { | |
| throw new Error("Could not find hCaptcha checkbox iframe content"); | |
| } | |
| const checkboxRect = await checkboxElement.boundingBox(); | |
| if (!checkboxRect) { | |
| throw new Error("Could not get checkbox position"); | |
| } | |
| log("Clicking checkbox with mouse"); | |
| const checkboxX = checkboxRect.x + checkboxRect.width / 2; | |
| const checkboxY = checkboxRect.y + checkboxRect.height / 2; | |
| await page.mouse.move(checkboxX, checkboxY); | |
| await randomDelay(100, 300); | |
| await page.mouse.click(checkboxX, checkboxY); | |
| await randomDelay(2000, 3000); | |
| const challengeElement = await page.$(HCAPTCHA_FRAME); | |
| if (!challengeElement) { | |
| log("No challenge appeared - likely passed"); | |
| const cookies = await context.cookies(); | |
| await browser.close(); | |
| return { | |
| success: true, | |
| message: "No challenge required", | |
| cookies: cookies | |
| }; | |
| } | |
| const challengeFrame = await challengeElement.contentFrame(); | |
| if (!challengeFrame) { | |
| throw new Error("Could not find hCaptcha challenge iframe content"); | |
| } | |
| await challengeFrame.waitForSelector(CHALLENGE_CONTAINER, { timeout: waitTimeout }); | |
| let attempts = 0; | |
| let solved = false; | |
| while (!solved && attempts < maxRetries) { | |
| attempts++; | |
| log(`Attempt ${attempts}/${maxRetries}`); | |
| const challengeText = await challengeFrame.$eval('.prompt-text', el => el.textContent).catch(() => ''); | |
| log("Challenge prompt:", challengeText); | |
| const isDragDrop = await challengeFrame.$('.draggable-piece') !== null; | |
| try { | |
| if (isDragDrop) { | |
| await solveDragDropChallenge(page, challengeFrame); | |
| } else { | |
| await solveImageChallenge(challengeFrame, challengeText); | |
| } | |
| await randomDelay(2000, 3000); | |
| const stillHasChallenge = await page.$(HCAPTCHA_FRAME) !== null; | |
| if (!stillHasChallenge) { | |
| solved = true; | |
| log("Challenge solved successfully with ML!"); | |
| } | |
| } catch (error) { | |
| log(`Attempt ${attempts} failed:`, error.message); | |
| await randomDelay(1000, 2000); | |
| } | |
| } | |
| if (!solved) { | |
| await browser.close(); | |
| return { | |
| success: false, | |
| message: "Could not solve hCaptcha after maximum retries" | |
| }; | |
| } | |
| const cookies = await context.cookies(); | |
| const token = await page.evaluate(() => { | |
| const responseInput = document.querySelector('[name="h-captcha-response"]'); | |
| return responseInput ? responseInput.value : null; | |
| }); | |
| await browser.close(); | |
| return { | |
| success: true, | |
| message: "hCaptcha solved successfully with ML", | |
| token: token, | |
| cookies: cookies | |
| }; | |
| } catch (error) { | |
| await browser.close(); | |
| throw error; | |
| } | |
| } | |
| app.post('/solve', async (req, res) => { | |
| try { | |
| const { url, options } = req.body; | |
| if (!url) { | |
| return res.status(400).json({ | |
| success: false, | |
| error: 'URL is required' | |
| }); | |
| } | |
| log(`Received solve request for: ${url}`); | |
| const result = await solveCaptcha(url, options); | |
| res.json(result); | |
| } catch (error) { | |
| log('Error:', error.message); | |
| res.status(500).json({ | |
| success: false, | |
| error: error.message | |
| }); | |
| } | |
| }); | |
| app.get('/health', (req, res) => { | |
| res.json({ status: 'ok', model: model ? 'loaded' : 'fallback', timestamp: new Date().toISOString() }); | |
| }); | |
| const PORT = process.env.PORT || 7860; | |
| app.listen(PORT, '0.0.0.0', () => { | |
| log(`hCaptcha Solver API with ML running on port ${PORT}`); | |
| }); |