// Browser-control HTTP endpoint for the BROWSER HOST. // // The on-screen Chrome, the X display (:1), xdotool (real cursor/keyboard) and // the broadcast capture all live on THIS machine. A remote `bot` on another PC // therefore cannot drive them directly — it must send a command here, where // chrome-control.mjs runs LOCALLY (real input lands on this host's screen, // visible on its VNC / Go-Live). // // POST /control body: {"action":"navigate","url":"naver.com", ...} // GET /health // // Internal-network use only (no auth, per deployment decision). Bind/port: // BROWSER_CONTROL_BIND (default 0.0.0.0), BROWSER_CONTROL_PORT (default 8777) import http from 'node:http'; import { execFile } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const PORT = parseInt(process.env.BROWSER_CONTROL_PORT || '8777', 10); const BIND = process.env.BROWSER_CONTROL_BIND || '0.0.0.0'; const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), 'chrome-control.mjs'); const server = http.createServer((req, res) => { if (req.method === 'GET' && req.url === '/health') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, host: 'browser' })); return; } if (req.method !== 'POST') { res.writeHead(405); res.end('POST /control'); return; } let body = ''; req.on('data', (c) => { body += c; if (body.length > 1e6) req.destroy(); }); req.on('end', () => { // Run the action LOCALLY: chrome-control.mjs uses CDP + xdotool on this // host, so the cursor really moves and text is typed on this screen. execFile('node', [SCRIPT, body || '{}'], { timeout: 95_000, env: process.env }, (err, stdout, stderr) => { res.writeHead(200, { 'Content-Type': 'application/json' }); const out = (stdout || '').trim(); res.end(out || JSON.stringify({ ok: false, error: String((stderr || '').trim() || err?.message || 'no output') })); }); }); }); server.listen(PORT, BIND, () => { console.log(`[control-server] listening on ${BIND}:${PORT} (browser host)`); });