#!/usr/bin/env node /** * Lightweight reverse proxy gateway for windmill-extra services. * * Listens on a single port (default 3000) and dispatches requests * to the correct backend service based on URL prefix: * * /ws/* → LSP (default: localhost:3001) * /ws_mp/* → Multiplayer (default: localhost:3002) * /ws_debug/* → Debugger (default: localhost:3003) * everything else → LSP (fallback) * * The prefix is stripped before forwarding so each service receives * clean paths (e.g. /ws_mp/room-name → /room-name on port 3002). * * Handles both HTTP and WebSocket upgrades. */ import http from 'http' const PORT = parseInt(process.env.PORT || process.env.GATEWAY_PORT || '3000') const HOST = process.env.GATEWAY_HOST || '0.0.0.0' const LSP_PORT = parseInt(process.env.LSP_PORT || '3001') const MULTIPLAYER_PORT = parseInt(process.env.MULTIPLAYER_PORT || '3002') const DEBUGGER_PORT = parseInt(process.env.DEBUGGER_PORT || '3003') const ROUTES = [ { prefix: '/ws_mp/', target: MULTIPLAYER_PORT, strip: '/ws_mp' }, { prefix: '/ws_mp', target: MULTIPLAYER_PORT, strip: '/ws_mp' }, { prefix: '/ws_debug/', target: DEBUGGER_PORT, strip: '/ws_debug' }, { prefix: '/ws_debug', target: DEBUGGER_PORT, strip: '/ws_debug' }, { prefix: '/ws/', target: LSP_PORT, strip: '' }, { prefix: '/ws', target: LSP_PORT, strip: '' }, ] function resolve (url) { for (const r of ROUTES) { if (url === r.prefix || url.startsWith(r.prefix + (r.prefix.endsWith('/') ? '' : '/'))) { const stripped = r.strip ? url.slice(r.strip.length) || '/' : url return { port: r.target, path: stripped } } } // fallback: forward as-is to LSP return { port: LSP_PORT, path: url } } // ---- HTTP proxy ---- const server = http.createServer((clientReq, clientRes) => { const { port, path } = resolve(clientReq.url) const opts = { hostname: '127.0.0.1', port, path, method: clientReq.method, headers: clientReq.headers, } const proxy = http.request(opts, (proxyRes) => { clientRes.writeHead(proxyRes.statusCode, proxyRes.headers) proxyRes.pipe(clientRes, { end: true }) }) proxy.on('error', (err) => { console.error(`[gateway] HTTP proxy error → :${port}${path}: ${err.message}`) if (!clientRes.headersSent) { clientRes.writeHead(502, { 'Content-Type': 'text/plain' }) } clientRes.end('Bad Gateway') }) clientReq.pipe(proxy, { end: true }) }) // ---- WebSocket proxy (upgrade) ---- server.on('upgrade', (clientReq, clientSocket, head) => { const { port, path } = resolve(clientReq.url) const opts = { hostname: '127.0.0.1', port, path, method: 'GET', headers: clientReq.headers, } const proxy = http.request(opts) proxy.on('upgrade', (proxyRes, proxySocket, proxyHead) => { // Relay the 101 back to the client let response = `HTTP/1.1 101 Switching Protocols\r\n` for (let i = 0; i < proxyRes.rawHeaders.length; i += 2) { response += `${proxyRes.rawHeaders[i]}: ${proxyRes.rawHeaders[i + 1]}\r\n` } response += '\r\n' clientSocket.write(response) if (proxyHead.length) { clientSocket.write(proxyHead) } // Bi-directional pipe proxySocket.pipe(clientSocket) clientSocket.pipe(proxySocket) proxySocket.on('error', () => clientSocket.destroy()) clientSocket.on('error', () => proxySocket.destroy()) }) proxy.on('error', (err) => { console.error(`[gateway] WS proxy error → :${port}${path}: ${err.message}`) clientSocket.destroy() }) proxy.end() }) server.listen(PORT, HOST, () => { console.log(`[gateway] listening on ${HOST}:${PORT}`) console.log(`[gateway] /ws/* → :${LSP_PORT}`) console.log(`[gateway] /ws_mp/* → :${MULTIPLAYER_PORT}`) console.log(`[gateway] /ws_debug/* → :${DEBUGGER_PORT}`) console.log(`[gateway] (fallback) → :${LSP_PORT}`) })