@node-red/nodes 2.2.1 → 3.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/99-sample.html.demo +1 -1
  2. package/core/common/05-junction.html +5 -0
  3. package/core/common/05-junction.js +12 -0
  4. package/core/common/20-inject.html +25 -13
  5. package/core/common/21-debug.html +59 -6
  6. package/core/common/21-debug.js +57 -27
  7. package/core/common/60-link.html +65 -29
  8. package/core/common/60-link.js +169 -20
  9. package/core/common/lib/debug/debug-utils.js +34 -1
  10. package/core/function/10-function.html +57 -21
  11. package/core/function/10-switch.html +19 -12
  12. package/core/function/10-switch.js +1 -0
  13. package/core/function/15-change.html +40 -12
  14. package/core/function/16-range.html +14 -5
  15. package/core/function/80-template.html +16 -12
  16. package/core/function/89-delay.html +46 -6
  17. package/core/function/89-trigger.html +12 -4
  18. package/core/function/rbe.html +7 -3
  19. package/core/network/05-tls.html +10 -4
  20. package/core/network/06-httpproxy.html +10 -1
  21. package/core/network/10-mqtt.html +73 -17
  22. package/core/network/10-mqtt.js +252 -105
  23. package/core/network/21-httpin.html +10 -6
  24. package/core/network/21-httprequest.html +219 -12
  25. package/core/network/21-httprequest.js +98 -17
  26. package/core/network/22-websocket.html +19 -5
  27. package/core/network/22-websocket.js +16 -13
  28. package/core/network/31-tcpin.html +47 -10
  29. package/core/network/31-tcpin.js +8 -3
  30. package/core/network/32-udp.html +14 -2
  31. package/core/parsers/70-CSV.html +4 -1
  32. package/core/parsers/70-JSON.html +3 -2
  33. package/core/parsers/70-XML.html +2 -1
  34. package/core/parsers/70-YAML.html +2 -1
  35. package/core/sequence/17-split.html +6 -2
  36. package/core/sequence/19-batch.html +28 -4
  37. package/core/storage/10-file.html +68 -8
  38. package/core/storage/10-file.js +46 -3
  39. package/core/storage/23-watch.html +2 -1
  40. package/core/storage/23-watch.js +21 -43
  41. package/locales/de/messages.json +1 -0
  42. package/locales/en-US/common/60-link.html +19 -3
  43. package/locales/en-US/messages.json +71 -18
  44. package/locales/en-US/network/21-httprequest.html +1 -1
  45. package/locales/en-US/storage/10-file.html +6 -2
  46. package/locales/ja/common/60-link.html +13 -0
  47. package/locales/ja/messages.json +84 -32
  48. package/locales/ja/network/21-httprequest.html +1 -1
  49. package/locales/ja/storage/10-file.html +8 -6
  50. package/locales/ko/messages.json +1 -0
  51. package/locales/ru/messages.json +1 -0
  52. package/locales/zh-CN/messages.json +1 -0
  53. package/locales/zh-TW/messages.json +1 -0
  54. package/package.json +12 -12
@@ -20,7 +20,30 @@ module.exports = function(RED) {
20
20
  var isUtf8 = require('is-utf8');
21
21
  var HttpsProxyAgent = require('https-proxy-agent');
22
22
  var url = require('url');
23
-
23
+ const knownMediaTypes = {
24
+ "text/css":"string",
25
+ "text/html":"string",
26
+ "text/plain":"string",
27
+ "text/html":"string",
28
+ "application/json":"json",
29
+ "application/octet-stream":"buffer",
30
+ "application/pdf":"buffer",
31
+ "application/x-gtar":"buffer",
32
+ "application/x-gzip":"buffer",
33
+ "application/x-tar":"buffer",
34
+ "application/xml":"string",
35
+ "application/zip":"buffer",
36
+ "audio/aac":"buffer",
37
+ "audio/ac3":"buffer",
38
+ "audio/basic":"buffer",
39
+ "audio/mp4":"buffer",
40
+ "audio/ogg":"buffer",
41
+ "image/bmp":"buffer",
42
+ "image/gif":"buffer",
43
+ "image/jpeg":"buffer",
44
+ "image/tiff":"buffer",
45
+ "image/png":"buffer",
46
+ }
24
47
  //#region "Supporting functions"
25
48
  function matchTopic(ts,t) {
26
49
  if (ts == "#") {
@@ -68,12 +91,21 @@ module.exports = function(RED) {
68
91
  }
69
92
 
70
93
  /**
71
- * Test a topic string is valid
94
+ * Test a topic string is valid for subscription
72
95
  * @param {string} topic
73
96
  * @returns `true` if it is a valid topic
74
97
  */
75
98
  function isValidSubscriptionTopic(topic) {
76
- return /^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/.test(topic)
99
+ return /^(#$|(\+|[^+#]*)(\/(\+|[^+#]*))*(\/(\+|#|[^+#]*))?$)/.test(topic);
100
+ }
101
+
102
+ /**
103
+ * Test a topic string is valid for publishing
104
+ * @param {string} topic
105
+ * @returns `true` if it is a valid topic
106
+ */
107
+ function isValidPublishTopic(topic) {
108
+ return !/[\+#\b\f\n\r\t\v\0]/.test(topic);
77
109
  }
78
110
 
79
111
  /**
@@ -103,7 +135,7 @@ module.exports = function(RED) {
103
135
  if(src[propName] === "true" || src[propName] === true) {
104
136
  dst[propName] = true;
105
137
  } else if(src[propName] === "false" || src[propName] === false) {
106
- dst[propName] = true;
138
+ dst[propName] = false;
107
139
  }
108
140
  } else {
109
141
  if(def != undefined) dst[propName] = def;
@@ -188,6 +220,19 @@ module.exports = function(RED) {
188
220
  */
189
221
  function subscriptionHandler(node, datatype ,topic, payload, packet) {
190
222
  const v5 = node.brokerConn.options && node.brokerConn.options.protocolVersion == 5;
223
+ var msg = {topic:topic, payload:null, qos:packet.qos, retain:packet.retain};
224
+ if(v5 && packet.properties) {
225
+ setStrProp(packet.properties, msg, "responseTopic");
226
+ setBufferProp(packet.properties, msg, "correlationData");
227
+ setStrProp(packet.properties, msg, "contentType");
228
+ setIntProp(packet.properties, msg, "messageExpiryInterval", 0);
229
+ setBoolProp(packet.properties, msg, "payloadFormatIndicator");
230
+ setStrProp(packet.properties, msg, "reasonString");
231
+ setUserProperties(packet.properties.userProperties, msg);
232
+ }
233
+ const v5isUtf8 = v5 ? msg.payloadFormatIndicator === true : null;
234
+ const v5HasMediaType = v5 ? !!msg.contentType : null;
235
+ const v5MediaTypeLC = v5 ? (msg.contentType + "").toLowerCase() : null;
191
236
 
192
237
  if (datatype === "buffer") {
193
238
  // payload = payload;
@@ -196,25 +241,65 @@ module.exports = function(RED) {
196
241
  } else if (datatype === "utf8") {
197
242
  payload = payload.toString('utf8');
198
243
  } else if (datatype === "json") {
199
- if (isUtf8(payload)) {
200
- payload = payload.toString();
201
- try { payload = JSON.parse(payload); }
202
- catch(e) { node.error(RED._("mqtt.errors.invalid-json-parse"),{payload:payload, topic:topic, qos:packet.qos, retain:packet.retain}); return; }
244
+ if (v5isUtf8 || isUtf8(payload)) {
245
+ try {
246
+ payload = JSON.parse(payload.toString());
247
+ } catch (e) {
248
+ node.error(RED._("mqtt.errors.invalid-json-parse"), { payload: payload, topic: topic, qos: packet.qos, retain: packet.retain }); return;
249
+ }
250
+ } else {
251
+ node.error((RED._("mqtt.errors.invalid-json-string")), { payload: payload, topic: topic, qos: packet.qos, retain: packet.retain }); return;
203
252
  }
204
- else { node.error((RED._("mqtt.errors.invalid-json-string")),{payload:payload, topic:topic, qos:packet.qos, retain:packet.retain}); return; }
205
253
  } else {
206
- if (isUtf8(payload)) { payload = payload.toString(); }
207
- }
208
- var msg = {topic:topic, payload:payload, qos:packet.qos, retain:packet.retain};
209
- if(v5 && packet.properties) {
210
- setStrProp(packet.properties, msg, "responseTopic");
211
- setBufferProp(packet.properties, msg, "correlationData");
212
- setStrProp(packet.properties, msg, "contentType");
213
- setIntProp(packet.properties, msg, "messageExpiryInterval", 0);
214
- setBoolProp(packet.properties, msg, "payloadFormatIndicator");
215
- setStrProp(packet.properties, msg, "reasonString");
216
- setUserProperties(packet.properties.userProperties, msg);
254
+ //"auto" (legacy) or "auto-detect" (new default)
255
+ if (v5isUtf8 || v5HasMediaType) {
256
+ const outputType = knownMediaTypes[v5MediaTypeLC]
257
+ switch (outputType) {
258
+ case "string":
259
+ payload = payload.toString();
260
+ break;
261
+ case "buffer":
262
+ //no change
263
+ break;
264
+ case "json":
265
+ try {
266
+ //since v5 type states this should be JSON, parse it & error out if NOT JSON
267
+ payload = payload.toString()
268
+ const obj = JSON.parse(payload);
269
+ if (datatype === "auto-detect") {
270
+ payload = obj; //as mode is "auto-detect", return the parsed JSON
271
+ }
272
+ } catch (e) {
273
+ node.error(RED._("mqtt.errors.invalid-json-parse"), { payload: payload, topic: topic, qos: packet.qos, retain: packet.retain }); return;
274
+ }
275
+ break;
276
+ default:
277
+ if (v5isUtf8 || isUtf8(payload)) {
278
+ payload = payload.toString(); //auto String
279
+ if (datatype === "auto-detect") {
280
+ try {
281
+ payload = JSON.parse(payload); //auto to parsed object (attempt)
282
+ } catch (e) {
283
+ /* mute error - it simply isnt JSON, just leave payload as a string */
284
+ }
285
+ }
286
+ }
287
+ break;
288
+ }
289
+ } else if (isUtf8(payload)) {
290
+ payload = payload.toString(); //auto String
291
+ if (datatype === "auto-detect") {
292
+ try {
293
+ payload = JSON.parse(payload);
294
+ } catch (e) {
295
+ /* mute error - it simply isnt JSON, just leave payload as a string */
296
+ }
297
+ }
298
+ } //else {
299
+ //leave as buffer
300
+ //}
217
301
  }
302
+ msg.payload = payload;
218
303
  if ((node.brokerConn.broker === "localhost")||(node.brokerConn.broker === "127.0.0.1")) {
219
304
  msg._topic = topic;
220
305
  }
@@ -264,38 +349,15 @@ module.exports = function(RED) {
264
349
  msg.messageExpiryInterval = node.messageExpiryInterval;
265
350
  }
266
351
  }
267
- if (msg.userProperties && typeof msg.userProperties !== "object") {
268
- delete msg.userProperties;
269
- }
270
- if (hasProperty(msg, "topicAlias") && !isNaN(msg.topicAlias) && (msg.topicAlias === 0 || bsp.topicAliasMaximum === 0 || msg.topicAlias > bsp.topicAliasMaximum)) {
271
- delete msg.topicAlias;
272
- }
273
-
274
352
  if (hasProperty(msg, "payload")) {
275
-
276
- //check & sanitise topic
277
- let topicOK = hasProperty(msg, "topic") && (typeof msg.topic === "string") && (msg.topic !== "");
278
-
279
- if (!topicOK && v5) {
280
- //NOTE: A value of 0 (in server props topicAliasMaximum) indicates that the Server does not accept any Topic Aliases on this connection
281
- if (hasProperty(msg, "topicAlias") && !isNaN(msg.topicAlias) && msg.topicAlias >= 0 && bsp.topicAliasMaximum && bsp.topicAliasMaximum >= msg.topicAlias) {
282
- topicOK = true;
283
- msg.topic = ""; //must be empty string
284
- } else if (hasProperty(msg, "responseTopic") && (typeof msg.responseTopic === "string") && (msg.responseTopic !== "")) {
285
- //TODO: if topic is empty but responseTopic has a string value, use that instead. Is this desirable?
286
- topicOK = true;
287
- msg.topic = msg.responseTopic;
288
- //TODO: delete msg.responseTopic - to prevent it being resent?
353
+ // send the message
354
+ node.brokerConn.publish(msg, function(err) {
355
+ if(err && err.warn) {
356
+ node.warn(err);
357
+ return;
289
358
  }
290
- }
291
- topicOK = topicOK && !/[\+#\b\f\n\r\t\v\0]/.test(msg.topic);
292
-
293
- if (topicOK) {
294
- node.brokerConn.publish(msg, done); // send the message
295
- } else {
296
- node.warn(RED._("mqtt.errors.invalid-topic"));
297
- done();
298
- }
359
+ done(err);
360
+ });
299
361
  } else {
300
362
  done();
301
363
  }
@@ -391,6 +453,7 @@ module.exports = function(RED) {
391
453
  node.options = {};
392
454
  node.queue = [];
393
455
  node.subscriptions = {};
456
+ node.clientListeners = []
394
457
  /** @type {mqtt.MqttClient}*/ this.client;
395
458
  node.setOptions = function(opts, init) {
396
459
  if(!opts || typeof opts !== "object") {
@@ -415,8 +478,12 @@ module.exports = function(RED) {
415
478
  setIfHasProperty(opts, node, "topicAliasMaximum", init);
416
479
  setIfHasProperty(opts, node, "maximumPacketSize", init);
417
480
  setIfHasProperty(opts, node, "receiveMaximum", init);
418
- setIfHasProperty(opts, node, "userProperties", init);//https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901116
419
- setIfHasProperty(opts, node, "userPropertiesType", init);
481
+ //https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901116
482
+ if (hasProperty(opts, "userProperties")) {
483
+ node.userProperties = opts.userProperties;
484
+ } else if (hasProperty(opts, "userProps")) {
485
+ node.userProperties = opts.userProps;
486
+ }
420
487
 
421
488
  function createLWT(topic, payload, qos, retain, v5opts, v5SubPropName) {
422
489
  let message = undefined;
@@ -465,7 +532,7 @@ module.exports = function(RED) {
465
532
  };
466
533
  if(hasProperty(opts, "willTopic")) {
467
534
  //will v5 properties must be set in the "properties" sub object
468
- node.options.will = createLWT(opts.willTopic, opts.willPayload, opts.willQos, opts.willRetain, opts.willMsg, "properies");
535
+ node.options.will = createLWT(opts.willTopic, opts.willPayload, opts.willQos, opts.willRetain, opts.willMsg, "properties");
469
536
  };
470
537
  } else {
471
538
  //update options
@@ -637,24 +704,8 @@ module.exports = function(RED) {
637
704
 
638
705
  node.deregister = function(mqttNode,done) {
639
706
  delete node.users[mqttNode.id];
640
- if (node.closing) {
641
- return done();
642
- }
643
- if (Object.keys(node.users).length === 0) {
644
- if (node.client && node.client.connected) {
645
- // Send close message
646
- if (node.closeMessage) {
647
- node.publish(node.closeMessage,function(err) {
648
- node.client.end(done);
649
- });
650
- } else {
651
- node.client.end(done);
652
- }
653
- return;
654
- } else {
655
- if (node.client) { node.client.end(); }
656
- return done();
657
- }
707
+ if (!node.closing && node.connected && Object.keys(node.users).length === 0) {
708
+ node.disconnect();
658
709
  }
659
710
  done();
660
711
  };
@@ -663,15 +714,22 @@ module.exports = function(RED) {
663
714
  }
664
715
  node.connect = function (callback) {
665
716
  if (node.canConnect()) {
717
+ node.closing = false;
666
718
  node.connecting = true;
667
719
  setStatusConnecting(node, true);
668
720
  try {
669
721
  node.serverProperties = {};
722
+ if(node.client) {
723
+ //belt and braces to avoid left over clients
724
+ node.client.end(true);
725
+ node._clientRemoveListeners();
726
+ }
670
727
  node.client = mqtt.connect(node.brokerurl, node.options);
671
728
  node.client.setMaxListeners(0);
672
- let callbackDone = false; //prevent re-connects causing node.client.on('connect' firing callback multiple times
729
+ let callbackDone = false; //prevent re-connects causing node._clientOn('connect' firing callback multiple times
673
730
  // Register successful connect or reconnect handler
674
- node.client.on('connect', function (connack) {
731
+ node._clientOn('connect', function (connack) {
732
+ node.closing = false;
675
733
  node.connecting = false;
676
734
  node.connected = true;
677
735
  if(!callbackDone && typeof callback == "function") {
@@ -702,7 +760,7 @@ module.exports = function(RED) {
702
760
  }
703
761
  setStatusConnected(node, true);
704
762
  // Remove any existing listeners before resubscribing to avoid duplicates in the event of a re-connection
705
- node.client.removeAllListeners('message');
763
+ node._clientRemoveListeners('message');
706
764
 
707
765
  // Re-subscribe to stored topics
708
766
  for (var s in node.subscriptions) {
@@ -714,7 +772,7 @@ module.exports = function(RED) {
714
772
  if (node.subscriptions[s].hasOwnProperty(r)) {
715
773
  qos = Math.max(qos,node.subscriptions[s][r].qos);
716
774
  _options = node.subscriptions[s][r].options;
717
- node.client.on('message',node.subscriptions[s][r].handler);
775
+ node._clientOn('message',node.subscriptions[s][r].handler);
718
776
  }
719
777
  }
720
778
  _options.qos = _options.qos || qos;
@@ -727,11 +785,11 @@ module.exports = function(RED) {
727
785
  node.publish(node.birthMessage);
728
786
  }
729
787
  });
730
- node.client.on("reconnect", function() {
788
+ node._clientOn("reconnect", function() {
731
789
  setStatusConnecting(node, true);
732
790
  });
733
791
  //Broker Disconnect - V5 event
734
- node.client.on("disconnect", function(packet) {
792
+ node._clientOn("disconnect", function(packet) {
735
793
  //Emitted after receiving disconnect packet from broker. MQTT 5.0 feature.
736
794
  const rc = (packet && packet.properties && packet.reasonCode) || packet.reasonCode;
737
795
  const rs = packet && packet.properties && packet.properties.reasonString || "";
@@ -740,11 +798,12 @@ module.exports = function(RED) {
740
798
  reasonCode: rc,
741
799
  reasonString: rs
742
800
  }
801
+ node.connected = false;
743
802
  node.log(RED._("mqtt.state.broker-disconnected", details));
744
803
  setStatusDisconnected(node, true);
745
804
  });
746
805
  // Register disconnect handlers
747
- node.client.on('close', function () {
806
+ node._clientOn('close', function () {
748
807
  if (node.connected) {
749
808
  node.connected = false;
750
809
  node.log(RED._("mqtt.state.disconnected",{broker:(node.clientid?node.clientid+"@":"")+node.brokerurl}));
@@ -756,33 +815,59 @@ module.exports = function(RED) {
756
815
 
757
816
  // Register connect error handler
758
817
  // The client's own reconnect logic will take care of errors
759
- node.client.on('error', function (error) {
818
+ node._clientOn('error', function (error) {
760
819
  });
761
820
  }catch(err) {
762
821
  console.log(err);
763
822
  }
764
823
  }
765
824
  };
825
+
766
826
  node.disconnect = function (callback) {
767
827
  const _callback = function () {
768
- setStatusDisconnected(node, true);
828
+ if(node.connected || node.connecting) {
829
+ setStatusDisconnected(node, true);
830
+ }
831
+ if(node.client) { node._clientRemoveListeners(); }
769
832
  node.connecting = false;
770
833
  node.connected = false;
771
834
  callback && typeof callback == "function" && callback();
772
835
  };
773
-
774
- if(node.client) {
775
- if(node.client.connected && node.closeMessage) {
776
- node.publish(node.closeMessage, function (err) {
777
- node.client.end(_callback);
778
- });
779
- } else if(node.client.connected || node.client.reconnecting) {
780
- node.client.end(_callback);
781
- } else if(node.client.disconnecting || node.client.connected === false) {
782
- _callback();
783
- }
836
+ if(!node.client) { return _callback(); }
837
+ if(node.closing) { return _callback(); }
838
+
839
+ let waitEnd = (client, ms) => {
840
+ return new Promise( (resolve, reject) => {
841
+ node.closing = true;
842
+ if(!client) {
843
+ resolve();
844
+ } else {
845
+ const t = setTimeout(() => {
846
+ //clean end() has exceeded WAIT_END, lets force end!
847
+ client && client.end(true);
848
+ reject();
849
+ }, ms);
850
+ client.end(() => {
851
+ clearTimeout(t);
852
+ resolve()
853
+ });
854
+ }
855
+ });
856
+ };
857
+ if(node.connected && node.closeMessage) {
858
+ node.publish(node.closeMessage, function (err) {
859
+ waitEnd(node.client, 2000).then(() => {
860
+ _callback();
861
+ }).catch((e) => {
862
+ _callback();
863
+ })
864
+ });
784
865
  } else {
785
- _callback();
866
+ waitEnd(node.client, 2000).then(() => {
867
+ _callback();
868
+ }).catch((e) => {
869
+ _callback();
870
+ })
786
871
  }
787
872
  }
788
873
  node.subscriptionIds = {};
@@ -819,7 +904,7 @@ module.exports = function(RED) {
819
904
  };
820
905
  node.subscriptions[topic][ref] = sub;
821
906
  if (node.connected) {
822
- node.client.on('message',sub.handler);
907
+ node._clientOn('message',sub.handler);
823
908
  node.client.subscribe(topic, options);
824
909
  }
825
910
  };
@@ -830,7 +915,7 @@ module.exports = function(RED) {
830
915
  if (sub) {
831
916
  if (sub[ref]) {
832
917
  if(node.client) {
833
- node.client.removeListener('message',sub[ref].handler);
918
+ node._clientRemoveListeners('message',sub[ref].handler);
834
919
  }
835
920
  delete sub[ref];
836
921
  }
@@ -864,8 +949,18 @@ module.exports = function(RED) {
864
949
  qos: msg.qos || 0,
865
950
  retain: msg.retain || false
866
951
  };
952
+ let topicOK = hasProperty(msg, "topic") && (typeof msg.topic === "string") && (isValidPublishTopic(msg.topic));
867
953
  //https://github.com/mqttjs/MQTT.js/blob/master/README.md#mqttclientpublishtopic-message-options-callback
868
954
  if(node.options.protocolVersion == 5) {
955
+ const bsp = node.serverProperties || {};
956
+ if (msg.userProperties && typeof msg.userProperties !== "object") {
957
+ delete msg.userProperties;
958
+ }
959
+ if (hasProperty(msg, "topicAlias") && !isNaN(Number(msg.topicAlias))) {
960
+ msg.topicAlias = parseInt(msg.topicAlias);
961
+ } else {
962
+ delete msg.topicAlias;
963
+ }
869
964
  options.properties = options.properties || {};
870
965
  setStrProp(msg, options.properties, "responseTopic");
871
966
  setBufferProp(msg, options.properties, "correlationData");
@@ -875,31 +970,75 @@ module.exports = function(RED) {
875
970
  setIntProp(msg, options.properties, "topicAlias", 1, node.serverProperties.topicAliasMaximum || 0);
876
971
  setBoolProp(msg, options.properties, "payloadFormatIndicator");
877
972
  //FUTURE setIntProp(msg, options.properties, "subscriptionIdentifier", 1, 268435455);
878
- if (options.properties.topicAlias) {
879
- if (!node.topicAliases.hasOwnProperty(options.properties.topicAlias) && msg.topic == "") {
973
+
974
+ //check & sanitise topic
975
+ if (topicOK && options.properties.topicAlias) {
976
+ let aliasValid = (bsp.topicAliasMaximum && bsp.topicAliasMaximum >= options.properties.topicAlias);
977
+ if (!aliasValid) {
880
978
  done("Invalid topicAlias");
881
979
  return
882
980
  }
883
981
  if (node.topicAliases[options.properties.topicAlias] === msg.topic) {
884
- msg.topic = ""
982
+ msg.topic = "";
885
983
  } else {
886
- node.topicAliases[options.properties.topicAlias] = msg.topic
984
+ node.topicAliases[options.properties.topicAlias] = msg.topic;
887
985
  }
986
+ } else if (!msg.topic && options.properties.responseTopic) {
987
+ msg.topic = msg.responseTopic;
988
+ topicOK = isValidPublishTopic(msg.topic);
989
+ delete msg.responseTopic; //prevent responseTopic being resent?
888
990
  }
889
991
  }
890
992
 
891
- node.client.publish(msg.topic, msg.payload, options, function(err) {
892
- done && done(err);
893
- return
894
- });
993
+ if (topicOK) {
994
+ node.client.publish(msg.topic, msg.payload, options, function(err) {
995
+ done && done(err);
996
+ return
997
+ });
998
+ } else {
999
+ const error = new Error(RED._("mqtt.errors.invalid-topic"));
1000
+ error.warn = true;
1001
+ done(error);
1002
+ }
895
1003
  }
896
1004
  };
897
1005
 
898
1006
  node.on('close', function(done) {
899
- node.closing = true;
900
- node.disconnect(done);
1007
+ node.disconnect(function() {
1008
+ done();
1009
+ });
901
1010
  });
902
1011
 
1012
+ /**
1013
+ * Add event handlers to the MQTT.js client and track them so that
1014
+ * we do not remove any handlers that the MQTT client uses internally.
1015
+ * Use {@link node._clientRemoveListeners `node._clientRemoveListeners`} to remove handlers
1016
+ * @param {string} event The name of the event
1017
+ * @param {function} handler The handler for this event
1018
+ */
1019
+ node._clientOn = function(event, handler) {
1020
+ node.clientListeners.push({event, handler})
1021
+ node.client.on(event, handler)
1022
+ }
1023
+
1024
+ /**
1025
+ * Remove event handlers from the MQTT.js client & only the events
1026
+ * that we attached in {@link node._clientOn `node._clientOn`}.
1027
+ * * If `event` is omitted, then all events matching `handler` are removed
1028
+ * * If `handler` is omitted, then all events named `event` are removed
1029
+ * * If both parameters are omitted, then all events are removed
1030
+ * @param {string} [event] The name of the event (optional)
1031
+ * @param {function} [handler] The handler for this event (optional)
1032
+ */
1033
+ node._clientRemoveListeners = function(event, handler) {
1034
+ node.clientListeners = node.clientListeners.filter((l) => {
1035
+ if (event && event !== l.event) { return true; }
1036
+ if (handler && handler !== l.handler) { return true; }
1037
+ node.client.removeListener(l.event, l.handler)
1038
+ return false; //found and removed, filter out this one
1039
+ })
1040
+ }
1041
+
903
1042
  }
904
1043
 
905
1044
  RED.nodes.registerType("mqtt-broker",MQTTBrokerNode,{
@@ -1074,6 +1213,9 @@ module.exports = function(RED) {
1074
1213
  node.brokerConn.unsubscribe(node.topic,node.id, removed);
1075
1214
  }
1076
1215
  node.brokerConn.deregister(node, done);
1216
+ node.brokerConn = null;
1217
+ } else {
1218
+ done();
1077
1219
  }
1078
1220
  });
1079
1221
  } else {
@@ -1134,7 +1276,12 @@ module.exports = function(RED) {
1134
1276
  }
1135
1277
  node.brokerConn.register(node);
1136
1278
  node.on('close', function(done) {
1137
- node.brokerConn.deregister(node,done);
1279
+ if (node.brokerConn) {
1280
+ node.brokerConn.deregister(node,done);
1281
+ node.brokerConn = null;
1282
+ } else {
1283
+ done();
1284
+ }
1138
1285
  });
1139
1286
  } else {
1140
1287
  node.error(RED._("mqtt.errors.missing-config"));
@@ -70,7 +70,8 @@
70
70
  color:"rgb(231, 231, 174)",
71
71
  defaults: {
72
72
  name: {value:""},
73
- url: {value:"",required:true},
73
+ url: {value:"", required:true,
74
+ label:RED._("node-red:httpin.label.url")},
74
75
  method: {value:"get",required:true},
75
76
  upload: {value:false},
76
77
  swaggerDoc: {type:"swagger-doc", required:false}
@@ -128,7 +129,7 @@
128
129
  var headerTypes = [
129
130
  {value:"content-type",label:"Content-Type",hasValue: false},
130
131
  {value:"location",label:"Location",hasValue: false},
131
- {value:"other",label:RED._("node-red:httpin.label.other"),icon:"red/images/typedInput/az.png"}
132
+ {value:"other",label:RED._("node-red:httpin.label.other"),icon:"red/images/typedInput/az.svg"}
132
133
  ]
133
134
  var contentTypes = [
134
135
  {value:"application/json",label:"application/json",hasValue: false},
@@ -138,7 +139,7 @@
138
139
  {value:"text/plain",label:"text/plain",hasValue: false},
139
140
  {value:"image/gif",label:"image/gif",hasValue: false},
140
141
  {value:"image/png",label:"image/png",hasValue: false},
141
- {value:"other",label:RED._("node-red:httpin.label.other"),icon:"red/images/typedInput/az.png"}
142
+ {value:"other",label:RED._("node-red:httpin.label.other"),icon:"red/images/typedInput/az.svg"}
142
143
  ];
143
144
 
144
145
  RED.nodes.registerType('http response',{
@@ -146,7 +147,10 @@
146
147
  color:"rgb(231, 231, 174)",
147
148
  defaults: {
148
149
  name: {value:""},
149
- statusCode: {value:"",validate: RED.validators.number(true)},
150
+ statusCode: {
151
+ value:"",
152
+ label: RED._("node-red:httpin.label.status"),
153
+ validate: RED.validators.number(true)},
150
154
  headers: {value:{}}
151
155
  },
152
156
  inputs:1,
@@ -176,7 +180,7 @@
176
180
  var propertyValue = $('<input/>',{class:"node-input-header-value",type:"text",style:"width: 100%"})
177
181
  .appendTo(propertyValueCell)
178
182
  .typedInput({types:
179
- header.h === 'content-type'?contentTypes:[{value:"other",label:"other",icon:"red/images/typedInput/az.png"}]
183
+ header.h === 'content-type'?contentTypes:[{value:"other",label:"other",icon:"red/images/typedInput/az.svg"}]
180
184
  });
181
185
 
182
186
  var matchedType = headerTypes.filter(function(ht) {
@@ -219,7 +223,7 @@
219
223
  if (type === 'content-type') {
220
224
  propertyValue.typedInput('types',contentTypes);
221
225
  } else {
222
- propertyValue.typedInput('types',[{value:"other",label:"other",icon:"red/images/typedInput/az.png"}]);
226
+ propertyValue.typedInput('types',[{value:"other",label:"other",icon:"red/images/typedInput/az.svg"}]);
223
227
  }
224
228
  });
225
229
  },