@node-red/nodes 2.2.0 → 3.0.0-beta.1

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 (51) hide show
  1. package/core/common/05-junction.html +5 -0
  2. package/core/common/05-junction.js +12 -0
  3. package/core/common/20-inject.html +25 -13
  4. package/core/common/21-debug.html +60 -6
  5. package/core/common/21-debug.js +60 -29
  6. package/core/common/60-link.html +66 -29
  7. package/core/common/60-link.js +169 -20
  8. package/core/common/lib/debug/debug-utils.js +34 -1
  9. package/core/function/10-function.html +57 -21
  10. package/core/function/10-switch.html +3 -1
  11. package/core/function/10-switch.js +1 -0
  12. package/core/function/15-change.html +40 -12
  13. package/core/function/16-range.html +14 -5
  14. package/core/function/80-template.html +16 -12
  15. package/core/function/89-delay.html +46 -6
  16. package/core/function/89-trigger.html +12 -4
  17. package/core/function/rbe.html +7 -3
  18. package/core/network/05-tls.html +10 -4
  19. package/core/network/06-httpproxy.html +10 -1
  20. package/core/network/10-mqtt.html +73 -17
  21. package/core/network/10-mqtt.js +205 -95
  22. package/core/network/21-httpin.html +6 -2
  23. package/core/network/21-httprequest.html +217 -12
  24. package/core/network/21-httprequest.js +98 -17
  25. package/core/network/22-websocket.html +19 -5
  26. package/core/network/22-websocket.js +16 -13
  27. package/core/network/31-tcpin.html +47 -10
  28. package/core/network/31-tcpin.js +8 -3
  29. package/core/network/32-udp.html +14 -2
  30. package/core/parsers/70-CSV.html +4 -1
  31. package/core/parsers/70-JSON.html +3 -2
  32. package/core/parsers/70-XML.html +2 -1
  33. package/core/parsers/70-YAML.html +2 -1
  34. package/core/sequence/17-split.html +5 -1
  35. package/core/sequence/19-batch.html +28 -4
  36. package/core/storage/10-file.html +68 -8
  37. package/core/storage/10-file.js +46 -3
  38. package/core/storage/23-watch.html +2 -1
  39. package/core/storage/23-watch.js +21 -43
  40. package/locales/de/messages.json +1 -0
  41. package/locales/en-US/common/60-link.html +18 -3
  42. package/locales/en-US/messages.json +68 -17
  43. package/locales/en-US/network/21-httprequest.html +1 -1
  44. package/locales/en-US/storage/10-file.html +6 -2
  45. package/locales/ja/common/60-link.html +12 -0
  46. package/locales/ja/messages.json +65 -18
  47. package/locales/ko/messages.json +1 -0
  48. package/locales/ru/messages.json +1 -0
  49. package/locales/zh-CN/messages.json +1 -0
  50. package/locales/zh-TW/messages.json +1 -0
  51. 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
  }
@@ -415,8 +477,12 @@ module.exports = function(RED) {
415
477
  setIfHasProperty(opts, node, "topicAliasMaximum", init);
416
478
  setIfHasProperty(opts, node, "maximumPacketSize", init);
417
479
  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);
480
+ //https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901116
481
+ if (hasProperty(opts, "userProperties")) {
482
+ node.userProperties = opts.userProperties;
483
+ } else if (hasProperty(opts, "userProps")) {
484
+ node.userProperties = opts.userProps;
485
+ }
420
486
 
421
487
  function createLWT(topic, payload, qos, retain, v5opts, v5SubPropName) {
422
488
  let message = undefined;
@@ -465,7 +531,7 @@ module.exports = function(RED) {
465
531
  };
466
532
  if(hasProperty(opts, "willTopic")) {
467
533
  //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");
534
+ node.options.will = createLWT(opts.willTopic, opts.willPayload, opts.willQos, opts.willRetain, opts.willMsg, "properties");
469
535
  };
470
536
  } else {
471
537
  //update options
@@ -637,24 +703,8 @@ module.exports = function(RED) {
637
703
 
638
704
  node.deregister = function(mqttNode,done) {
639
705
  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
- }
706
+ if (!node.closing && node.connected && Object.keys(node.users).length === 0) {
707
+ node.disconnect();
658
708
  }
659
709
  done();
660
710
  };
@@ -663,6 +713,7 @@ module.exports = function(RED) {
663
713
  }
664
714
  node.connect = function (callback) {
665
715
  if (node.canConnect()) {
716
+ node.closing = false;
666
717
  node.connecting = true;
667
718
  setStatusConnecting(node, true);
668
719
  try {
@@ -672,6 +723,7 @@ module.exports = function(RED) {
672
723
  let callbackDone = false; //prevent re-connects causing node.client.on('connect' firing callback multiple times
673
724
  // Register successful connect or reconnect handler
674
725
  node.client.on('connect', function (connack) {
726
+ node.closing = false;
675
727
  node.connecting = false;
676
728
  node.connected = true;
677
729
  if(!callbackDone && typeof callback == "function") {
@@ -740,6 +792,7 @@ module.exports = function(RED) {
740
792
  reasonCode: rc,
741
793
  reasonString: rs
742
794
  }
795
+ node.connected = false;
743
796
  node.log(RED._("mqtt.state.broker-disconnected", details));
744
797
  setStatusDisconnected(node, true);
745
798
  });
@@ -763,26 +816,48 @@ module.exports = function(RED) {
763
816
  }
764
817
  }
765
818
  };
819
+
766
820
  node.disconnect = function (callback) {
767
821
  const _callback = function () {
768
- setStatusDisconnected(node, true);
822
+ if(node.connected || node.connecting) {
823
+ setStatusDisconnected(node, true);
824
+ }
825
+ if(node.client) { node.client.removeAllListeners(); }
769
826
  node.connecting = false;
770
827
  node.connected = false;
771
828
  callback && typeof callback == "function" && callback();
772
829
  };
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
- }
830
+ if(!node.client) { return _callback(); }
831
+ if(node.closing) { return _callback(); }
832
+
833
+ let waitEnd = (client, ms) => {
834
+ return new Promise( (resolve, reject) => {
835
+ node.closing = true;
836
+ if(!client) {
837
+ resolve();
838
+ } else {
839
+ const t = setTimeout(reject, ms);
840
+ client.end(() => {
841
+ clearTimeout(t);
842
+ resolve()
843
+ });
844
+ }
845
+ });
846
+ };
847
+ if(node.connected && node.closeMessage) {
848
+ node.publish(node.closeMessage, function (err) {
849
+ waitEnd(node.client, 2000).then(() => {
850
+ _callback();
851
+ }).catch((e) => {
852
+ _callback();
853
+ })
854
+ });
784
855
  } else {
785
- _callback();
856
+ waitEnd(node.client, 2000).then(() => {
857
+ _callback();
858
+ }).catch((e) => {
859
+ _callback();
860
+ })
786
861
  }
787
862
  }
788
863
  node.subscriptionIds = {};
@@ -864,8 +939,18 @@ module.exports = function(RED) {
864
939
  qos: msg.qos || 0,
865
940
  retain: msg.retain || false
866
941
  };
942
+ let topicOK = hasProperty(msg, "topic") && (typeof msg.topic === "string") && (isValidPublishTopic(msg.topic));
867
943
  //https://github.com/mqttjs/MQTT.js/blob/master/README.md#mqttclientpublishtopic-message-options-callback
868
944
  if(node.options.protocolVersion == 5) {
945
+ const bsp = node.serverProperties || {};
946
+ if (msg.userProperties && typeof msg.userProperties !== "object") {
947
+ delete msg.userProperties;
948
+ }
949
+ if (hasProperty(msg, "topicAlias") && !isNaN(Number(msg.topicAlias))) {
950
+ msg.topicAlias = parseInt(msg.topicAlias);
951
+ } else {
952
+ delete msg.topicAlias;
953
+ }
869
954
  options.properties = options.properties || {};
870
955
  setStrProp(msg, options.properties, "responseTopic");
871
956
  setBufferProp(msg, options.properties, "correlationData");
@@ -875,29 +960,46 @@ module.exports = function(RED) {
875
960
  setIntProp(msg, options.properties, "topicAlias", 1, node.serverProperties.topicAliasMaximum || 0);
876
961
  setBoolProp(msg, options.properties, "payloadFormatIndicator");
877
962
  //FUTURE setIntProp(msg, options.properties, "subscriptionIdentifier", 1, 268435455);
878
- if (options.properties.topicAlias) {
879
- if (!node.topicAliases.hasOwnProperty(options.properties.topicAlias) && msg.topic == "") {
963
+
964
+ //check & sanitise topic
965
+ if (topicOK && options.properties.topicAlias) {
966
+ let aliasValid = (bsp.topicAliasMaximum && bsp.topicAliasMaximum >= options.properties.topicAlias);
967
+ if (!aliasValid) {
880
968
  done("Invalid topicAlias");
881
969
  return
882
970
  }
883
971
  if (node.topicAliases[options.properties.topicAlias] === msg.topic) {
884
- msg.topic = ""
972
+ msg.topic = "";
885
973
  } else {
886
- node.topicAliases[options.properties.topicAlias] = msg.topic
974
+ node.topicAliases[options.properties.topicAlias] = msg.topic;
887
975
  }
976
+ } else if (!msg.topic && options.properties.responseTopic) {
977
+ msg.topic = msg.responseTopic;
978
+ topicOK = isValidPublishTopic(msg.topic);
979
+ delete msg.responseTopic; //prevent responseTopic being resent?
888
980
  }
889
981
  }
890
982
 
891
- node.client.publish(msg.topic, msg.payload, options, function(err) {
892
- done && done(err);
893
- return
894
- });
983
+ if (topicOK) {
984
+ node.client.publish(msg.topic, msg.payload, options, function(err) {
985
+ done && done(err);
986
+ return
987
+ });
988
+ } else {
989
+ const error = new Error(RED._("mqtt.errors.invalid-topic"));
990
+ error.warn = true;
991
+ done(error);
992
+ }
895
993
  }
896
994
  };
897
995
 
898
996
  node.on('close', function(done) {
899
- node.closing = true;
900
- node.disconnect(done);
997
+ node.disconnect(function() {
998
+ if(node.client) {
999
+ node.client.removeAllListeners();
1000
+ }
1001
+ done();
1002
+ });
901
1003
  });
902
1004
 
903
1005
  }
@@ -1074,6 +1176,9 @@ module.exports = function(RED) {
1074
1176
  node.brokerConn.unsubscribe(node.topic,node.id, removed);
1075
1177
  }
1076
1178
  node.brokerConn.deregister(node, done);
1179
+ node.brokerConn = null;
1180
+ } else {
1181
+ done();
1077
1182
  }
1078
1183
  });
1079
1184
  } else {
@@ -1134,7 +1239,12 @@ module.exports = function(RED) {
1134
1239
  }
1135
1240
  node.brokerConn.register(node);
1136
1241
  node.on('close', function(done) {
1137
- node.brokerConn.deregister(node,done);
1242
+ if (node.brokerConn) {
1243
+ node.brokerConn.deregister(node,done);
1244
+ node.brokerConn = null;
1245
+ } else {
1246
+ done();
1247
+ }
1138
1248
  });
1139
1249
  } else {
1140
1250
  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}
@@ -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,