Spaces:
Paused
Paused
| 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')) | |