const express = require("express"); const puppeteer = require("puppeteer-extra"); const StealthPlugin = require("puppeteer-extra-plugin-stealth"); const { execSync } = require("child_process"); const fs = require("fs"); const path = require("path"); const os = require("os"); puppeteer.use(StealthPlugin()); const app = express(); app.use(express.json({ limit: "10mb" })); const PORT = process.env.PORT || 7860; const POOL_SIZE = 2; const MAX_QUEUE = 20; const PAGE_TIMEOUT = 30000; const TMP_DIR = path.join(os.tmpdir(), "pp_files"); if (!fs.existsSync(TMP_DIR)) fs.mkdirSync(TMP_DIR, { recursive: true }); const pool = { browsers: [], available: [], waiters: [], stats: { totalRequests: 0, completed: 0, failed: 0, queued: 0 }, }; function getChromiumPath() { const candidates = [ "/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/snap/bin/chromium", ]; for (const p of candidates) { try { execSync(`test -f ${p}`); return p; } catch {} } return null; } async function launchBrowser(id) { const chromiumPath = getChromiumPath(); const opts = { headless: "new", args: [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--no-zygote", "--single-process", "--disable-extensions", "--mute-audio", "--no-first-run", "--window-size=1280,720", ], ignoreHTTPSErrors: true, }; if (chromiumPath) opts.executablePath = chromiumPath; const browser = await puppeteer.launch(opts); browser._poolId = id; browser.on("disconnected", () => restartBrowser(id)); console.log(`[Pool] Browser #${id} up (PID: ${browser.process()?.pid})`); return browser; } async function restartBrowser(id) { pool.browsers[id] = null; pool.available = pool.available.filter((b) => b._poolId !== id); try { await new Promise((r) => setTimeout(r, 2000)); const browser = await launchBrowser(id); pool.browsers[id] = browser; releaseSlot(browser); } catch { setTimeout(() => restartBrowser(id), 5000); } } async function initPool() { console.log(`[Pool] Starting ${POOL_SIZE} browsers...`); for (let i = 0; i < POOL_SIZE; i++) { try { const browser = await launchBrowser(i); pool.browsers[i] = browser; pool.available.push(browser); } catch (e) { console.error(`[Pool] Browser #${i} failed:`, e.message); } } console.log(`[Pool] Ready — ${pool.available.length}/${POOL_SIZE} up`); } function acquireBrowser() { return new Promise((resolve, reject) => { if (pool.waiters.length >= MAX_QUEUE) return reject(new Error(`Queue full (max ${MAX_QUEUE})`)); if (pool.available.length > 0) { const b = pool.available.shift(); pool.stats.queued = pool.waiters.length; return resolve(b); } pool.waiters.push({ resolve, reject }); pool.stats.queued = pool.waiters.length; console.log(`[Queue] Waiting — size: ${pool.waiters.length}`); }); } function releaseSlot(browser) { if (pool.waiters.length > 0) { const w = pool.waiters.shift(); pool.stats.queued = pool.waiters.length; w.resolve(browser); } else { pool.available.push(browser); } } async function withPage(taskFn) { pool.stats.totalRequests++; const browser = await acquireBrowser(); let page = null; try { page = await browser.newPage(); await page.setUserAgent( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ); await page.setViewport({ width: 1280, height: 720 }); await page.setDefaultNavigationTimeout(PAGE_TIMEOUT); await page.setDefaultTimeout(PAGE_TIMEOUT); const result = await taskFn(page, browser); pool.stats.completed++; return result; } finally { if (page && !page.isClosed()) await page.close().catch(() => {}); releaseSlot(browser); } } // ─── POST /run — kirim raw JS code string, page & utils tersedia ────────────── app.post("/run", async (req, res) => { const { code } = req.body; if (!code) return res.status(400).json({ error: "code required" }); const start = Date.now(); const logs = []; const tmpId = Date.now() + "_" + Math.random().toString(36).slice(2); const tmpBase = path.join(TMP_DIR, tmpId); try { const result = await withPage(async (page, browser) => { const fakeConsole = { log: (...a) => logs.push(a.map(String).join(" ")), error: (...a) => logs.push("[ERR] " + a.map(String).join(" ")), warn: (...a) => logs.push("[WARN] " + a.map(String).join(" ")), }; const ctx = { page, browser, console: fakeConsole, __file: tmpBase, goto: (url, opts) => page.goto(url, opts), screenshot: (opts = {}) => page.screenshot({ path: opts.path || `${tmpBase}.png`, fullPage: opts.fullPage || false, ...opts }), evaluate: (fn) => page.evaluate(fn), waitForSelector: (sel, opts) => page.waitForSelector(sel, opts), content: () => page.content(), url: () => page.url(), title: () => page.title(), click: (sel) => page.click(sel), type: (sel, text) => page.type(sel, text), select: (sel, val) => page.select(sel, val), waitForNavigation: (opts) => page.waitForNavigation(opts), setViewport: (vp) => page.setViewport(vp), setUserAgent: (ua) => page.setUserAgent(ua), }; const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; const fn = new AsyncFunction(...Object.keys(ctx), code); return await fn(...Object.values(ctx)); }); const files = []; const exts = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".pdf", ".html", ".txt", ".json"]; for (const ext of exts) { const fp = `${tmpBase}${ext}`; if (fs.existsSync(fp)) { const buf = fs.readFileSync(fp); files.push({ fileName: path.basename(fp), extension: ext.replace(".", ""), base64: buf.toString("base64"), size: buf.length, }); fs.unlinkSync(fp); } } res.json({ ok: true, elapsed: Date.now() - start, result: result ?? null, logs, files }); } catch (err) { pool.stats.failed++; res.status(500).json({ ok: false, error: err.message, logs, elapsed: Date.now() - start }); } }); // ─── Standard endpoints ─────────────────────────────────────────────────────── app.get("/", (req, res) => { res.json({ service: "Puppeteer Pool Server", pool: { total: POOL_SIZE, available: pool.available.length, queued: pool.waiters.length }, stats: pool.stats, endpoints: { "GET /status": "Pool status", "POST /run": "Run raw code { code } — tersedia: page, browser, goto, screenshot, evaluate, waitForSelector, click, type, content, url, title", "POST /scrape": "Scrape { url, waitFor? }", "POST /screenshot": "Screenshot { url, fullPage? }", "POST /html": "Raw HTML { url }", "POST /custom": "JS evaluate { url, script }", }, }); }); app.get("/status", (req, res) => { res.json({ available: pool.available.length, queued: pool.waiters.length, total: POOL_SIZE, browsers: pool.browsers.map((b, i) => ({ id: i, alive: b !== null, connected: b?.isConnected?.() ?? false, })), stats: pool.stats, }); }); app.post("/scrape", async (req, res) => { const { url, waitFor } = req.body; if (!url) return res.status(400).json({ error: "url required" }); const start = Date.now(); try { const data = await withPage(async (page) => { await page.goto(url, { waitUntil: waitFor || "domcontentloaded" }); return await page.evaluate(() => ({ title: document.title, url: location.href, description: document.querySelector('meta[name="description"]')?.content || "", h1: Array.from(document.querySelectorAll("h1")).map((h) => h.innerText.trim()), text: document.body?.innerText?.slice(0, 3000) || "", })); }); res.json({ ok: true, elapsed: Date.now() - start, data }); } catch (err) { pool.stats.failed++; res.status(500).json({ ok: false, error: err.message, elapsed: Date.now() - start }); } }); app.post("/screenshot", async (req, res) => { const { url, fullPage = false, waitFor } = req.body; if (!url) return res.status(400).json({ error: "url required" }); const start = Date.now(); try { const image = await withPage(async (page) => { await page.goto(url, { waitUntil: waitFor || "networkidle2" }); return await page.screenshot({ type: "png", fullPage, encoding: "base64" }); }); res.json({ ok: true, elapsed: Date.now() - start, image, mime: "image/png" }); } catch (err) { pool.stats.failed++; res.status(500).json({ ok: false, error: err.message, elapsed: Date.now() - start }); } }); app.post("/html", async (req, res) => { const { url, waitFor } = req.body; if (!url) return res.status(400).json({ error: "url required" }); const start = Date.now(); try { const html = await withPage(async (page) => { await page.goto(url, { waitUntil: waitFor || "domcontentloaded" }); return await page.content(); }); res.json({ ok: true, elapsed: Date.now() - start, html }); } catch (err) { pool.stats.failed++; res.status(500).json({ ok: false, error: err.message, elapsed: Date.now() - start }); } }); app.post("/custom", async (req, res) => { const { url, script, waitFor } = req.body; if (!url || !script) return res.status(400).json({ error: "url and script required" }); const start = Date.now(); try { const result = await withPage(async (page) => { await page.goto(url, { waitUntil: waitFor || "domcontentloaded" }); const fn = new Function(`return (${script})`); return await page.evaluate(fn()); }); res.json({ ok: true, elapsed: Date.now() - start, result }); } catch (err) { pool.stats.failed++; res.status(500).json({ ok: false, error: err.message, elapsed: Date.now() - start }); } }); async function main() { await initPool(); app.listen(PORT, "0.0.0.0", () => { console.log(`\n🚀 Ready at http://0.0.0.0:${PORT}`); }); } main().catch((e) => { console.error("Fatal:", e); process.exit(1); });