iobroker.js-controller
Version:
Updated by reinstall.js on 2018-06-11T15:19:56.688Z
263 lines • 9.48 kB
JavaScript
/**
* 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