UNPKG

node-red-contrib-identity-message-broker

Version:
544 lines (506 loc) 16.8 kB
/** * Copyright JS Foundation and other contributors, http://js.foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ module.exports = function(RED) { 'use strict'; const mqtt = require('mqtt'); const isUtf8 = require('is-utf8'); const fetch = require('node-fetch'); function matchTopic(ts, t) { if (ts == '#') { return true; } var re = new RegExp( '^' + ts .replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g, '\\$1') .replace(/\+/g, '[^/]+') .replace(/\/#$/, '(/.*)?') + '$' ); return re.test(t); } function MessageBrokerNode(n) { RED.nodes.createNode(this, n); // Configuration options passed by Node Red this.broker = n.broker; this.port = n.port; this.clientid = n.clientid; this.usetls = n.usetls; this.usews = n.usews; this.verifyservercert = n.verifyservercert; this.compatmode = n.compatmode; this.keepalive = n.keepalive; this.cleansession = n.cleansession; // Config node state this.brokerurl = ''; this.connected = false; this.connecting = false; this.closing = false; this.options = {}; this.queue = []; this.subscriptions = {}; // Oauth this.oauthUrl = n.oauthUrl; this.oidcScope = n.oidcScope; if (n.birthTopic) { this.birthMessage = { topic: n.birthTopic, payload: n.birthPayload || '', qos: Number(n.birthQos || 0), retain: n.birthRetain == 'true' || n.birthRetain === true }; } if (this.credentials) { this.username = this.credentials.user; this.password = this.credentials.password; this.clientId = this.credentials.clientId; this.clientSecret = this.credentials.clientSecret; this.oauthUsername = this.credentials.oauthUsername; this.oauthPassword = this.credentials.oauthPassword; } // If the config node is missing certain options (it was probably deployed prior to an update to the node code), // select/generate sensible options for the new fields if (typeof this.usetls === 'undefined') { this.usetls = false; } if (typeof this.usews === 'undefined') { this.usews = false; } if (typeof this.compatmode === 'undefined') { this.compatmode = true; } if (typeof this.verifyservercert === 'undefined') { this.verifyservercert = false; } if (typeof this.keepalive === 'undefined') { this.keepalive = 60; } else if (typeof this.keepalive === 'string') { this.keepalive = Number(this.keepalive); } if (typeof this.cleansession === 'undefined') { this.cleansession = true; } // Create the URL to pass in to the MQTT.js library if (this.brokerurl === '') { // if the broker may be ws:// or wss:// or even tcp:// if (this.broker.indexOf('://') > -1) { this.brokerurl = this.broker; } else { // construct the std mqtt:// url if (this.usetls) { this.brokerurl = 'mqtts://'; } else { this.brokerurl = 'mqtt://'; } if (this.broker !== '') { this.brokerurl = this.brokerurl + this.broker + ':'; // port now defaults to 1883 if unset. if (!this.port) { this.brokerurl = this.brokerurl + '1883'; } else { this.brokerurl = this.brokerurl + this.port; } } else { this.brokerurl = this.brokerurl + 'localhost:1883'; } } } if (!this.cleansession && !this.clientid) { this.cleansession = true; this.warn(RED._('imbroker.errors.nonclean-missingclientid')); } // Build options for passing to the MQTT.js API this.options.clientId = this.clientid || 'mqtt_' + (1 + Math.random() * 4294967295).toString(16); this.options.username = this.username; this.options.password = this.password; this.options.keepalive = this.keepalive; this.options.clean = this.cleansession; this.options.reconnectPeriod = RED.settings.mqttReconnectTime || 5000; if (this.compatmode == 'true' || this.compatmode === true) { this.options.protocolId = 'MQIsdp'; this.options.protocolVersion = 3; } if (this.usetls && n.tls) { var tlsNode = RED.nodes.getNode(n.tls); if (tlsNode) { tlsNode.addTLSOptions(this.options); } } // console.log(this.brokerurl,this.options); // If there's no rejectUnauthorized already, then this could be an // old config where this option was provided on the broker node and // not the tls node if (typeof this.options.rejectUnauthorized === 'undefined') { this.options.rejectUnauthorized = this.verifyservercert == 'true' || this.verifyservercert === true; } if (n.willTopic) { this.options.will = { topic: n.willTopic, payload: n.willPayload || '', qos: Number(n.willQos || 0), retain: n.willRetain == 'true' || n.willRetain === true }; } // Define functions called by MQTT in and out nodes var node = this; this.users = {}; this.register = function(mqttNode) { node.users[mqttNode.id] = mqttNode; if (Object.keys(node.users).length === 1) { node.connect(); } }; this.deregister = function(mqttNode, done) { delete node.users[mqttNode.id]; if (node.closing) { return done(); } if (Object.keys(node.users).length === 0) { if (node.client && node.client.connected) { return node.client.end(done); } else { node.client.end(); return done(); } } done(); }; this.connect = function() { if (!node.connected && !node.connecting) { node.connecting = true; try { node.client = mqtt.connect( node.brokerurl, node.options ); node.client.setMaxListeners(0); // Register successful connect or reconnect handler node.client.on('connect', function() { node.connecting = false; node.connected = true; node.log( RED._('imbroker.state.connected', { broker: (node.clientid ? node.clientid + '@' : '') + node.brokerurl }) ); for (var id in node.users) { if (node.users.hasOwnProperty(id)) { node.users[id].status({ fill: 'green', shape: 'dot', text: 'node-red:common.status.connected' }); } } // Remove any existing listeners before resubscribing to avoid duplicates in the event of a re-connection node.client.removeAllListeners('message'); // Re-subscribe to stored topics for (var s in node.subscriptions) { if (node.subscriptions.hasOwnProperty(s)) { var topic = s; var qos = 0; for (var r in node.subscriptions[s]) { if (node.subscriptions[s].hasOwnProperty(r)) { qos = Math.max(qos, node.subscriptions[s][r].qos); node.client.on('message', node.subscriptions[s][r].handler); } } var options = { qos: qos }; node.client.subscribe(topic, options); } } // Send any birth message if (node.birthMessage) { node.publish(node.birthMessage); } }); node.client.on('reconnect', function() { for (var id in node.users) { if (node.users.hasOwnProperty(id)) { node.users[id].status({ fill: 'yellow', shape: 'ring', text: 'node-red:common.status.connecting' }); } } }); // Register disconnect handlers node.client.on('close', () => { let reqBody = `client_id=${this.clientId}&client_secret=${ this.clientSecret }&grant_type=password&username=${this.oauthUsername}&password=${ this.oauthPassword }&scope=${this.oidcScope}`; const oauthRequest = async () => { const response = await fetch(this.oauthUrl, { body: reqBody, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: 'POST' }); const json = await response.json(); node.client.options.username = json.id_token; node.client.options.password = json.access_token; console.log(node.client.options) if (node.connected) { node.connected = false; node.log( RED._('imbroker.state.disconnected', { broker: (node.clientid ? node.clientid + '@' : '') + node.brokerurl }) ); for (var id in node.users) { if (node.users.hasOwnProperty(id)) { node.users[id].status({ fill: 'red', shape: 'ring', text: 'node-red:common.status.disconnected' }); } } } else if (node.connecting) { node.log( RED._('imbroker.state.connect-failed', { broker: (node.clientid ? node.clientid + '@' : '') + node.brokerurl }) ); } }; oauthRequest(); }); // Register connect error handler // The client's own reconnect logic will take care of errors node.client.on('error', function(error) {}); } catch (err) { console.log(err); } } }; this.subscribe = function(topic, qos, callback, ref) { ref = ref || 0; node.subscriptions[topic] = node.subscriptions[topic] || {}; var sub = { topic: topic, qos: qos, handler: function(mtopic, mpayload, mpacket) { if (matchTopic(topic, mtopic)) { callback(mtopic, mpayload, mpacket); } }, ref: ref }; node.subscriptions[topic][ref] = sub; if (node.connected) { node.client.on('message', sub.handler); var options = {}; options.qos = qos; node.client.subscribe(topic, options); } }; this.unsubscribe = function(topic, ref) { ref = ref || 0; var sub = node.subscriptions[topic]; if (sub) { if (sub[ref]) { node.client.removeListener('message', sub[ref].handler); delete sub[ref]; } if (Object.keys(sub).length === 0) { delete node.subscriptions[topic]; if (node.connected) { node.client.unsubscribe(topic); } } } }; this.publish = function(msg) { if (node.connected) { if (msg.payload === null || msg.payload === undefined) { msg.payload = ''; } else if (!Buffer.isBuffer(msg.payload)) { if (typeof msg.payload === 'object') { msg.payload = JSON.stringify(msg.payload); } else if (typeof msg.payload !== 'string') { msg.payload = '' + msg.payload; } } var options = { qos: msg.qos || 0, retain: msg.retain || false }; node.client.publish(msg.topic, msg.payload, options, function(err) { return; }); } }; this.on('close', function(done) { this.closing = true; if (this.connected) { this.client.once('close', function() { done(); }); this.client.end(); } else if (this.connecting || node.client.reconnecting) { node.client.end(); done(); } else { done(); } }); } RED.nodes.registerType('identity-message-broker', MessageBrokerNode, { credentials: { user: { type: 'text' }, password: { type: 'password' }, clientId: { type: 'text' }, clientSecret: { type: 'password' }, oauthUsername: { type: 'text' }, oauthPassword: { type: 'password' } } }); function MessageBrokerInNode(n) { RED.nodes.createNode(this, n); this.topic = n.topic; this.qos = parseInt(n.qos); if (isNaN(this.qos) || this.qos < 0 || this.qos > 2) { this.qos = 2; } this.broker = n.broker; this.brokerConn = RED.nodes.getNode(this.broker); if ( !/^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/.test(this.topic) ) { return this.warn(RED._('imbroker.errors.invalid-topic')); } var node = this; if (this.brokerConn) { this.status({ fill: 'red', shape: 'ring', text: 'node-red:common.status.disconnected' }); if (this.topic) { node.brokerConn.register(this); this.brokerConn.subscribe( this.topic, this.qos, function(topic, payload, packet) { if (isUtf8(payload)) { payload = payload.toString(); } var msg = { topic: topic, payload: payload, qos: packet.qos, retain: packet.retain }; if ( node.brokerConn.broker === 'localhost' || node.brokerConn.broker === '127.0.0.1' ) { msg._topic = topic; } node.send(msg); }, this.id ); if (this.brokerConn.connected) { node.status({ fill: 'green', shape: 'dot', text: 'node-red:common.status.connected' }); } } else { this.error(RED._('imbroker.errors.not-defined')); } this.on('close', function(done) { if (node.brokerConn) { node.brokerConn.unsubscribe(node.topic, node.id); node.brokerConn.deregister(node, done); } }); } else { this.error(RED._('imbroker.errors.missing-config')); } } RED.nodes.registerType('imbroker in', MessageBrokerInNode); function MessageBrokerOutNode(n) { RED.nodes.createNode(this, n); this.topic = n.topic; this.qos = n.qos || null; this.retain = n.retain; this.broker = n.broker; this.brokerConn = RED.nodes.getNode(this.broker); var node = this; if (this.brokerConn) { this.status({ fill: 'red', shape: 'ring', text: 'node-red:common.status.disconnected' }); this.on('input', function(msg) { if (msg.qos) { msg.qos = parseInt(msg.qos); if (msg.qos !== 0 && msg.qos !== 1 && msg.qos !== 2) { msg.qos = null; } } msg.qos = Number(node.qos || msg.qos || 0); msg.retain = node.retain || msg.retain || false; msg.retain = msg.retain === true || msg.retain === 'true' || false; if (node.topic) { msg.topic = node.topic; } if (msg.hasOwnProperty('payload')) { if ( msg.hasOwnProperty('topic') && typeof msg.topic === 'string' && msg.topic !== '' ) { // topic must exist this.brokerConn.publish(msg); // send the message } else { node.warn(RED._('imbroker.errors.invalid-topic')); } } }); if (this.brokerConn.connected) { node.status({ fill: 'green', shape: 'dot', text: 'node-red:common.status.connected' }); } node.brokerConn.register(node); this.on('close', function(done) { node.brokerConn.deregister(node, done); }); } else { this.error(RED._('imbroker.errors.missing-config')); } } RED.nodes.registerType('imbroker out', MessageBrokerOutNode); };