node-red/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js

802 lines
41 KiB
JavaScript
Raw Normal View History

2013-09-05 16:02:48 +02:00
/**
* Copyright JS Foundation and other contributors, http://js.foundation
2013-09-05 16:02:48 +02:00
*
* 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.
**/
2020-10-08 21:24:35 +02:00
const { debug } = require("console");
2014-05-04 00:32:04 +02:00
module.exports = function(RED) {
"use strict";
var mqtt = require("mqtt");
var util = require("util");
var isUtf8 = require('is-utf8');
var HttpsProxyAgent = require('https-proxy-agent');
var url = require('url');
function matchTopic(ts,t) {
if (ts == "#") {
return true;
}
/* The following allows shared subscriptions (as in MQTT v5)
http://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html#_Toc514345522
4.8.2 describes shares like:
$share/{ShareName}/{filter}
$share is a literal string that marks the Topic Filter as being a Shared Subscription Topic Filter.
{ShareName} is a character string that does not include "/", "+" or "#"
{filter} The remainder of the string has the same syntax and semantics as a Topic Filter in a non-shared subscription. Refer to section 4.7.
*/
else if(ts.startsWith("$share")){
ts = ts.replace(/^\$share\/[^#+/]+\/(.*)/g,"$1");
}
var re = new RegExp("^"+ts.replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g,"\\$1").replace(/\+/g,"[^/]+").replace(/\/#$/,"(\/.*)?")+"$");
return re.test(t);
}
2020-10-08 21:24:35 +02:00
/**
* Helper function for setting integer property values in the MQTT V5 properties object
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set/add properties
* @param {string} propName The property name to set in the Destination object
* @param {integer} [minVal] The minimum value. If the src value is less than minVal, it will NOT be set in the destination
* @param {integer} [maxVal] The maximum value. If the src value is greater than maxVal, it will NOT be set in the destination
* @param {integer} [def] An optional default to set in the destination object if prop is NOT present in the soruce object
*/
function setIntProp(src, dst, propName, minVal, maxVal, def) {
if (src.hasOwnProperty(propName)) {
var v = parseInt(src[propName]);
if(isNaN(v)) return;
if(minVal != null) {
if(v < minVal) return;
}
if(maxVal != null) {
if(v > maxVal) return;
}
dst[propName] = v;
} else {
if(def != undefined) dst[propName] = def;
}
}
/**
* Helper function for setting string property values in the MQTT V5 properties object
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set/add properties
* @param {string} propName The property name to set in the Destination object
* @param {string} [def] An optional default to set in the destination object if prop is NOT present in the soruce object
*/
function setStrProp(src, dst, propName, def) {
if (src[propName] && typeof src[propName] == "string") {
dst[propName] = src[propName];
} else {
if(def != undefined) dst[propName] = def;
}
}
/**
* Helper function for setting boolean property values in the MQTT V5 properties object
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set/add properties
* @param {string} propName The property name to set in the Destination object
* @param {boolean} [def] An optional default to set in the destination object if prop is NOT present in the soruce object
*/
function setBoolProp(src, dst, propName, def) {
if (src[propName] != null) {
if(src[propName] === "true" || src[propName] === true) {
dst[propName] = true;
} else if(src[propName] === "false" || src[propName] === false) {
dst[propName] = true;
}
} else {
if(def != undefined) dst[propName] = def;
}
}
/**
* Helper function for copying the MQTT v5 srcUserProperties object (parameter1) to the properties object (parameter2).
* Any property in srcUserProperties that is NOT a key/string pair will be silently discarded.
* NOTE: if no sutable properties are present, the userProperties object will NOT be added to the properties object
* @param {object} srcUserProperties An object with key/value string pairs
* @param {object} properties A properties object in which userProperties will be copied to
*/
function setUserProperties(srcUserProperties, properties) {
if (srcUserProperties && typeof srcUserProperties == "object") {
let _clone = {};
let count = 0;
let keys = Object.keys(srcUserProperties);
if(!keys || !keys.length) return null;
keys.forEach(key => {
let val = srcUserProperties[key];
if(typeof val == "string") {
count++;
_clone[key] = val;
}
});
if(count) properties.userProperties = _clone;
}
}
/**
2020-10-15 00:30:03 +02:00
* Helper function for copying the MQTT v5 buffer type properties
* NOTE: if src[propName] is not a buffer, dst[propName] will NOT be assigned a value (unless def is set)
* @param {object} src Source object containing properties
* @param {object} dst Destination object to set/add properties
* @param {string} propName The property name to set in the Destination object
* @param {boolean} [def] An optional default to set in the destination object if prop is NOT present in the Source object
2020-10-08 21:24:35 +02:00
*/
2020-10-15 00:30:03 +02:00
function setBufferProp(src, dst, propName, def) {
if(!dst) return;
if (src && dst) {
var buf = src[propName];
if (buf && typeof Buffer.isBuffer(buf)) {
dst[propName] = Buffer.from(buf);
}
} else {
if(def != undefined) dst[propName] = def;
2020-10-08 21:24:35 +02:00
}
}
2014-05-04 00:32:04 +02:00
function MQTTBrokerNode(n) {
RED.nodes.createNode(this,n);
// Configuration options passed by Node Red
2014-05-04 00:32:04 +02:00
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;
2020-10-08 21:24:35 +02:00
this.protocolVersion = n.protocolVersion;
this.keepalive = n.keepalive;
this.cleansession = n.cleansession;
2020-10-08 21:24:35 +02:00
this.sessionExpiryInterval = n.sessionExpiryInterval;
this.topicAliasMaximum = n.topicAliasMaximum;
this.maximumPacketSize = n.maximumPacketSize;
2020-10-15 00:30:03 +02:00
this.receiveMaximum = n.receiveMaximum;
this.userProperties = n.userProperties;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901116
this.userPropertiesType = n.userPropertiesType;
// Config node state
this.brokerurl = "";
this.connected = false;
this.connecting = false;
this.closing = false;
this.options = {};
this.queue = [];
this.subscriptions = {};
2015-09-02 12:18:59 +02:00
if (n.birthTopic) {
this.birthMessage = {
topic: n.birthTopic,
payload: n.birthPayload || "",
qos: Number(n.birthQos||0),
2020-10-15 00:30:03 +02:00
retain: n.birthRetain=="true"|| n.birthRetain===true,
//TODO: add payloadFormatIndicator, messageExpiryInterval, contentType, responseTopic, correlationData, userProperties
2015-09-02 12:18:59 +02:00
};
}
if (n.closeTopic) {
this.closeMessage = {
topic: n.closeTopic,
payload: n.closePayload || "",
qos: Number(n.closeQos||0),
2020-10-15 00:30:03 +02:00
retain: n.closeRetain=="true"|| n.closeRetain===true,
//TODO: add payloadFormatIndicator, messageExpiryInterval, contentType, responseTopic, correlationData, userProperties
};
}
if (this.credentials) {
this.username = this.credentials.user;
this.password = this.credentials.password;
}
// 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.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;
}
var prox, noprox;
if (process.env.http_proxy) { prox = process.env.http_proxy; }
if (process.env.HTTP_PROXY) { prox = process.env.HTTP_PROXY; }
if (process.env.no_proxy) { noprox = process.env.no_proxy.split(","); }
if (process.env.NO_PROXY) { noprox = process.env.NO_PROXY.split(","); }
// Create the URL to pass in to the MQTT.js library
if (this.brokerurl === "") {
2018-01-11 23:46:18 +01:00
// if the broker may be ws:// or wss:// or even tcp://
if (this.broker.indexOf("://") > -1) {
this.brokerurl = this.broker;
// Only for ws or wss, check if proxy env var for additional configuration
if (this.brokerurl.indexOf("wss://") > -1 || this.brokerurl.indexOf("ws://") > -1 ) {
// check if proxy is set in env
var noproxy;
if (noprox) {
for (var i = 0; i < noprox.length; i += 1) {
if (this.brokerurl.indexOf(noprox[i].trim()) !== -1) { noproxy=true; }
}
}
if (prox && !noproxy) {
var parsedUrl = url.parse(this.brokerurl);
var proxyOpts = url.parse(prox);
// true for wss
proxyOpts.secureEndpoint = parsedUrl.protocol ? parsedUrl.protocol === 'wss:' : true;
// Set Agent for wsOption in MQTT
var agent = new HttpsProxyAgent(proxyOpts);
this.options.wsOptions = {
agent: agent
}
}
}
} else {
// construct the std mqtt:// url
if (this.usetls) {
this.brokerurl="mqtts://";
} else {
this.brokerurl="mqtt://";
}
if (this.broker !== "") {
//Check for an IPv6 address
if (/(?:^|(?<=\s))(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(?=\s|$)/.test(this.broker)) {
this.brokerurl = this.brokerurl+"["+this.broker+"]:";
} else {
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._("mqtt.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;
2020-10-08 21:24:35 +02:00
if (this.compatmode == "true" || this.compatmode === true || this.protocolVersion == 3) {
this.options.protocolId = 'MQIsdp';
this.options.protocolVersion = 3;
2020-10-08 21:24:35 +02:00
} else if ( this.protocolVersion == 5 ) {
this.options.protocolVersion = 5;
this.options.properties = {};
this.options.properties.requestResponseInformation = true;
this.options.properties.requestProblemInformation = true;
2020-10-15 00:30:03 +02:00
if(this.userProperties) {
let userProperties = RED.util.evaluateNodeProperty(this.userProperties, this.userPropertiesType, this, {});
setUserProperties(userProperties, this.options.properties);
}
2020-10-08 21:24:35 +02:00
setIntProp(this,this.options.properties,"sessionExpiryInterval", 0);
setIntProp(this,this.options.properties,"topicAliasMaximum", 0);
setIntProp(this,this.options.properties,"maximumPacketSize", 0);
2020-10-15 00:30:03 +02:00
setIntProp(this,this.options.properties,"receiveMaximum", 0);
setStrProp(this,this.options.properties,"authenticationMethod"); //TODO: authenticationMethod: the name of the authentication method used for extended authentication string,
setBufferProp(this,this.options.properties,"authenticationData"); //TODO: authenticationData: Binary Data containing authentication data
}
if (this.usetls && n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
if (tlsNode) {
tlsNode.addTLSOptions(this.options);
}
}
2018-01-23 00:15:20 +01:00
// console.log(this.brokerurl,this.options);
2017-09-20 11:30:07 +02:00
// 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),
2020-10-15 00:30:03 +02:00
retain: n.willRetain=="true"|| n.willRetain===true,
//TODO: add willDelayInterval, payloadFormatIndicator, messageExpiryInterval, contentType, responseTopic, correlationData, userProperties
};
}
// 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 {
2020-10-08 21:24:35 +02:00
node.serverProperties = {};
2020-10-15 00:30:03 +02:00
// debug("MQTT: ⬆️ mqtt.connect(node.brokerurl ,node.options)", node.brokerurl, node.options);
node.client = mqtt.connect(node.brokerurl ,node.options);
node.client.setMaxListeners(0);
// Register successful connect or reconnect handler
2020-10-15 00:30:03 +02:00
node.client.on('connect', function (connack) {
node.connecting = false;
node.connected = true;
node.log(RED._("mqtt.state.connected",{broker:(node.clientid?node.clientid+"@":"")+node.brokerurl}));
2020-10-15 00:30:03 +02:00
if(node.options.protocolVersion == 5 && connack && connack.hasOwnProperty("properties")) {
2020-12-07 13:25:51 +01:00
if(typeof connack.properties == "object") {
//clean & assign all props sent from server.
setIntProp(connack.properties, node.serverProperties, "topicAliasMaximum", 1);
setIntProp(connack.properties, node.serverProperties, "receiveMaximum", 0);
setIntProp(connack.properties, node.serverProperties, "sessionExpiryInterval", 0, 0xFFFFFFFF);
setIntProp(connack.properties, node.serverProperties, "maximumQoS", 0, 2);
setBoolProp(connack.properties, node.serverProperties, "retainAvailable");
setBoolProp(connack.properties, node.serverProperties, "wildcardSubscriptionAvailable");
setBoolProp(connack.properties, node.serverProperties, "subscriptionIdentifiersAvailable");
setBoolProp(connack.properties, node.serverProperties, "sharedSubscriptionAvailable");
setIntProp(connack.properties, node.serverProperties, "maximumPacketSize", 0);
setIntProp(connack.properties, node.serverProperties, "serverKeepAlive");
setStrProp(connack.properties, node.serverProperties, "responseInformation");
setStrProp(connack.properties, node.serverProperties, "serverReference");
setStrProp(connack.properties, node.serverProperties, "assignedClientIdentifier");
setStrProp(connack.properties, node.serverProperties, "reasonString");
setUserProperties(connack.properties, node.serverProperties);
2020-10-15 00:30:03 +02:00
debug("MQTTBrokerNode: ⬆ CONNECTED. node.serverProperties ==> ", node.serverProperties );
2020-10-08 21:24:35 +02:00
}
}
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)) {
2020-10-08 21:24:35 +02:00
let topic = s;
let qos = 0;
let _options = {};
for (var r in node.subscriptions[s]) {
if (node.subscriptions[s].hasOwnProperty(r)) {
qos = Math.max(qos,node.subscriptions[s][r].qos);
2020-10-08 21:24:35 +02:00
_options = node.subscriptions[s][r].options;
2020-10-15 00:30:03 +02:00
debug(`MQTTBrokerNode:${node.id}: Re-subscribe - registering handler ref ${r} for ${s} `);
node.client.on('message',node.subscriptions[s][r].handler);
}
}
2020-10-08 21:24:35 +02:00
_options.qos = _options.qos || qos;
2020-10-15 00:30:03 +02:00
debug(`MQTTBrokerNode:${node.id}: Re-subscribe - subscribing to topic '${topic}'`, _options);
2020-10-08 21:24:35 +02:00
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"});
}
}
2020-10-15 00:30:03 +02:00
});
2020-12-07 13:26:27 +01:00
//TODO: what to do with this event? Anything? Necessary?
2020-10-15 00:30:03 +02:00
node.client.on("disconnect", function(packet) {
//Emitted after receiving disconnect packet from broker. MQTT 5.0 feature.
console.log('MQTTBrokerNode disconnect', packet)
var rc = packet && packet.properties && packet.properties.reasonString;
var rc = packet && packet.properties && packet.reasonCode;
});
// Register disconnect handlers
node.client.on('close', function () {
2020-10-15 00:30:03 +02:00
console.log('MQTTBrokerNode closed', arguments)
if (node.connected) {
node.connected = false;
node.log(RED._("mqtt.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._("mqtt.state.connect-failed",{broker:(node.clientid?node.clientid+"@":"")+node.brokerurl}));
}
});
// Register connect error handler
// The client's own reconnect logic will take care of errors
2020-10-15 00:30:03 +02:00
node.client.on('error', function (error) {
console.log('MQTTBrokerNode errored', error)
});
}catch(err) {
console.log(err);
}
}
};
2020-10-08 21:24:35 +02:00
this.subscribe = function (topic,options,callback,ref) {
ref = ref||0;
2020-10-08 21:24:35 +02:00
var qos;
if(typeof options == "object") {
qos = options.qos;
} else {
qos = options;
options = {};
}
options.qos = qos;
node.subscriptions[topic] = node.subscriptions[topic]||{};
var sub = {
topic:topic,
qos:qos,
2020-10-08 21:24:35 +02:00
options:options,
handler:function(mtopic,mpayload, mpacket) {
2020-10-15 00:30:03 +02:00
debug(`MQTTBrokerNode:${node.id}: this.subscribe.handler - attempting to match '${topic}' to '${mtopic}' `, mpacket);
if(mpacket.properties && options.properties && mpacket.properties.subscriptionIdentifier && options.properties.subscriptionIdentifier && (mpacket.properties.subscriptionIdentifier !== options.properties.subscriptionIdentifier) ) {
//do nothing as subscriptionIdentifier does not match
debug(`MQTTBrokerNode:${node.id}: > no match - this nodes subID (${options.properties.subscriptionIdentifier}) !== packet subID (${mpacket.properties.subscriptionIdentifier})`);
} else if (matchTopic(topic,mtopic)) {
debug(`MQTTBrokerNode:${node.id}: > MATCHED '${topic}' to '${mtopic}' - performing callback`);
callback(mtopic,mpayload, mpacket);
2020-10-15 00:30:03 +02:00
} else
debug(`MQTTBrokerNode:${node.id}: > no match / no callback`);
},
ref: ref
};
node.subscriptions[topic][ref] = sub;
if (node.connected) {
2020-10-15 00:30:03 +02:00
debug(`MQTTBrokerNode:${node.id}: this.subscribe - registering handler ref ${ref} for ${topic} and subscribing`, options);
node.client.on('message',sub.handler);
node.client.subscribe(topic, options);
}
};
this.unsubscribe = function (topic, ref, removed) {
ref = ref||0;
var sub = node.subscriptions[topic];
2020-10-15 00:30:03 +02:00
var _debug = `MQTTBrokerNode ${node.id}: unsubscribe for topic ${topic} called... ` ;
if (sub) {
2020-10-15 00:30:03 +02:00
_debug += "sub found. "
if (sub[ref]) {
2020-10-15 00:30:03 +02:00
// debug(`MQTTBrokerNode:${node.id}: this.unsubscribe - removing handler ref ${ref} for ${topic} `);
_debug += `removing handler ref ${ref} for ${topic}. `
node.client.removeListener('message',sub[ref].handler);
delete sub[ref];
}
2020-10-15 00:30:03 +02:00
//if (removed) {
if (Object.keys(sub).length === 0) {
delete node.subscriptions[topic];
if (node.connected) {
2020-10-15 00:30:03 +02:00
_debug += `calling client.unsubscribe to remove topic ${topic}`
// debug(`MQTTBrokerNode:${node.id}: this.unsubscribe - calling client.unsubscribe to remove topic ${topic} `);
node.client.unsubscribe(topic);
}
}
2020-10-15 00:30:03 +02:00
//}
} else {
_debug += "sub not found! "
}
2020-10-15 00:30:03 +02:00
debug(_debug);
};
this.publish = function (msg,done) {
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
};
2020-10-08 21:24:35 +02:00
//https://github.com/mqttjs/MQTT.js/blob/master/README.md#mqttclientpublishtopic-message-options-callback
if(node.options.protocolVersion == 5) {
options.properties = options.properties || {};
setStrProp(msg, options.properties, "responseTopic");
setStrProp(msg, options.properties, "contentType");
setIntProp(msg, options.properties, "topicAlias", 1, node.serverProperties.topicAliasMaximum || 0);
setIntProp(msg, options.properties, "subscriptionIdentifier", 1, 268435455);
setIntProp(msg, options.properties, "messageExpiryInterval", 0);
setBoolProp(msg, options.properties, "payloadFormatIndicator");
setUserProperties(msg.userProperties, options.properties);
2020-10-15 00:30:03 +02:00
setBufferProp(msg, options.properties, "correlationData");
2020-10-08 21:24:35 +02:00
}
2020-10-15 00:30:03 +02:00
debug(`MQTTBrokerNode:${node.id}: publish - sending payload to ${msg.topic ? msg.topic : (msg.topicAlias ? 'topicAlias-'+msg.topicAlias : '???') } `, options);
node.client.publish(msg.topic, msg.payload, options, function(err) {
2020-10-15 00:30:03 +02:00
if(err) node.error(err,msg);//catch errors
done && done();
return
});
}
};
this.on('close', function(done) {
this.closing = true;
if (this.connected) {
// Send close message
if (node.closeMessage) {
node.publish(node.closeMessage);
}
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("mqtt-broker",MQTTBrokerNode,{
credentials: {
user: {type:"text"},
password: {type: "password"}
}
2014-05-04 00:32:04 +02:00
});
2014-05-04 00:32:04 +02:00
function MQTTInNode(n) {
RED.nodes.createNode(this,n);
this.topic = n.topic;
this.qos = parseInt(n.qos);
2020-10-08 21:24:35 +02:00
this.subscriptionIdentifier = n.subscriptionIdentifier;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901117
this.userProperties = n.userProperties;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901116
this.userPropertiesType = n.userPropertiesType;
this.nl = n.nl;
this.rap = n.rap;
this.rh = n.rh;
if (isNaN(this.qos) || this.qos < 0 || this.qos > 2) {
this.qos = 2;
}
2014-05-04 00:32:04 +02:00
this.broker = n.broker;
this.brokerConn = RED.nodes.getNode(this.broker);
2016-02-10 23:38:59 +01:00
if (!/^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/.test(this.topic)) {
return this.warn(RED._("mqtt.errors.invalid-topic"));
}
this.datatype = n.datatype || "utf8";
var node = this;
if (this.brokerConn) {
2020-10-08 21:24:35 +02:00
let v5 = this.brokerConn.options && this.brokerConn.options.protocolVersion == 5;
this.status({fill:"red",shape:"ring",text:"node-red:common.status.disconnected"});
2015-03-16 18:07:46 +01:00
if (this.topic) {
node.brokerConn.register(this);
2020-10-15 00:30:03 +02:00
let options = { qos: this.qos };
2020-10-08 21:24:35 +02:00
if(v5) {
options.properties = {};
2020-10-15 00:30:03 +02:00
if(node.userProperties) {
2020-10-08 21:24:35 +02:00
let userProperties = RED.util.evaluateNodeProperty(node.userProperties, node.userPropertiesType, node, {});
2020-10-15 00:30:03 +02:00
setUserProperties(userProperties, options.properties);
2020-10-08 21:24:35 +02:00
}
2020-10-15 00:30:03 +02:00
setIntProp(node,options.properties,"subscriptionIdentifier", 1);
setIntProp(node, options, "rh");
2020-10-08 21:24:35 +02:00
if(node.nl === "true" || node.nl === true) options.nl = true;
2020-10-15 00:30:03 +02:00
else if(node.nl === "false" || node.nl === false) options.nl = false;
2020-10-08 21:24:35 +02:00
if(node.rap === "true" || node.rap === true) options.rap = true;
2020-10-15 00:30:03 +02:00
else if(node.rap === "false" || node.rap === false) options.rap = false;
2020-10-08 21:24:35 +02:00
}
2020-10-15 00:30:03 +02:00
// debug("MQTT: ⬅️⬅️ this.brokerConn.subscribe",this.topic,options);
2020-10-08 21:24:35 +02:00
this.brokerConn.subscribe(this.topic,options,function(topic,payload,packet) {
2020-10-15 00:30:03 +02:00
debug(`MQTTInNode:${node.id}: Broker sent ${topic}, datatype ${node.datatype}`, packet);
if (node.datatype === "buffer") {
// payload = payload;
} else if (node.datatype === "base64") {
payload = payload.toString('base64');
} else if (node.datatype === "utf8") {
payload = payload.toString('utf8');
} else if (node.datatype === "json") {
if (isUtf8(payload)) {
payload = payload.toString();
try { payload = JSON.parse(payload); }
catch(e) { node.error(RED._("mqtt.errors.invalid-json-parse"),{payload:payload, topic:topic, qos:packet.qos, retain:packet.retain}); return; }
}
else { node.error((RED._("mqtt.errors.invalid-json-string")),{payload:payload, topic:topic, qos:packet.qos, retain:packet.retain}); return; }
} else {
if (isUtf8(payload)) { payload = payload.toString(); }
}
var msg = {topic:topic, payload:payload, qos:packet.qos, retain:packet.retain};
2020-10-08 21:24:35 +02:00
if(v5 && packet.properties) {
//msg.properties = packet.properties;
setStrProp(packet.properties, msg, "responseTopic");
setStrProp(packet.properties, msg, "contentType");
2020-10-15 00:30:03 +02:00
setIntProp(packet.properties, msg, "topicAlias", 1, node.brokerConn.serverProperties.topicAliasMaximum || 0);
2020-10-08 21:24:35 +02:00
setIntProp(packet.properties, msg, "subscriptionIdentifier", 1, 268435455);
setIntProp(packet.properties, msg, "messageExpiryInterval", 0);
setBoolProp(packet.properties, msg, "payloadFormatIndicator");
2020-10-15 00:30:03 +02:00
setStrProp(packet.properties, msg, "reasonString");
setUserProperties(packet.properties.userProperties, msg);
setBufferProp(packet.properties, msg, "correlationData");
2020-10-08 21:24:35 +02:00
}
if ((node.brokerConn.broker === "localhost")||(node.brokerConn.broker === "127.0.0.1")) {
2015-03-16 18:07:46 +01:00
msg._topic = topic;
}
node.send(msg);
}, this.id);
if (this.brokerConn.connected) {
node.status({fill:"green",shape:"dot",text:"node-red:common.status.connected"});
}
2015-03-16 18:07:46 +01:00
}
else {
this.error(RED._("mqtt.errors.not-defined"));
2015-03-16 18:07:46 +01:00
}
this.on('close', function(removed, done) {
if (node.brokerConn) {
node.brokerConn.unsubscribe(node.topic,node.id, removed);
node.brokerConn.deregister(node,done);
}
});
2014-05-04 00:32:04 +02:00
} else {
this.error(RED._("mqtt.errors.missing-config"));
2014-05-04 00:32:04 +02:00
}
2013-09-05 16:02:48 +02:00
}
2014-05-04 00:32:04 +02:00
RED.nodes.registerType("mqtt in",MQTTInNode);
2014-05-04 00:32:04 +02:00
function MQTTOutNode(n) {
RED.nodes.createNode(this,n);
this.topic = n.topic;
this.qos = n.qos || null;
this.retain = n.retain;
2014-05-04 00:32:04 +02:00
this.broker = n.broker;
2020-10-08 21:24:35 +02:00
this.responseTopic = n.responseTopic;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901114
this.correlationData = n.correlationData;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901115
this.correlationDataType = n.correlationDataType;
this.contentType = n.contentType;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901118
this.topicAlias = n.topicAlias; //https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901113
this.messageExpiryInterval = n.messageExpiryInterval; //https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901112
this.payloadFormatIndicator = n.payloadFormatIndicator; //https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901111
this.subscriptionIdentifier = n.subscriptionIdentifier;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901117
this.userProperties = n.userProperties;//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901116
this.userPropertiesType = n.userPropertiesType;
this.brokerConn = RED.nodes.getNode(this.broker);
var node = this;
var chk = /[\+#]/;
if (this.brokerConn) {
2020-10-08 21:24:35 +02:00
let v5 = this.brokerConn.options && this.brokerConn.options.protocolVersion == 5;
this.status({fill:"red",shape:"ring",text:"node-red:common.status.disconnected"});
this.on("input",function(msg,send,done) {
2014-09-08 22:10:06 +02:00
if (msg.qos) {
msg.qos = parseInt(msg.qos);
if ((msg.qos !== 0) && (msg.qos !== 1) && (msg.qos !== 2)) {
msg.qos = null;
}
2013-09-05 16:02:48 +02:00
}
2014-09-08 22:10:06 +02:00
msg.qos = Number(node.qos || msg.qos || 0);
msg.retain = node.retain || msg.retain || false;
msg.retain = ((msg.retain === true) || (msg.retain === "true")) || false;
2020-12-07 13:26:27 +01:00
/** If node property exists, override/set that to property in msg */
2020-10-15 00:30:03 +02:00
let msgPropOverride = function(propName) { if(node[propName]) { msg[propName] = node[propName]; } }
msgPropOverride("topic");
2020-10-08 21:24:35 +02:00
if(v5) {
2020-10-15 00:30:03 +02:00
if(node.userProperties) {
2020-10-08 21:24:35 +02:00
let userProperties = RED.util.evaluateNodeProperty(node.userProperties, node.userPropertiesType, node, msg);
if(userProperties) msg.userProperties = userProperties;
}
if(node.correlationData && node.correlationDataType !== "none") {
let correlationData = RED.util.evaluateNodeProperty(node.correlationData, node.correlationDataType, node, msg);
if(correlationData) msg.correlationData = correlationData;
}
2020-12-07 13:26:27 +01:00
//Next, update/override the msg.xxxx properties from config
//TODO: Should we be expecting msg.properties.xxxx instead of msg.xxxx?
2020-10-08 21:24:35 +02:00
msgPropOverride("responseTopic");
msgPropOverride("contentType");
msgPropOverride("topicAlias");
msgPropOverride("messageExpiryInterval");
msgPropOverride("payloadFormatIndicator");
msgPropOverride("subscriptionIdentifier");
2020-10-15 00:30:03 +02:00
setIntProp(msg,msg,"topicAlias");
setIntProp(msg,msg,"messageExpiryInterval");
setBoolProp(msg,msg,"payloadFormatIndicator");
setIntProp(msg,msg,"subscriptionIdentifier");
2014-09-08 22:10:06 +02:00
}
if ( msg.hasOwnProperty("payload")) {
2020-10-08 21:24:35 +02:00
let topicOK = msg.hasOwnProperty("topic") && (typeof msg.topic === "string") && (msg.topic !== "");
if (!topicOK && v5) {
2020-12-07 13:26:27 +01:00
//NOTE: A value of 0 (in server props topicAliasMaximum) indicates that the Server does not accept any Topic Aliases on this connection
if (msg.hasOwnProperty("topicAlias") && typeof msg.topicAlias === "number" && msg.topicAlias >= 0 && node.brokerConn.serverProperties.topicAliasMaximum && node.brokerConn.serverProperties.topicAliasMaximum >= msg.topicAlias) {
topicOK = true;
msg.topic = ""; //must be empty string
} else if (msg.hasOwnProperty("responseTopic") && (typeof msg.responseTopic === "string") && (msg.responseTopic !== "")) {
2020-12-07 13:26:27 +01:00
//TODO: if topic is empty but responseTopic has a string value, use that instead. Is this desirable?
topicOK = true;
msg.topic = msg.responseTopic;
2020-12-07 13:26:27 +01:00
//TODO: delete msg.responseTopic - to prevent it being resent?
}
2020-10-08 21:24:35 +02:00
}
if (topicOK) { // topic must exist
2020-10-15 00:30:03 +02:00
debug(`MQTTOutNode:${node.id}: sending msg to ${msg.topic}`, msg);
this.brokerConn.publish(msg, function(){
let args = arguments;
let l = args.length;
done();
}); // send the message
} else {
node.warn(RED._("mqtt.errors.invalid-topic"));
done();
}
} else {
done();
2014-09-08 22:10:06 +02:00
}
2014-05-04 00:32:04 +02:00
});
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);
});
2014-05-04 00:32:04 +02:00
} else {
this.error(RED._("mqtt.errors.missing-config"));
2014-05-04 00:32:04 +02:00
}
2013-09-05 16:02:48 +02:00
}
2014-05-04 00:32:04 +02:00
RED.nodes.registerType("mqtt out",MQTTOutNode);
2020-10-08 21:24:35 +02:00
};