Files
location-mqtt-tracker-app/lib/mosquitto-sync.ts
2025-11-24 16:30:37 +00:00

240 lines
6.9 KiB
TypeScript

// Mosquitto configuration synchronization service
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as crypto from 'crypto';
import { mqttCredentialDb, mqttAclRuleDb, mqttSyncStatusDb } from './mqtt-db';
const execPromise = promisify(exec);
// Konfiguration aus Environment Variablen
const PASSWORD_FILE = process.env.MOSQUITTO_PASSWORD_FILE || '/mosquitto/config/password.txt';
const ACL_FILE = process.env.MOSQUITTO_ACL_FILE || '/mosquitto/config/acl.txt';
const MOSQUITTO_CONTAINER = process.env.MOSQUITTO_CONTAINER_NAME || 'mosquitto';
const ADMIN_USERNAME = process.env.MOSQUITTO_ADMIN_USERNAME || 'admin';
const ADMIN_PASSWORD = process.env.MOSQUITTO_ADMIN_PASSWORD || 'admin';
/**
* Hash ein Passwort im Mosquitto-kompatiblen Format (PBKDF2-SHA512)
* Format: $7$<iterations>$<base64_salt>$<base64_hash>
*
* Mosquitto verwendet PBKDF2 mit SHA-512, 101 Iterationen (Standard),
* 12-Byte Salt und 64-Byte Hash
*/
async function hashPassword(password: string): Promise<string> {
try {
// Mosquitto Standard-Parameter
const iterations = 101;
const saltLength = 12;
const hashLength = 64;
// Generiere zufälligen Salt
const salt = crypto.randomBytes(saltLength);
// PBKDF2 mit SHA-512
const hash = await new Promise<Buffer>((resolve, reject) => {
crypto.pbkdf2(password, salt, iterations, hashLength, 'sha512', (err, derivedKey) => {
if (err) reject(err);
else resolve(derivedKey);
});
});
// Base64-Kodierung (Standard Base64, nicht URL-safe)
const saltBase64 = salt.toString('base64');
const hashBase64 = hash.toString('base64');
// Mosquitto Format: $7$iterations$salt$hash
// $7$ = PBKDF2-SHA512 Identifier
const mosquittoHash = `$7$${iterations}$${saltBase64}$${hashBase64}`;
return mosquittoHash;
} catch (error) {
console.error('Failed to hash password:', error);
throw new Error('Password hashing failed');
}
}
/**
* Generiere Mosquitto Password File Entry
*/
async function generatePasswordEntry(username: string, password: string): Promise<string> {
const hash = await hashPassword(password);
return `${username}:${hash}`;
}
/**
* Generiere die Mosquitto Password Datei
*/
async function generatePasswordFile(): Promise<string> {
let content = '';
// Füge Admin User hinzu
const adminEntry = await generatePasswordEntry(ADMIN_USERNAME, ADMIN_PASSWORD);
content += `# Admin user\n${adminEntry}\n\n`;
// Füge Device Credentials hinzu
const credentials = mqttCredentialDb.findAllActive();
if (credentials.length > 0) {
content += '# Provisioned devices\n';
for (const cred of credentials) {
content += `${cred.mqtt_username}:${cred.mqtt_password_hash}\n`;
}
}
return content;
}
/**
* Generiere die Mosquitto ACL Datei
*/
function generateACLFile(): string {
let content = '';
// Füge Admin ACL hinzu
content += `# Admin user - full access\n`;
content += `user ${ADMIN_USERNAME}\n`;
content += `topic readwrite #\n\n`;
// Füge Device ACLs hinzu
const rules = mqttAclRuleDb.findAll();
if (rules.length > 0) {
content += '# Device permissions\n';
// Gruppiere Regeln nach device_id
const rulesByDevice = rules.reduce((acc, rule) => {
if (!acc[rule.device_id]) {
acc[rule.device_id] = [];
}
acc[rule.device_id].push(rule);
return acc;
}, {} as Record<string, typeof rules>);
// Schreibe ACL Regeln pro Device
for (const [deviceId, deviceRules] of Object.entries(rulesByDevice)) {
const credential = mqttCredentialDb.findByDeviceId(deviceId);
if (!credential) continue;
content += `# Device: ${deviceId}\n`;
content += `user ${credential.mqtt_username}\n`;
for (const rule of deviceRules) {
content += `topic ${rule.permission} ${rule.topic_pattern}\n`;
}
content += '\n';
}
}
return content;
}
/**
* Schreibe Password File
*/
async function writePasswordFile(content: string): Promise<void> {
const configDir = path.dirname(PASSWORD_FILE);
// Stelle sicher dass das Config-Verzeichnis existiert
await fs.mkdir(configDir, { recursive: true });
// Schreibe Datei mit sicheren Permissions (nur owner kann lesen/schreiben)
await fs.writeFile(PASSWORD_FILE, content, { mode: 0o600 });
console.log(`✓ Password file written: ${PASSWORD_FILE}`);
}
/**
* Schreibe ACL File
*/
async function writeACLFile(content: string): Promise<void> {
const configDir = path.dirname(ACL_FILE);
// Stelle sicher dass das Config-Verzeichnis existiert
await fs.mkdir(configDir, { recursive: true });
// Schreibe Datei mit sicheren Permissions
await fs.writeFile(ACL_FILE, content, { mode: 0o600 });
console.log(`✓ ACL file written: ${ACL_FILE}`);
}
/**
* Reload Mosquitto Konfiguration
* Sendet SIGHUP an Mosquitto Container
*/
async function reloadMosquitto(): Promise<boolean> {
try {
// Sende SIGHUP an mosquitto container um config zu reloaden
await execPromise(`docker exec ${MOSQUITTO_CONTAINER} kill -HUP 1`);
console.log('✓ Mosquitto configuration reloaded');
return true;
} catch (error) {
console.log('⚠ Could not reload Mosquitto automatically (requires docker socket permissions)');
console.log('→ Changes saved to config files - restart Mosquitto to apply: docker-compose restart mosquitto');
// Werfe keinen Fehler - Config-Dateien sind aktualisiert, werden beim nächsten Restart geladen
return false;
}
}
/**
* Sync alle MQTT Konfigurationen nach Mosquitto
*/
export async function syncMosquittoConfig(): Promise<{
success: boolean;
message: string;
reloaded: boolean;
}> {
try {
console.log('Starting Mosquitto sync...');
// Generiere Password File
const passwordContent = await generatePasswordFile();
await writePasswordFile(passwordContent);
// Generiere ACL File
const aclContent = generateACLFile();
await writeACLFile(aclContent);
// Versuche Mosquitto zu reloaden
const reloaded = await reloadMosquitto();
// Markiere als synced
mqttSyncStatusDb.markSynced();
return {
success: true,
message: reloaded
? 'Mosquitto configuration synced and reloaded successfully'
: 'Mosquitto configuration synced. Restart Mosquitto to apply changes.',
reloaded
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error('Failed to sync Mosquitto config:', error);
// Markiere Sync als fehlgeschlagen
mqttSyncStatusDb.markSyncFailed(errorMessage);
return {
success: false,
message: `Failed to sync Mosquitto configuration: ${errorMessage}`,
reloaded: false
};
}
}
/**
* Hole Mosquitto Sync Status
*/
export function getMosquittoSyncStatus() {
return mqttSyncStatusDb.get();
}
/**
* Hash ein Passwort für MQTT Credentials
* Exportiere dies damit es in API Routes verwendet werden kann
*/
export { hashPassword };