// Express + WebSocket Server Setup import express from 'express'; import fs from 'fs'; import http from 'http'; import { WebSocketServer } from 'ws'; import cors from 'cors'; import helmet from 'helmet'; import rateLimit from 'express-rate-limit'; import path from 'path'; import { config } from './config'; import { logger } from './utils/logger'; import { SessionManager } from './services/SessionManager'; import { ConnectionManager } from './services/ConnectionManager'; import { handleDesktopConnection } from './handlers/desktopHandler'; import { handleWebClientConnection } from './handlers/webClientHandler'; import { createApiProxy } from './handlers/apiProxy'; // Resolve the client IP for an upgrade request, honouring the proxy's // X-Forwarded-For first hop when we trust the proxy. function getClientIp(request: http.IncomingMessage): string { if (config.trustProxy) { const xff = request.headers['x-forwarded-for']; if (typeof xff === 'string' && xff.length > 0) { return xff.split(',')[0].trim(); } if (Array.isArray(xff) && xff.length > 0) { return xff[0].split(',')[0].trim(); } } return request.socket.remoteAddress || 'unknown'; } // Simple in-memory sliding-window rate limiter for WS upgrades, keyed by IP. function createUpgradeRateLimiter() { const hits = new Map(); const cleanup = setInterval(() => { const cutoff = Date.now() - config.wsUpgradeWindowMs; for (const [ip, times] of hits) { const recent = times.filter((t) => t > cutoff); if (recent.length === 0) { hits.delete(ip); } else { hits.set(ip, recent); } } }, config.wsUpgradeWindowMs); if (typeof cleanup.unref === 'function') cleanup.unref(); return function allow(ip: string): boolean { const now = Date.now(); const cutoff = now - config.wsUpgradeWindowMs; const times = (hits.get(ip) || []).filter((t) => t > cutoff); times.push(now); hits.set(ip, times); return times.length <= config.wsUpgradeMax; }; } // Validate the Origin header for a WS upgrade against the allowlist. // Native (desktop) clients send no Origin and are permitted. function isOriginAllowed(origin: string | undefined, host: string | undefined): boolean { if (!origin) return true; // non-browser clients (e.g. desktop app) if (config.allowedOrigins.length > 0) { return config.allowedOrigins.includes(origin); } // Default: allow only same-host origins. try { return new URL(origin).host === host; } catch { return false; } } export function createServer() { const app = express(); const server = http.createServer(app); // Trust the reverse proxy so client IPs (rate limiting) are accurate. app.set('trust proxy', config.trustProxy); // Initialize managers const sessionManager = new SessionManager(); const connectionManager = new ConnectionManager(); // Middleware app.use(helmet({ // Page scripts are served as external files (/static/app.js, // /static/login.js) with no inline handlers, so scripts are restricted to // 'self' (no 'unsafe-inline'). The pages still use inline