UNPKG

signalk-mosquitto

Version:

SignalK plugin for managing Mosquitto MQTT broker with bridge connections and security

493 lines 20.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.MosquittoManagerImpl = void 0; const file_utils_1 = require("../utils/file-utils"); const path = __importStar(require("path")); const child_process_1 = require("child_process"); class MosquittoManagerImpl { constructor(app, config) { this.mosquittoProcess = null; this.lastStatsTime = 0; this.lastStats = {}; this.app = app; this.config = config; this.dataDir = file_utils_1.FileUtils.getDataDir('signalk-mosquitto'); this.configDir = path.join(this.dataDir, 'config'); this.configFile = path.join(this.configDir, 'mosquitto.conf'); this.pidFile = path.join(this.dataDir, 'mosquitto.pid'); this.logFile = path.join(this.dataDir, 'mosquitto.log'); } async start() { try { await file_utils_1.FileUtils.ensureDir(this.dataDir); await file_utils_1.FileUtils.ensureDir(this.configDir); // Ensure persistence directory exists if (this.config.persistence) { const persistenceDir = path.join(this.dataDir, 'persistence'); await file_utils_1.FileUtils.ensureDir(persistenceDir); } const configContent = await this.generateConfig(this.config); await this.writeConfig(configContent); if (!(await this.validateConfig())) { throw new Error('Generated configuration is invalid'); } await this.startMosquittoProcess(); console.log('Mosquitto broker started successfully'); } catch (error) { console.error(`Failed to start Mosquitto: ${error.message}`); throw error; } } async stop() { try { if (this.mosquittoProcess) { this.mosquittoProcess.kill('SIGTERM'); const processExited = await file_utils_1.FileUtils.waitForProcess(this.mosquittoProcess.pid, 5000); if (!processExited) { console.log('Mosquitto process did not exit gracefully, force killing'); this.mosquittoProcess.kill('SIGKILL'); } this.mosquittoProcess = null; } const pids = await file_utils_1.FileUtils.findProcessByName('mosquitto'); for (const pid of pids) { await file_utils_1.FileUtils.killProcess(pid); } if (await file_utils_1.FileUtils.fileExists(this.pidFile)) { await file_utils_1.FileUtils.deleteFile(this.pidFile); } console.log('Mosquitto broker stopped'); } catch (error) { console.error(`Error stopping Mosquitto: ${error.message}`); throw error; } } async restart() { await this.stop(); await new Promise(resolve => setTimeout(resolve, 1000)); await this.start(); } async getStatus() { const status = { running: false, connectedClients: 0, totalConnections: 0, messagesReceived: 0, messagesPublished: 0, bytesReceived: 0, bytesPublished: 0, }; try { const pids = await file_utils_1.FileUtils.findProcessByName('mosquitto'); status.running = pids.length > 0; if (status.running && pids.length > 0) { status.pid = pids[0]; try { const uptime = await this.getProcessUptime(status.pid); status.uptime = uptime; } catch (error) { console.log(`Failed to get process uptime: ${error.message}`); } try { const version = await this.getMosquittoVersion(); status.version = version; } catch (error) { console.log(`Failed to get Mosquitto version: ${error.message}`); } try { const stats = await this.getConnectionStats(); Object.assign(status, stats); } catch (error) { console.log(`Failed to get connection stats: ${error.message}`); } } } catch (error) { console.error(`Error getting Mosquitto status: ${error.message}`); } return status; } async generateConfig(config) { const lines = []; lines.push('# Mosquitto configuration file generated by SignalK plugin'); lines.push('# Do not edit manually - changes will be overwritten'); lines.push(''); lines.push('# Basic configuration'); lines.push(`listener ${config.brokerPort} ${config.brokerHost}`); lines.push(`max_connections ${config.maxConnections}`); if (config.persistence) { lines.push('persistence true'); const persistenceDir = path.join(this.dataDir, 'persistence'); lines.push(`persistence_location ${persistenceDir}/`); lines.push('autosave_interval 1800'); } else { lines.push('persistence false'); } if (config.enableLogging) { lines.push(`log_dest file ${this.logFile}`); lines.push(`log_type ${config.logLevel}`); lines.push('log_timestamp true'); } lines.push(`pid_file ${this.pidFile}`); // System statistics lines.push(''); lines.push('# System monitoring'); lines.push('sys_interval 10'); if (config.enableWebsockets) { lines.push(''); lines.push('# WebSocket configuration'); lines.push(`listener ${config.websocketPort} ${config.brokerHost}`); lines.push('protocol websockets'); } if (config.allowAnonymous) { lines.push('allow_anonymous true'); } else if (config.enableSecurity) { lines.push(''); lines.push('# Security configuration'); lines.push('allow_anonymous false'); const passwordFile = path.join(this.configDir, 'passwd'); lines.push(`password_file ${passwordFile}`); if (config.acls.length > 0) { const aclFile = path.join(this.configDir, 'acl'); lines.push(`acl_file ${aclFile}`); } } if (config.tlsEnabled && config.tlsCertPath && config.tlsKeyPath) { lines.push(''); lines.push('# TLS configuration'); lines.push(`certfile ${config.tlsCertPath}`); lines.push(`keyfile ${config.tlsKeyPath}`); if (config.tlsCaPath) { lines.push(`cafile ${config.tlsCaPath}`); } lines.push('require_certificate false'); lines.push('use_identity_as_username false'); } if (config.bridges.length > 0) { lines.push(''); lines.push('# Bridge configurations'); for (const bridge of config.bridges) { if (bridge.enabled) { lines.push(''); lines.push(`# Bridge: ${bridge.name}`); lines.push(`connection ${bridge.id}`); lines.push(`address ${bridge.remoteHost}:${bridge.remotePort}`); if (bridge.remoteUsername) { lines.push(`remote_username ${bridge.remoteUsername}`); } if (bridge.remotePassword) { lines.push(`remote_password ${bridge.remotePassword}`); } lines.push(`keepalive_interval ${bridge.keepalive}`); lines.push(`cleansession ${bridge.cleanSession}`); lines.push(`try_private ${bridge.tryPrivate}`); for (const topic of bridge.topics) { let topicLine = `topic ${topic.pattern} ${topic.direction} ${topic.qos}`; if (topic.localPrefix) { topicLine += ` ${topic.localPrefix}`; } if (topic.remotePrefix) { topicLine += ` ${topic.remotePrefix}`; } lines.push(topicLine); } if (bridge.tlsEnabled) { if (bridge.tlsCertPath) { lines.push(`bridge_certfile ${bridge.tlsCertPath}`); } if (bridge.tlsKeyPath) { lines.push(`bridge_keyfile ${bridge.tlsKeyPath}`); } if (bridge.tlsCaPath) { lines.push(`bridge_cafile ${bridge.tlsCaPath}`); } lines.push('bridge_insecure false'); } } } } return lines.join('\n') + '\n'; } async writeConfig(configContent) { await file_utils_1.FileUtils.writeFile(this.configFile, configContent); console.log(`Configuration written to ${this.configFile}`); } async validateConfig() { try { // Just check if the config file exists and is readable const configContent = await file_utils_1.FileUtils.readFile(this.configFile); if (!configContent || configContent.trim().length === 0) { console.error('Configuration file is empty or unreadable'); return false; } // Basic syntax validation - check for required listener directive if (!configContent.includes('listener')) { console.error('Configuration missing required listener directive'); return false; } return true; } catch (error) { console.error(`Configuration validation error: ${error.message}`); return false; } } async startMosquittoProcess() { const mosquittoPath = (await file_utils_1.FileUtils.isCommandAvailable('mosquitto')) ? 'mosquitto' : file_utils_1.FileUtils.getMosquittoBinPath(); this.mosquittoProcess = (0, child_process_1.spawn)(mosquittoPath, ['-c', this.configFile], { detached: false, stdio: ['ignore', 'pipe', 'pipe'], }); this.mosquittoProcess.stdout?.on('data', data => { console.log(`Mosquitto stdout: ${data}`); }); this.mosquittoProcess.stderr?.on('data', data => { console.log(`Mosquitto stderr: ${data}`); }); this.mosquittoProcess.on('close', code => { console.log(`Mosquitto process exited with code ${code}`); this.mosquittoProcess = null; }); this.mosquittoProcess.on('error', error => { console.error(`Mosquitto process error: ${error.message}`); this.mosquittoProcess = null; }); await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Mosquitto start timeout')); }, 10000); const checkProcess = setInterval(async () => { try { const pids = await file_utils_1.FileUtils.findProcessByName('mosquitto'); if (pids.length > 0) { clearTimeout(timeout); clearInterval(checkProcess); resolve(void 0); } } catch (error) { clearTimeout(timeout); clearInterval(checkProcess); reject(error); } }, 500); }); } async getProcessUptime(pid) { try { const { stdout } = await file_utils_1.FileUtils.executeCommand('ps', [ '-o', 'etime=', '-p', pid.toString(), ]); const etimeStr = stdout.trim(); const parts = etimeStr.split(':').reverse(); let seconds = 0; if (parts[0]) seconds += parseInt(parts[0]); if (parts[1]) seconds += parseInt(parts[1]) * 60; if (parts[2]) seconds += parseInt(parts[2]) * 3600; if (parts[3]) seconds += parseInt(parts[3]) * 86400; return seconds; } catch { return 0; } } async getMosquittoVersion() { try { const { stdout } = await file_utils_1.FileUtils.executeCommand('mosquitto', ['-h']); const lines = stdout.split('\n'); const versionLine = lines.find(line => line.includes('mosquitto version')); if (versionLine) { const match = versionLine.match(/mosquitto version (\S+)/); return match ? match[1] : 'unknown'; } return 'unknown'; } catch { return 'unknown'; } } async getConnectionStats() { try { // Connect to broker and query $SYS topics for statistics const mqtt = await Promise.resolve().then(() => __importStar(require('mqtt'))); const client = mqtt.connect(`mqtt://localhost:${this.config.brokerPort}`); return new Promise(resolve => { const stats = { connectedClients: 0, totalConnections: 0, messagesReceived: 0, messagesPublished: 0, bytesReceived: 0, bytesPublished: 0, }; let responseCount = 0; const expectedResponses = 6; const timeout = setTimeout(() => { client.end(); resolve(stats); }, 2000); client.on('connect', () => { // Subscribe to Mosquitto system topics client.subscribe('$SYS/broker/clients/connected', { qos: 0 }); client.subscribe('$SYS/broker/clients/total', { qos: 0 }); client.subscribe('$SYS/broker/messages/received', { qos: 0 }); client.subscribe('$SYS/broker/messages/sent', { qos: 0 }); client.subscribe('$SYS/broker/bytes/received', { qos: 0 }); client.subscribe('$SYS/broker/bytes/sent', { qos: 0 }); }); client.on('message', (topic, message) => { const value = parseInt(message.toString()) || 0; switch (topic) { case '$SYS/broker/clients/connected': stats.connectedClients = value; break; case '$SYS/broker/clients/total': stats.totalConnections = value; break; case '$SYS/broker/messages/received': stats.messagesReceived = value; break; case '$SYS/broker/messages/sent': stats.messagesPublished = value; break; case '$SYS/broker/bytes/received': stats.bytesReceived = value; break; case '$SYS/broker/bytes/sent': stats.bytesPublished = value; break; } responseCount++; if (responseCount >= expectedResponses) { clearTimeout(timeout); client.end(); resolve(stats); } }); client.on('error', () => { clearTimeout(timeout); client.end(); resolve(stats); }); }); } catch (error) { console.error(`Failed to get connection stats: ${error.message}`); return { connectedClients: 0, totalConnections: 0, messagesReceived: 0, messagesPublished: 0, bytesReceived: 0, bytesPublished: 0, }; } } async getMonitoringMetrics() { try { const currentTime = Date.now(); const currentStats = await this.getConnectionStats(); // Show current active connections instead of rate const activeConnections = `${currentStats.connectedClients || 0}`; // Default values for rates let messageRate = '0/min'; let dataRate = '0 KB/s'; // Calculate rates if we have previous data if (this.lastStatsTime > 0 && currentTime > this.lastStatsTime) { const timeDiffSeconds = (currentTime - this.lastStatsTime) / 1000; const timeDiffMinutes = timeDiffSeconds / 60; if (this.lastStats.messagesReceived !== undefined && currentStats.messagesReceived !== undefined) { const messageDiff = currentStats.messagesReceived - this.lastStats.messagesReceived; messageRate = `${Math.round(messageDiff / timeDiffMinutes)}/min`; } if (this.lastStats.bytesReceived !== undefined && currentStats.bytesReceived !== undefined) { const bytesDiff = currentStats.bytesReceived - this.lastStats.bytesReceived; const bytesPerSecond = bytesDiff / timeDiffSeconds; dataRate = this.formatDataRate(bytesPerSecond); } } // Store current stats for next calculation this.lastStatsTime = currentTime; this.lastStats = currentStats; return { connectionRate: activeConnections, messageRate, dataRate, monitorStatus: 'Active', }; } catch (error) { console.error(`Failed to get monitoring metrics: ${error.message}`); return { connectionRate: '0', messageRate: '0/min', dataRate: '0 KB/s', monitorStatus: 'Error', }; } } formatDataRate(bytesPerSecond) { if (bytesPerSecond < 1024) { return `${Math.round(bytesPerSecond)} B/s`; } else if (bytesPerSecond < 1024 * 1024) { return `${Math.round(bytesPerSecond / 1024)} KB/s`; } else { return `${Math.round(bytesPerSecond / (1024 * 1024))} MB/s`; } } } exports.MosquittoManagerImpl = MosquittoManagerImpl; //# sourceMappingURL=mosquitto-manager.js.map