UNPKG

@nodedaemon/core

Version:

Production-ready Node.js process manager with zero external dependencies

343 lines 12.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebUIServer = void 0; const http_1 = require("http"); const events_1 = require("events"); const fs_1 = require("fs"); const path_1 = require("path"); const crypto_1 = require("crypto"); const WebSocketServer_1 = require("./WebSocketServer"); const constants_1 = require("../utils/constants"); class WebUIServer extends events_1.EventEmitter { httpServer = null; wsServer = null; config; clients = new Map(); subscriptions = new Map(); // clientId -> processIds staticPath; constructor(config = {}) { super(); this.config = { ...constants_1.DEFAULT_WEB_UI_CONFIG, ...config }; // Try multiple paths to find web directory const possiblePaths = [ (0, path_1.join)(__dirname, '..', 'web'), (0, path_1.join)(__dirname, '..', '..', 'web'), (0, path_1.join)(process.cwd(), 'dist', 'web'), (0, path_1.join)(process.cwd(), 'web') ]; for (const path of possiblePaths) { if ((0, fs_1.existsSync)(path)) { this.staticPath = path; break; } } if (!this.staticPath) { this.staticPath = possiblePaths[0]; // fallback } } start() { return new Promise((resolve, reject) => { if (!this.config.enabled) { resolve(); return; } this.httpServer = (0, http_1.createServer)(this.handleHttpRequest.bind(this)); this.wsServer = new WebSocketServer_1.SimpleWebSocketServer(); // Handle WebSocket upgrade requests this.httpServer.on('upgrade', (request, socket, head) => { if (request.url === '/ws') { this.wsServer.handleUpgrade(request, socket, head); } else { socket.destroy(); } }); this.wsServer.on('connection', this.handleWebSocketConnection.bind(this)); this.httpServer.listen(this.config.port, this.config.host, () => { console.log(`Web UI server listening on http://${this.config.host}:${this.config.port}`); console.log(`Serving static files from: ${this.staticPath}`); this.emit('started'); resolve(); }); this.httpServer.on('error', (error) => { console.error('Web UI server error:', error); reject(error); }); }); } stop() { return new Promise((resolve) => { if (this.wsServer) { this.clients.forEach(client => client.close()); this.wsServer.close(); } if (this.httpServer) { this.httpServer.close(() => { this.emit('stopped'); resolve(); }); } else { resolve(); } }); } handleHttpRequest(req, res) { const url = req.url || '/'; // Basic authentication check if (this.config.auth && !this.checkAuth(req)) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="NodeDaemon Web UI"' }); res.end('Unauthorized'); return; } // Route handling if (url === '/') { this.serveStaticFile('/index.html', res); } else if (url.startsWith('/api/')) { this.handleApiRequest(url, req, res); } else { this.serveStaticFile(url, res); } } checkAuth(req) { if (!this.config.auth) return true; const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Basic ')) return false; const base64Credentials = authHeader.split(' ')[1]; const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii'); const [username, password] = credentials.split(':'); return username === this.config.auth.username && password === this.config.auth.password; } handleApiRequest(url, req, res) { res.setHeader('Content-Type', 'application/json'); const path = url.substring(5); // Remove /api/ if (req.method === 'GET') { if (path === 'processes') { this.emit('api:list', (processes) => { res.writeHead(200); res.end(JSON.stringify(processes)); }); } else if (path === 'status') { this.emit('api:status', (status) => { res.writeHead(200); res.end(JSON.stringify(status)); }); } else { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); } } else if (req.method === 'POST') { let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { try { const data = JSON.parse(body); this.handleApiCommand(path, data, res); } catch (error) { res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid JSON' })); } }); } else { res.writeHead(405); res.end(JSON.stringify({ error: 'Method not allowed' })); } } handleApiCommand(path, data, res) { const [resource, action] = path.split('/'); if (resource === 'process' && data.processId) { this.emit(`api:${action}`, data.processId, (result) => { if (result.error) { res.writeHead(400); res.end(JSON.stringify({ error: result.error })); } else { res.writeHead(200); res.end(JSON.stringify({ success: true, data: result })); } }); } else { res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid request' })); } } serveStaticFile(urlPath, res) { // Sanitize path const normalizedPath = urlPath.replace(/^\/+/, ''); const filePath = (0, path_1.join)(this.staticPath, normalizedPath); // Security: Ensure we're not serving files outside static directory if (!filePath.startsWith(this.staticPath)) { res.writeHead(403); res.end('Forbidden'); return; } if (!(0, fs_1.existsSync)(filePath)) { res.writeHead(404); res.end('Not found'); return; } const ext = (0, path_1.extname)(filePath).toLowerCase(); const contentType = this.getContentType(ext); try { const content = (0, fs_1.readFileSync)(filePath); res.writeHead(200, { 'Content-Type': contentType }); res.end(content); } catch (error) { res.writeHead(500); res.end('Internal server error'); } } getContentType(ext) { const types = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon' }; return types[ext] || 'application/octet-stream'; } handleWebSocketConnection(ws, req) { const clientId = this.generateClientId(); this.clients.set(clientId, ws); this.subscriptions.set(clientId, new Set()); ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); this.handleWebSocketMessage(clientId, message); } catch (error) { ws.send(JSON.stringify({ error: 'Invalid message format' })); } }); ws.on('close', () => { this.clients.delete(clientId); this.subscriptions.delete(clientId); }); ws.on('error', (error) => { console.error('WebSocket error:', error); }); // Send initial connection confirmation ws.send(JSON.stringify({ type: 'connected', clientId, timestamp: Date.now() })); } handleWebSocketMessage(clientId, message) { const ws = this.clients.get(clientId); if (!ws) return; switch (message.type) { case 'subscribe': if (message.processId) { const subs = this.subscriptions.get(clientId) || new Set(); subs.add(message.processId); this.subscriptions.set(clientId, subs); } break; case 'unsubscribe': if (message.processId) { const subs = this.subscriptions.get(clientId); if (subs) { subs.delete(message.processId); } } break; case 'command': this.emit(`ws:${message.action}`, message.data, (result) => { ws.send(JSON.stringify({ type: 'response', action: message.action, data: result, timestamp: Date.now() })); }); break; } } broadcastProcessUpdate(processInfo) { // Transform process data to include aggregated values const mainInstance = processInfo.instances[0]; const totalMemory = processInfo.instances.reduce((sum, i) => sum + (i.memory || 0), 0); const totalCpu = processInfo.instances.reduce((sum, i) => sum + (i.cpu || 0), 0); const uptime = mainInstance && mainInstance.uptime ? Math.floor((Date.now() - mainInstance.uptime) / 1000) : 0; const transformedProcess = { ...processInfo, memory: totalMemory, cpu: totalCpu, uptime: uptime }; const event = { type: 'process_update', data: transformedProcess, timestamp: Date.now() }; this.broadcast(event, (clientId) => { const subs = this.subscriptions.get(clientId); return !subs || subs.size === 0 || subs.has(processInfo.id); }); } broadcastLog(log) { const event = { type: 'log', data: log, timestamp: Date.now() }; this.broadcast(event, (clientId) => { if (!log.processId) return true; const subs = this.subscriptions.get(clientId); return !subs || subs.size === 0 || subs.has(log.processId); }); } broadcastMetric(processId, metric) { const event = { type: 'metric', data: { processId, ...metric }, timestamp: Date.now() }; this.broadcast(event, (clientId) => { const subs = this.subscriptions.get(clientId); return !subs || subs.size === 0 || subs.has(processId); }); } broadcast(event, filter) { const message = JSON.stringify(event); this.clients.forEach((ws, clientId) => { if (ws.readyState === 1) { // OPEN state if (!filter || filter(clientId)) { ws.send(message); } } }); } generateClientId() { return (0, crypto_1.createHash)('md5').update(Date.now().toString()).digest('hex').substring(0, 16); } isRunning() { return this.httpServer !== null && this.httpServer.listening; } getConfig() { return { ...this.config }; } } exports.WebUIServer = WebUIServer; //# sourceMappingURL=WebUIServer.js.map