signalk-mosquitto
Version:
SignalK plugin for managing Mosquitto MQTT broker with bridge connections and security
493 lines • 20.7 kB
JavaScript
;
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