@node-red/nodes 2.2.2 → 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 +57 -27
  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 +191 -80
  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 +63 -16
  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
@@ -750,32 +816,48 @@ module.exports = function(RED) {
750
816
  }
751
817
  }
752
818
  };
819
+
753
820
  node.disconnect = function (callback) {
754
- const _callback = function (resetNodeConnectedState) {
755
- setStatusDisconnected(node, true);
756
- if(resetNodeConnectedState) {
757
- node.closing = true;
758
- node.connecting = false;
759
- node.connected = false;
821
+ const _callback = function () {
822
+ if(node.connected || node.connecting) {
823
+ setStatusDisconnected(node, true);
760
824
  }
825
+ if(node.client) { node.client.removeAllListeners(); }
826
+ node.connecting = false;
827
+ node.connected = false;
761
828
  callback && typeof callback == "function" && callback();
762
829
  };
830
+ if(!node.client) { return _callback(); }
831
+ if(node.closing) { return _callback(); }
763
832
 
764
- if(node.closing) {
765
- return _callback(false);
766
- }
767
- var endCallBack = function endCallBack() {
768
- }
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
+ };
769
847
  if(node.connected && node.closeMessage) {
770
848
  node.publish(node.closeMessage, function (err) {
771
- node.client.end(endCallBack);
772
- _callback(true);
849
+ waitEnd(node.client, 2000).then(() => {
850
+ _callback();
851
+ }).catch((e) => {
852
+ _callback();
853
+ })
773
854
  });
774
- } else if(node.connected) {
775
- node.client.end(endCallBack);
776
- _callback(true);
777
855
  } else {
778
- _callback(false);
856
+ waitEnd(node.client, 2000).then(() => {
857
+ _callback();
858
+ }).catch((e) => {
859
+ _callback();
860
+ })
779
861
  }
780
862
  }
781
863
  node.subscriptionIds = {};
@@ -857,8 +939,18 @@ module.exports = function(RED) {
857
939
  qos: msg.qos || 0,
858
940
  retain: msg.retain || false
859
941
  };
942
+ let topicOK = hasProperty(msg, "topic") && (typeof msg.topic === "string") && (isValidPublishTopic(msg.topic));
860
943
  //https://github.com/mqttjs/MQTT.js/blob/master/README.md#mqttclientpublishtopic-message-options-callback
861
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
+ }
862
954
  options.properties = options.properties || {};
863
955
  setStrProp(msg, options.properties, "responseTopic");
864
956
  setBufferProp(msg, options.properties, "correlationData");
@@ -868,29 +960,46 @@ module.exports = function(RED) {
868
960
  setIntProp(msg, options.properties, "topicAlias", 1, node.serverProperties.topicAliasMaximum || 0);
869
961
  setBoolProp(msg, options.properties, "payloadFormatIndicator");
870
962
  //FUTURE setIntProp(msg, options.properties, "subscriptionIdentifier", 1, 268435455);
871
- if (options.properties.topicAlias) {
872
- 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) {
873
968
  done("Invalid topicAlias");
874
969
  return
875
970
  }
876
971
  if (node.topicAliases[options.properties.topicAlias] === msg.topic) {
877
- msg.topic = ""
972
+ msg.topic = "";
878
973
  } else {
879
- node.topicAliases[options.properties.topicAlias] = msg.topic
974
+ node.topicAliases[options.properties.topicAlias] = msg.topic;
880
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?
881
980
  }
882
981
  }
883
982
 
884
- node.client.publish(msg.topic, msg.payload, options, function(err) {
885
- done && done(err);
886
- return
887
- });
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
+ }
888
993
  }
889
994
  };
890
995
 
891
996
  node.on('close', function(done) {
892
- node.closing = true;
893
- node.disconnect(done);
997
+ node.disconnect(function() {
998
+ if(node.client) {
999
+ node.client.removeAllListeners();
1000
+ }
1001
+ done();
1002
+ });
894
1003
  });
895
1004
 
896
1005
  }
@@ -1067,6 +1176,7 @@ module.exports = function(RED) {
1067
1176
  node.brokerConn.unsubscribe(node.topic,node.id, removed);
1068
1177
  }
1069
1178
  node.brokerConn.deregister(node, done);
1179
+ node.brokerConn = null;
1070
1180
  } else {
1071
1181
  done();
1072
1182
  }
@@ -1131,6 +1241,7 @@ module.exports = function(RED) {
1131
1241
  node.on('close', function(done) {
1132
1242
  if (node.brokerConn) {
1133
1243
  node.brokerConn.deregister(node,done);
1244
+ node.brokerConn = null;
1134
1245
  } else {
1135
1246
  done();
1136
1247
  }
@@ -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,