const express = require('express') const { chromium } = require('playwright-extra') const StealthPlugin = require('puppeteer-extra-plugin-stealth') const crypto = require('crypto') chromium.use(StealthPlugin()) const app = express() app.use(express.json()) const sessions = new Map() const SESSION_TIMEOUT = 10 * 60 * 1000 function makeId() { return crypto.randomBytes(8).toString('hex') } function touchSession(id) { const s = sessions.get(id) if (!s) return clearTimeout(s.timer) s.timer = setTimeout(async () => { await s.browser.close().catch(() => {}) sessions.delete(id) }, SESSION_TIMEOUT) } async function getSession(id, res) { const s = sessions.get(id) if (!s) { res.status(404).json({ error: 'Session not found or expired' }) return null } touchSession(id) return s } app.post('/session/create', async (req, res) => { try { const { headless = true, userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', viewport = { width: 1280, height: 720 }, locale = 'en-US', extraArgs = [] } = req.body || {} const browser = await chromium.launch({ headless, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-blink-features=AutomationControlled', ...extraArgs ] }) const context = await browser.newContext({ userAgent, viewport, locale }) const page = await context.newPage() const id = makeId() const timer = setTimeout(async () => { await browser.close().catch(() => {}) sessions.delete(id) }, SESSION_TIMEOUT) sessions.set(id, { browser, context, page, timer, createdAt: Date.now() }) res.json({ ok: true, sessionId: id }) } catch (err) { res.status(500).json({ error: err.message }) } }) app.post('/session/:id/goto', async (req, res) => { const s = await getSession(req.params.id, res) if (!s) return try { const { url, waitUntil = 'networkidle', timeout = 30000 } = req.body if (!url) return res.status(400).json({ error: 'Missing url' }) const response = await s.page.goto(url, { waitUntil, timeout }) res.json({ ok: true, status: response?.status(), url: s.page.url() }) } catch (err) { res.status(500).json({ error: err.message }) } }) app.post('/session/:id/content', async (req, res) => { const s = await getSession(req.params.id, res) if (!s) return try { const content = await s.page.content() const url = s.page.url() res.json({ ok: true, url, content }) } catch (err) { res.status(500).json({ error: err.message }) } }) app.post('/session/:id/cookies', async (req, res) => { const s = await getSession(req.params.id, res) if (!s) return try { const cookies = await s.context.cookies() const cfClearance = cookies.find(c => c.name === 'cf_clearance') const cookieHeader = cookies.map(c => `${c.name}=${c.value}`).join('; ') res.json({ ok: true, cookies, cfClearance: cfClearance?.value || null, cookieHeader }) } catch (err) { res.status(500).json({ error: err.message }) } }) app.post('/session/:id/set-cookies', async (req, res) => { const s = await getSession(req.params.id, res) if (!s) return try { const { cookies } = req.body if (!Array.isArray(cookies)) return res.status(400).json({ error: 'cookies must be array' }) await s.context.addCookies(cookies) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: err.message }) } }) app.post('/session/:id/eval', async (req, res) => { const s = await getSession(req.params.id, res) if (!s) return try { const { script } = req.body if (!script) return res.status(400).json({ error: 'Missing script' }) const result = await s.page.evaluate(script) res.json({ ok: true, result }) } catch (err) { res.status(500).json({ error: err.message }) } }) app.post('/session/:id/click', async (req, res) => { const s = await getSession(req.params.id, res) if (!s) return try { const { selector, timeout = 5000 } = req.body if (!selector) return res.status(400).json({ error: 'Missing selector' }) await s.page.click(selector, { timeout }) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: err.message }) } }) app.post('/session/:id/type', async (req, res) => { const s = await getSession(req.params.id, res) if (!s) return try { const { selector, text, delay = 50 } = req.body if (!selector || !text) return res.status(400).json({ error: 'Missing selector or text' }) await s.page.type(selector, text, { delay }) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: err.message }) } }) app.post('/session/:id/wait', async (req, res) => { const s = await getSession(req.params.id, res) if (!s) return try { const { selector, timeout = 10000, state = 'visible' } = req.body if (!selector) return res.status(400).json({ error: 'Missing selector' }) await s.page.waitForSelector(selector, { timeout, state }) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: err.message }) } }) app.post('/session/:id/screenshot', async (req, res) => { const s = await getSession(req.params.id, res) if (!s) return try { const { fullPage = false } = req.body || {} const buf = await s.page.screenshot({ fullPage, type: 'png' }) res.set('Content-Type', 'image/png') res.send(buf) } catch (err) { res.status(500).json({ error: err.message }) } }) app.post('/session/:id/fetch', async (req, res) => { const s = await getSession(req.params.id, res) if (!s) return try { const { url, method = 'GET', headers = {}, body } = req.body if (!url) return res.status(400).json({ error: 'Missing url' }) const result = await s.page.evaluate(async ({ url, method, headers, body }) => { const r = await fetch(url, { method, headers, body }) const text = await r.text() let data try { data = JSON.parse(text) } catch { data = text } return { status: r.status, ok: r.ok, data } }, { url, method, headers, body }) res.json({ ok: true, ...result }) } catch (err) { res.status(500).json({ error: err.message }) } }) app.post('/session/:id/headers', async (req, res) => { const s = await getSession(req.params.id, res) if (!s) return try { const { headers } = req.body if (!headers) return res.status(400).json({ error: 'Missing headers' }) await s.context.setExtraHTTPHeaders(headers) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: err.message }) } }) app.get('/session/:id/status', async (req, res) => { const s = sessions.get(req.params.id) if (!s) return res.status(404).json({ error: 'Session not found' }) res.json({ ok: true, sessionId: req.params.id, url: s.page.url(), createdAt: s.createdAt, age: Date.now() - s.createdAt }) }) app.delete('/session/:id', async (req, res) => { const s = sessions.get(req.params.id) if (!s) return res.status(404).json({ error: 'Session not found' }) clearTimeout(s.timer) await s.browser.close().catch(() => {}) sessions.delete(req.params.id) res.json({ ok: true, message: 'Session closed' }) }) app.get('/sessions', (req, res) => { const list = [...sessions.entries()].map(([id, s]) => ({ sessionId: id, url: s.page.url(), createdAt: s.createdAt, age: Date.now() - s.createdAt })) res.json({ ok: true, count: list.length, sessions: list }) }) app.get('/', (req, res) => { res.json({ ok: true, name: 'Playwright Browser API', endpoints: { 'POST /session/create': 'Create new browser session', 'POST /session/:id/goto': 'Navigate to URL { url, waitUntil, timeout }', 'POST /session/:id/content': 'Get page HTML content', 'POST /session/:id/cookies': 'Get all cookies + cf_clearance', 'POST /session/:id/set-cookies': 'Set cookies { cookies: [] }', 'POST /session/:id/eval': 'Execute JS { script }', 'POST /session/:id/click': 'Click element { selector }', 'POST /session/:id/type': 'Type text { selector, text, delay }', 'POST /session/:id/wait': 'Wait for selector { selector, state, timeout }', 'POST /session/:id/screenshot': 'Get screenshot (returns image/png)', 'POST /session/:id/fetch': 'Fetch URL from browser context { url, method, headers, body }', 'POST /session/:id/headers': 'Set extra HTTP headers { headers: {} }', 'GET /session/:id/status': 'Session info', 'DELETE /session/:id': 'Close session', 'GET /sessions': 'List all active sessions' } }) }) app.listen(7860, () => console.log('Playwright API running on :7860'))