import { Router } from 'express'; import { exec } from 'child_process'; import { promisify } from 'util'; import path from 'path'; import { fileURLToPath } from 'url'; import { getForumSiteById, updateForumSite } from './db.js'; const execAsync = promisify(exec); const router = Router(); const FLARESOLVERR_URL = process.env.FLARESOLVERR_URL || 'http://localhost:8191'; const CHROMIUM_PATH = process.env.CHROMIUM_PATH || '/usr/bin/chromium-browser'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Refresh forum cookies using undetected_chromedriver (Python). * Runs login_helper.py via xvfb-run so Chrome runs in headed mode * with a virtual display — this is what lets Turnstile auto-solve. */ export async function refreshForumCookies(siteId) { const site = getForumSiteById(siteId); if (!site) throw new Error(`Forum site ${siteId} not found`); if (!site.username || !site.password) { throw new Error('Forum site has no saved credentials — set username and password first'); } const baseUrl = site.base_url || 'https://simpcity.su'; const loginUrl = `${baseUrl}/login/`; const helperPath = path.join(__dirname, 'login_helper.py'); console.log(`[flaresolverr] Refreshing cookies for site ${siteId} (${site.name})`); console.log(`[flaresolverr] Login URL: ${loginUrl}`); // Run the Python helper with xvfb-run for virtual display // Escape arguments for shell safety const escapedUrl = loginUrl.replace(/'/g, "'\\''"); const escapedUser = site.username.replace(/'/g, "'\\''"); const escapedPass = site.password.replace(/'/g, "'\\''"); const cmd = `xvfb-run --auto-servernum --server-args='-screen 0 1920x1080x24' python3 '${helperPath}' '${escapedUrl}' '${escapedUser}' '${escapedPass}'`; try { const { stdout, stderr } = await execAsync(cmd, { timeout: 120000, // 2 minutes maxBuffer: 10 * 1024 * 1024, env: { ...process.env, CHROMIUM_PATH }, }); // Log stderr (debug output from login_helper.py) if (stderr) { for (const line of stderr.split('\n').filter(Boolean)) { console.log(`[flaresolverr] ${line}`); } } // Parse JSON from stdout const result = JSON.parse(stdout.trim()); if (!result.ok) { throw new Error(result.error || 'Login failed'); } // Update DB with new cookies const expiresAt = new Date(Date.now() + 25 * 24 * 60 * 60 * 1000).toISOString(); updateForumSite(siteId, { cookies: result.cookies, cookie_expires_at: expiresAt, }); console.log(`[flaresolverr] Cookie refresh successful for site ${siteId}`); return result.cookies; } catch (err) { // If execAsync fails, the error might have stderr info if (err.stderr) { for (const line of err.stderr.split('\n').filter(Boolean)) { console.error(`[flaresolverr] ${line}`); } } // Try to parse stdout for a structured error if (err.stdout) { try { const result = JSON.parse(err.stdout.trim()); if (result.error) throw new Error(result.error); } catch (parseErr) { // Not JSON, use original error } } throw new Error(`Cookie refresh failed: ${err.message}`); } } // --- API Endpoints --- // Manual cookie refresh router.post('/api/flaresolverr/refresh/:siteId', async (req, res) => { const siteId = parseInt(req.params.siteId, 10); try { const cookieStr = await refreshForumCookies(siteId); res.json({ ok: true, cookies: cookieStr }); } catch (err) { console.error(`[flaresolverr] Refresh failed for site ${siteId}:`, err.message); res.status(500).json({ error: err.message }); } }); // Check if cookie refresh is available (Chromium + xvfb-run installed) router.get('/api/flaresolverr/status', async (_req, res) => { try { // Check for xvfb-run and chromium await execAsync('which xvfb-run && which chromium-browser || which chromium', { timeout: 5000 }); // Check for undetected_chromedriver python package await execAsync('python3 -c "import undetected_chromedriver"', { timeout: 5000 }); res.json({ available: true }); } catch { // Fallback: check FlareSolverr service try { const resp = await fetch(`${FLARESOLVERR_URL}/health`, { signal: AbortSignal.timeout(5000), }); res.json({ available: resp.ok }); } catch { res.json({ available: false, error: 'Neither undetected_chromedriver nor FlareSolverr available' }); } } }); export default router;