UNPKG

iobroker.js-controller

Version:

Updated by reinstall.js on 2018-06-11T15:19:56.688Z

263 lines • 9.48 kB
/** * Multihost server * * Master multihost functionality * * Copyright 2014-2024 bluefox <dogafox@gmail.com>, * MIT License * */ import dgram from 'node:dgram'; import { createHash } from 'node:crypto'; import { isLocalObjectsDbServer } from '@iobroker/js-controller-common'; const PORT = 50005; const MULTICAST_ADDR = '239.255.255.250'; /** * The Multihost Server allows connection from other ioBroker hosts * * @param hostname name of the host * @param logger * @param config * @param info * @param secret */ export class MHServer { count = 0; buffer = {}; lastFrame = {}; authList = {}; config; logger; info; secret; hostname; server = null; initTimer = null; stopped = false; constructor(hostname, logger, config, info, secret) { this.hostname = hostname; this.config = config; this.logger = logger; this.info = info; this.secret = secret; this.init(); } send(msg, rinfo) { if (this.server) { setImmediate(() => { const text = JSON.stringify(msg); try { this.server?.send(text, 0, text.length, rinfo.port, rinfo.address); } catch (e) { this.logger.warn(`host.${this.hostname} Multi-host discovery server: cannot send answer to ${rinfo.address}:${rinfo.port}: ${e}`); } }); } } // delete all old connections checkAuthList(ts) { ts = ts || new Date().getTime(); for (const id of Object.keys(this.authList)) { if (!this.authList[id]) { delete this.authList[id]; } else if (ts - this.authList[id].ts > 31000) { delete this.authList[id]; } } } sha(secret, salt, callback) { // calculate sha256 const hash = createHash('sha256'); hash.on('readable', () => { const data = hash.read(); if (data) { callback(data.toString('hex')); } }); hash.write(secret + salt); hash.end(); } // hello => auth => browse async process(msg, rinfo) { if (!msg) { return; } const ts = new Date().getTime(); this.checkAuthList(ts); const id = `${rinfo.address}:${rinfo.port}`; switch (msg.cmd) { case 'browse': if (this.secret && msg.password && this.authList[id]) { this.sha(this.secret, this.authList[id].salt, async (shaText) => { if (shaText !== msg.password) { this.send({ auth: this.config.multihostService.secure, cmd: 'browse', id: msg.id, result: 'invalid password', }, rinfo); } else { this.authList[id].auth = true; this.send({ auth: this.config.multihostService.secure, cmd: 'browse', id: msg.id, result: 'ok', objects: this.config.objects, states: this.config.states, info: this.info, hostname: this.hostname, slave: !(await isLocalObjectsDbServer(this.config.objects.type, this.config.objects.host)), }, rinfo); } }); return; } if (!this.config.multihostService.secure || (this.authList[id] && this.authList[id].auth)) { this.send({ auth: this.config.multihostService.secure, cmd: 'browse', id: msg.id, result: 'ok', objects: this.config.objects, states: this.config.states, info: this.info, hostname: this.hostname, slave: !(await isLocalObjectsDbServer(this.config.objects.type, this.config.objects.host)), }, rinfo); } else { this.authList[id] = { ts, salt: (Math.random() * 1000000 + ts).toString().substring(0, 16), auth: false, }; // padding if (this.authList[id].salt.length < 16) { this.authList[id].salt += new Array(16 - this.authList[id].salt.length).join('_'); } this.send({ auth: this.config.multihostService.secure, cmd: 'browse', id: msg.id, result: 'not authenticated', salt: this.authList[id].salt, }, rinfo); } break; default: this.send({ cmd: msg.cmd, id: msg.id, result: 'unknown command', }, rinfo); break; } } init() { this.stopped = false; if (this.initTimer) { clearTimeout(this.initTimer); this.initTimer = null; } if (this.count > 10) { return this.logger.warn(`host.${this.hostname} Multi-host discovery server: Port ${PORT} is occupied. Service stopped.`); } this.server = dgram.createSocket({ type: 'udp4', reuseAddr: true }); this.server.on('error', err => { this.logger.error(`host.${this.hostname} Multi-host discovery server: error: ${err.stack}`); this.server?.close(); this.server = null; this.initTimer = this.initTimer || setTimeout(() => { this.initTimer = null; this.init(); }, 5000); }); this.server.on('close', () => { this.server = null; if (!this.initTimer && !this.stopped) { this.initTimer = setTimeout(() => { this.initTimer = null; this.init(); }, 5000); } }); this.server.on('message', (msg, rinfo) => { // following messages are allowed const text = msg.toString(); const now = new Date().getTime(); const id = `${rinfo.address}:${rinfo.port}`; for (const ids in this.buffer) { if (!this.lastFrame[ids]) { delete this.buffer[ids]; } else if (now - this.lastFrame[ids] > 1000) { delete this.buffer[ids]; delete this.lastFrame[ids]; } } if (this.lastFrame[id] && now - this.lastFrame[id] > 1000) { this.buffer[id] = ''; } this.lastFrame[id] = now; if (!this.buffer[id] && text[0] !== '{') { // ignore message this.logger.debug(`host.${this.hostname} Multi-host discovery server: Message from ${rinfo.address} ignored: ${text}`); } else { this.buffer[id] = (this.buffer[id] || '') + msg.toString(); if (this.buffer[id] && this.buffer[id][this.buffer[id].length - 1] === '}') { try { const data = JSON.parse(this.buffer[id]); this.buffer[id] = ''; if (data) { this.process(data, rinfo); } } catch { // may be not yet complete. } } } }); this.server.on('listening', () => { try { this.server?.addMembership(MULTICAST_ADDR); } catch { this.logger.warn(`host.${this.hostname} Multi-host discovery server: Multicast membership could not be added.`); } const address = this.server?.address(); this.logger.info(`host.${this.hostname} Multi-host discovery server: service started on ${address?.address}:${address?.port}`); }); this.server.bind(PORT); } close(callback) { this.stopped = true; if (this.initTimer) { clearTimeout(this.initTimer); this.initTimer = null; } if (this.server) { try { this.server.close(callback); this.server = null; } catch { this.server = null; if (callback) { callback(); } } } else if (callback) { callback(); } } } //# sourceMappingURL=multihostServer.js.map