import { createReadStream, statSync } from 'node:fs' import { createServer } from 'node:http' import { extname, resolve, sep } from 'node:path' import { pathToFileURL } from 'node:url' import { loadConfig } from './server/config.mjs' import { OAuthClient } from './server/oauth.mjs' import { ApiProxy } from './server/proxy.mjs' import { SessionStore } from './server/sessions.mjs' export function createWebServer(config, dependencies = {}) { const store = dependencies.store ?? new SessionStore(config) const oauth = dependencies.oauth ?? new OAuthClient(config, store, dependencies.fetchImplementation) const proxy = dependencies.proxy ?? new ApiProxy(config, oauth, dependencies.fetchImplementation) const startedAt = Date.now() let requests = 0 const server = createServer(async (request, response) => { requests += 1 const traceId = traceIdentifier(request.headers.traceparent) secureHeaders(response, traceId) response.once('finish', () => { process.stdout.write( `${JSON.stringify({ timestamp: new Date().toISOString(), level: 'info', component: 'remora-web', trace_id: traceId, method: request.method, path: requestPath(request.url), status: response.statusCode, })}\n`, ) }) try { await route( request, response, config, store, oauth, proxy, startedAt, requests, ) } catch { if (!response.headersSent) response.writeHead(502, { 'Cache-Control': 'no-store' }) response.end() } }) server.on('upgrade', async (request, socket, head) => { try { if (!(await proxy.upgrade(request, socket, head))) socket.destroy() } catch { socket.destroy() } }) return { server, store } } async function route( request, response, config, store, oauth, proxy, startedAt, requests, ) { const url = new URL(request.url ?? '/', config.publicOrigin) if (request.method === 'POST' && url.pathname === '/client-errors') { await recordClientError(request, response, config) return } if (await oauth.route(request, response, url)) return if (await proxy.route(request, response, url)) return if (request.method !== 'GET' && request.method !== 'HEAD') { response.writeHead(405, { Allow: 'GET, HEAD' }) response.end() return } if (url.pathname === '/health/live') { json(response, 200, { status: 'live' }, request.method) return } if (url.pathname === '/health/ready') { json( response, store.ready ? 200 : 503, { status: store.ready ? 'ready' : 'unavailable' }, request.method, ) return } if (url.pathname === '/metrics') { const body = '# TYPE remora_web_build_info gauge\n' + 'remora_web_build_info 1\n' + '# TYPE remora_web_requests_total counter\n' + `remora_web_requests_total ${requests}\n` + '# TYPE remora_web_uptime_seconds gauge\n' + `remora_web_uptime_seconds ${Math.floor((Date.now() - startedAt) / 1000)}\n` response.writeHead(200, { 'Content-Type': 'text/plain; version=0.0.4', 'Content-Length': Buffer.byteLength(body), }) response.end(request.method === 'HEAD' ? undefined : body) return } if (url.pathname === '/runtime-config.js') { const body = `window.remoraRuntimeConfig=${JSON.stringify({ apiUrl: '/bff/api/', eventsUrl: `${config.publicOrigin.replace(/^http/, 'ws')}/bff/events`, })};\n` response.writeHead(200, { 'Cache-Control': 'no-store', 'Content-Type': 'text/javascript; charset=utf-8', 'Content-Length': Buffer.byteLength(body), }) response.end(request.method === 'HEAD' ? undefined : body) return } serveStatic(request, response, url, config.root) } async function recordClientError(request, response, config) { if ( request.headers.origin !== config.publicOrigin || request.headers['sec-fetch-site'] === 'cross-site' ) { response.writeHead(403, { 'Cache-Control': 'no-store' }) response.end() return } const chunks = [] let size = 0 for await (const chunk of request) { size += chunk.length if (size > 4096) { response.writeHead(413, { 'Cache-Control': 'no-store' }) response.end() return } chunks.push(chunk) } let body try { body = JSON.parse(Buffer.concat(chunks).toString()) } catch { response.writeHead(400, { 'Cache-Control': 'no-store' }) response.end() return } const validKind = body?.kind === 'error' || body?.kind === 'unhandled_rejection' if ( !validKind || typeof body.path !== 'string' || body.path.length > 1024 || !body.path.startsWith('/') ) { response.writeHead(400, { 'Cache-Control': 'no-store' }) response.end() return } process.stdout.write( `${JSON.stringify({ timestamp: new Date().toISOString(), level: 'error', component: 'remora-web', event: 'browser_error', kind: body.kind, path: body.path, })}\n`, ) response.writeHead(204, { 'Cache-Control': 'no-store' }) response.end() } function serveStatic(request, response, url, root) { const candidate = safeFile(url.pathname, root) let file = candidate && regularFile(candidate) ? candidate : null if ( file === null && request.headers.accept?.includes('text/html') && !url.pathname.includes('.') ) file = resolve(root, 'index.html') if (file === null || !regularFile(file)) { response.writeHead(404) response.end() return } const compressed = compressedFile(file, request.headers['accept-encoding']) const served = compressed?.path ?? file const size = statSync(served).size const headers = { 'Cache-Control': file.endsWith('index.html') ? 'no-cache' : 'public, max-age=31536000, immutable', 'Content-Type': contentType(file), 'Content-Length': size, } if (compressed !== null) { headers['Content-Encoding'] = compressed.encoding headers.Vary = 'Accept-Encoding' } response.writeHead(200, headers) if (request.method === 'HEAD') response.end() else createReadStream(served).pipe(response) } function compressedFile(path, accepted = '') { for (const [encoding, suffix] of [ ['br', '.br'], ['gzip', '.gz'], ]) if (accepted.includes(encoding) && regularFile(`${path}${suffix}`)) return { encoding, path: `${path}${suffix}` } return null } function secureHeaders(response, traceId) { response.setHeader('X-Content-Type-Options', 'nosniff') response.setHeader('Referrer-Policy', 'no-referrer') response.setHeader('X-Frame-Options', 'DENY') response.setHeader('Cross-Origin-Opener-Policy', 'same-origin') response.setHeader( 'Content-Security-Policy', "default-src 'self'; base-uri 'none'; frame-ancestors 'none'; object-src 'none'; connect-src 'self'", ) response.setHeader('Traceparent', `00-${traceId}-0000000000000001-01`) } function requestPath(value) { try { return new URL(value ?? '/', 'http://localhost').pathname } catch { return '/' } } function safeFile(pathname, root) { let decoded try { decoded = decodeURIComponent(pathname) } catch { return null } const candidate = resolve(root, `.${decoded}`) return candidate === root || candidate.startsWith(`${root}${sep}`) ? candidate : null } function regularFile(path) { try { return statSync(path).isFile() } catch { return false } } function json(response, status, value, method) { const body = JSON.stringify(value) response.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), }) response.end(method === 'HEAD' ? undefined : body) } function contentType(path) { return ( { '.css': 'text/css; charset=utf-8', '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml', '.woff2': 'font/woff2', }[extname(path)] ?? 'application/octet-stream' ) } function traceIdentifier(traceparent) { const match = typeof traceparent === 'string' ? /^00-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$/.exec(traceparent) : null return match?.[1] ?? crypto.randomUUID().replaceAll('-', '') } async function main() { const config = loadConfig() const { server, store } = createWebServer(config) await store.connect() server.listen(config.port, '0.0.0.0', () => { process.stdout.write( `${JSON.stringify({ timestamp: new Date().toISOString(), level: 'info', component: 'remora-web', message: 'web application ready', port: config.port, })}\n`, ) }) } if (import.meta.url === pathToFileURL(process.argv[1] || '').href) main().catch((error) => { process.stderr.write( `${JSON.stringify({ timestamp: new Date().toISOString(), level: 'error', component: 'remora-web', message: error.message, })}\n`, ) process.exitCode = 1 })