node-red-contrib-remote 1.2.3 → 1.3.3

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.
package/README.md CHANGED
@@ -26,7 +26,7 @@ There is a help page describing how the the nodes work together and how to conne
26
26
 
27
27
  [Help in English](https://www.remote-red.com/en/help/)
28
28
 
29
- [Hilfe in deutsch](https://www.remote-red.com/de/hilfe/)
29
+ [Hilfe in Deutsch](https://www.remote-red.com/de/hilfe/)
30
30
 
31
31
 
32
32
  ## Contact
@@ -36,6 +36,19 @@ You will find more information on [www.remote-red-com](https://www.remote-red.co
36
36
 
37
37
  ## Version History
38
38
 
39
+ Version 1.3.3:
40
+ - Improved compatibility for Windows.
41
+
42
+ Version 1.3.2:
43
+ - Changed the method to get the installed node version. Thanks hardillb!
44
+
45
+ Version 1.3.1:
46
+ - Added timeout handling to the heartbeat function.
47
+ - Catches errors when evaluating the title, body or sound of an notification.
48
+
49
+ Version 1.3.0:
50
+ - A heartbeat function is added to the remote access node. This will automatically reconnect if the connection is interrupted, e.g. due to an unstable internet connection.
51
+
39
52
  Version 1.2.3:
40
53
  - The settings base url and port are filled automatically for new config nodes. This is helpful when using alternative installations such as RedMatic.
41
54
  - Notifications and Questions can have a computed sound. So you can include the sound based on the incoming message.
@@ -11,6 +11,9 @@
11
11
  "serving": "aktiv",
12
12
  "stopping": "stoppe",
13
13
  "stopped": "gestoppt",
14
+ "heartbeaterror": "Kommuniktionsfehler",
15
+ "heartbeaterroractive": "aktiv - kein Webserver erkannt",
16
+ "heartbeaterrornotfound": "aktiv - jedoch 404 erhalten",
14
17
  "commerror": "Fehler bei der Kommunikation mit dem Server.",
15
18
  "noconfig": "Keine Konfiguration gefunden.",
16
19
  "incompleteconfig": "Konfiguration unvollständig.",
@@ -11,6 +11,9 @@
11
11
  "serving": "serving",
12
12
  "stopping": "stopping",
13
13
  "stopped": "stopped",
14
+ "heartbeaterror": "communication error",
15
+ "heartbeaterroractive": "serving - no endpoint detected",
16
+ "heartbeaterrornotfound": "serving - but received 404 response",
14
17
  "commerror": "Error communicating with server.",
15
18
  "noconfig": "No configuration found.",
16
19
  "incompleteconfig": "Configuration incomplete.",
@@ -2,10 +2,18 @@ module.exports = function(RED) {
2
2
  const commons = require('./remote-commons');
3
3
  const child_process = require('child_process');
4
4
  const os = require("os");
5
+ let sshprocess = undefined
6
+ let statustext = ''
5
7
 
6
8
  function startSSH(node, server, port) {
7
9
  try {
8
10
  node.log("starting ssh process");
11
+
12
+ // Reset heartbeat status
13
+ node.initialHeartbeatStatus = ''
14
+ node.lastHeartbeatStatus = ''
15
+
16
+ // Create ssh command
9
17
  let host = node.confignode.host;
10
18
  if ( host === 'localhost' && os.platform() === 'win32' ) {
11
19
  host = '127.0.0.1'
@@ -15,10 +23,10 @@ module.exports = function(RED) {
15
23
  if ( node.verbose ) {
16
24
  sshparameters.push('-v');
17
25
  }
18
- const sshprocess = child_process.spawn("ssh", sshparameters);
26
+ sshprocess = child_process.spawn("ssh", sshparameters);
19
27
 
20
28
  // Set serving.. if not working, the process will exit or close
21
- node.status({fill:"green",shape:"dot",text:"remote-access.status.serving"});
29
+ setStatus(node, {fill:"green",shape:"dot",text:"remote-access.status.serving"});
22
30
  node.serving = true
23
31
 
24
32
  // Attach to process events
@@ -31,13 +39,17 @@ module.exports = function(RED) {
31
39
  });
32
40
 
33
41
  sshprocess.on('close', (code, signal) => {
34
- node.status({fill:"red",shape:"dot",text:"remote-access.status.stopped"});
42
+ if ( statustext !== "remote-access.status.heartbeaterror" ) {
43
+ setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.stopped"});
44
+ }
35
45
  node.serving = false
36
46
  node.log("ssh process stopped (close: " + code + " / " + signal + ")");
37
47
  });
38
48
 
39
49
  sshprocess.on('exit', (code, signal) => {
40
- node.status({fill:"red",shape:"dot",text:"remote-access.status.stopped"});
50
+ if ( statustext !== "remote-access.status.heartbeaterror" ) {
51
+ setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.stopped"});
52
+ }
41
53
  node.serving = false
42
54
  node.log("ssh process stopped (exit: " + code + " / " + signal + ")");
43
55
  });
@@ -47,7 +59,7 @@ module.exports = function(RED) {
47
59
  });
48
60
 
49
61
  node.on('close', function() {
50
- node.status({fill:"red",shape:"dot",text:"remote-access.status.stopping"});
62
+ setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.stopping"});
51
63
  node.log("stopping ssh process");
52
64
  sshprocess.kill();
53
65
  });
@@ -68,6 +80,14 @@ module.exports = function(RED) {
68
80
  }
69
81
  }
70
82
 
83
+ function setStatus(node, options) {
84
+ // Set the status, remember text
85
+ if ( options !== undefined && options.text !== undefined ) {
86
+ statustext = options.text
87
+ }
88
+ node.status(options);
89
+ }
90
+
71
91
  function requestInstanceSlot(node) {
72
92
  // Call API to retrive server and port.
73
93
  const axiosInstance = commons.createAxiosInstance();
@@ -89,7 +109,7 @@ module.exports = function(RED) {
89
109
  if ( error.response && error.response.data && error.response.data.message ) {
90
110
  node.error(`${error.response.data.message}`);
91
111
  }
92
- node.status({fill:"red",shape:"dot",text:"remote-access.status.commerror"});
112
+ setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.commerror"});
93
113
  return;
94
114
  });
95
115
  }
@@ -125,6 +145,53 @@ module.exports = function(RED) {
125
145
  }
126
146
  }
127
147
 
148
+ function heartbeat(node) {
149
+ // Performs a heartbeat: Ask to the server if ssh is available
150
+ if ( node.serving ) {
151
+ const axiosInstance = commons.createAxiosInstance();
152
+ axiosInstance.post(`https://api-${node.confignode.server}/heartbeat`, {
153
+ 'instancehash': node.confignode.instancehash,
154
+ 'instanceauth': node.confignode.instanceauth,
155
+ 'version': commons.getNodeVersion()
156
+ }, {
157
+ 'timeout': 60*1000
158
+ })
159
+ .then(response => {
160
+ // Remember the status
161
+ if ( node.initialHeartbeatStatus === '' ) {
162
+ node.initialHeartbeatStatus = response.data.status
163
+ }
164
+ node.lastHeartbeatStatus = response.data.status
165
+
166
+ // If the communication is not ok...
167
+ if ( node.lastHeartbeatStatus === 'NOTFOUND' ) {
168
+ // The local endpoint responed a 404...
169
+ setStatus(node, {fill:"yellow",shape:"dot",text:"remote-access.status.heartbeaterrornotfound"});
170
+ node.log(`Heartbeat detected no valid endpoint, got a 404 response. Please check the base URL in the connection settings.`);
171
+ } else if ( node.lastHeartbeatStatus !== 'OK' ) {
172
+ if ( node.initialHeartbeatStatus === 'OK' ) {
173
+ // If the status before was ok > Restart communication
174
+ setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.heartbeaterror"});
175
+ sshprocess.kill();
176
+ node.serving = false
177
+ node.log(`Heartbeat error. Reconnecting soon.`);
178
+ } else {
179
+ // If the status before had also an error > Just change the label.
180
+ setStatus(node, {fill:"yellow",shape:"dot",text:"remote-access.status.heartbeaterroractive"});
181
+ node.log(`Heartbeat detected no valid endpoint. Please check your connection settings (Base URL, Serving Port and Protocol).`);
182
+ }
183
+ }
184
+ })
185
+ .catch((error) => {
186
+ // Error on api call > Just log
187
+ node.error('heartbeat: ' + error);
188
+ if ( error.response && error.response.data && error.response.data.message ) {
189
+ node.error(`${error.response.data.message}`);
190
+ }
191
+ });
192
+ }
193
+ }
194
+
128
195
  function RemoteAccessNode(config) {
129
196
  RED.nodes.createNode(this,config);
130
197
  const node = this;
@@ -133,7 +200,7 @@ module.exports = function(RED) {
133
200
  node.errorcounter = 0;
134
201
 
135
202
  // Status
136
- node.status({fill:"orange",shape:"dot",text:"remote-access.status.starting"});
203
+ setStatus(node, {fill:"orange",shape:"dot",text:"remote-access.status.starting"});
137
204
 
138
205
  // Retrieve the config node
139
206
  node.confignode = RED.nodes.getNode(config.confignode);
@@ -141,20 +208,25 @@ module.exports = function(RED) {
141
208
  node.log("Server: " + node.confignode.server + " InstanceHash: " + node.confignode.instancehash );
142
209
  } else {
143
210
  node.log("No configuration found.");
144
- node.status({fill:"red",shape:"dot",text:"remote-access.status.noconfig"});
211
+ setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.noconfig"});
145
212
  return;
146
213
  }
147
214
  if ( node.confignode.instancehash === undefined || node.confignode.instancehash === '' ) {
148
215
  node.log("Configuration incomplete.");
149
- node.status({fill:"red",shape:"dot",text:"remote-access.status.incompleteconfig"});
216
+ setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.incompleteconfig"});
150
217
  return;
151
218
  }
152
219
  if ( node.confignode.instancehash !== undefined && node.confignode.instanceauth === undefined ) {
153
220
  node.log("Configuration has no instanceauth. Maybe restored backup!");
154
- node.status({fill:"red",shape:"dot",text:"remote-access.status.backupconfig"});
221
+ setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.backupconfig"});
155
222
  return;
156
223
  }
157
224
 
225
+ // Init heartbeat
226
+ node.initialHeartbeatStatus = ''
227
+ node.lastHeartbeatStatus = ''
228
+ node.heartbeatinterval = setInterval(heartbeat, 5*60*1000, node);
229
+
158
230
  // Call API for announce the instacehash and authentication, retrive server and port.
159
231
  tryConnect(node);
160
232
 
@@ -195,6 +267,7 @@ module.exports = function(RED) {
195
267
  // Cancel timeouts
196
268
  clearTimeout(node.checkservingtimeout);
197
269
  clearTimeout(node.tryconnecttimeout);
270
+ clearInterval(node.heartbeatinterval);
198
271
 
199
272
  // Remove old routes, without a new deploy would break it..
200
273
  RED.httpNode._router.stack.forEach(function(route,i,routes) {
@@ -2,11 +2,13 @@ const https = require('https')
2
2
  const axios = require('axios')
3
3
  const fs = require('fs');
4
4
  const crypto = require('crypto');
5
+ const path = require('path');
5
6
 
6
7
  module.exports = {
7
8
  createAxiosInstance: function() {
9
+ var filepath = path.join(__dirname, 'resources', 'ca.cer');
8
10
  const httpsAgent = new https.Agent({
9
- ca: fs.readFileSync(__dirname + '/resources/ca.cer'),
11
+ ca: fs.readFileSync(filepath),
10
12
  checkServerIdentity: function(host, cert) {
11
13
  const pubkeyPinned = 'YHGEo54+LKxdpCuSAFt+Zwx/RVSHER96vM/Rh0/zcQ4=';
12
14
  const pubkeyServer = crypto.createHash('sha256').update(cert.pubkey).digest('base64');
@@ -19,7 +21,20 @@ module.exports = {
19
21
  },
20
22
 
21
23
  getNodeVersion: function() {
22
- var pjson = require(__dirname.replace('/nodes', '') + '/package.json');
24
+ var filepath = path.join(__dirname, '..', 'package.json');
25
+ var pjson = require(filepath);
23
26
  return pjson.version;
27
+ },
28
+
29
+ evaluateValue: function(RED, value, type, node, msg) {
30
+ // Evaluates the value
31
+ let evalValue = ''
32
+ try {
33
+ evalValue = RED.util.evaluateNodeProperty(value, type, node, msg);
34
+ } catch (e) {
35
+ node.error(`Error evaluating value: ${e}`)
36
+ }
37
+ return evalValue;
24
38
  }
39
+
25
40
  }
@@ -20,8 +20,8 @@ module.exports = function(RED) {
20
20
  if (node.confignode != undefined) {
21
21
  node.limiter.removeTokens(1, function(err, remainingRequests) {
22
22
  if (remainingRequests >= 0) {
23
- const title = RED.util.evaluateNodeProperty(config.notificationTitle, config.notificationTitleType, node, msg);
24
- const body = RED.util.evaluateNodeProperty(config.notificationBody, config.notificationBodyType, node, msg);
23
+ const title = commons.evaluateValue(RED, config.notificationTitle, config.notificationTitleType, node, msg);
24
+ const body = commons.evaluateValue(RED, config.notificationBody, config.notificationBodyType, node, msg);
25
25
  const notificationEmpty = ((title == undefined || title == '') && (body == undefined || body == ''));
26
26
  if (!notificationEmpty) {
27
27
  // Title and/or body are filled
@@ -34,7 +34,7 @@ module.exports = function(RED) {
34
34
  // Sound configured or computed?
35
35
  let sound = config.notificationSound
36
36
  if (sound === 'computed') {
37
- sound = RED.util.evaluateNodeProperty(config.notificationSoundComputed, config.notificationSoundComputedType, node, msg);
37
+ sound = commons.evaluateValue(RED, config.notificationSoundComputed, config.notificationSoundComputedType, node, msg);
38
38
  }
39
39
 
40
40
  // Call API to send notification
@@ -20,8 +20,8 @@ module.exports = function(RED) {
20
20
  if (node.confignode != undefined) {
21
21
  node.limiter.removeTokens(1, function(err, remainingRequests) {
22
22
  if (remainingRequests >= 0) {
23
- const title = RED.util.evaluateNodeProperty(config.questionTitle, config.questionTitleType, node, msg);
24
- const body = RED.util.evaluateNodeProperty(config.questionBody, config.questionBodyType, node, msg);
23
+ const title = commons.evaluateValue(RED, config.questionTitle, config.questionTitleType, node, msg);
24
+ const body = commons.evaluateValue(RED, config.questionBody, config.questionBodyType, node, msg);
25
25
  const notificationEmpty = ((title == undefined || title == '') && (body == undefined || body == ''));
26
26
  if (!notificationEmpty) {
27
27
  // Title and/or body are filled, generate question data
@@ -31,16 +31,16 @@ module.exports = function(RED) {
31
31
  "questiontype" : 1,
32
32
  "answers": [
33
33
  {
34
- "text": RED.util.evaluateNodeProperty(config.questionAnswerOne, config.questionAnswerOneType, node, msg),
35
- "value": RED.util.evaluateNodeProperty(config.questionAnswerOneValue, config.questionAnswerOneValueType, node, msg)
34
+ "text": commons.evaluateValue(RED, config.questionAnswerOne, config.questionAnswerOneType, node, msg),
35
+ "value": commons.evaluateValue(RED, config.questionAnswerOneValue, config.questionAnswerOneValueType, node, msg)
36
36
  },
37
37
  {
38
- "text": RED.util.evaluateNodeProperty(config.questionAnswerTwo, config.questionAnswerTwoType, node, msg),
39
- "value": RED.util.evaluateNodeProperty(config.questionAnswerTwoValue, config.questionAnswerTwoValueType, node, msg)
38
+ "text": commons.evaluateValue(RED, config.questionAnswerTwo, config.questionAnswerTwoType, node, msg),
39
+ "value": commons.evaluateValue(RED, config.questionAnswerTwoValue, config.questionAnswerTwoValueType, node, msg)
40
40
  },
41
41
  {
42
- "text": RED.util.evaluateNodeProperty(config.questionAnswerThree, config.questionAnswerThreeType, node, msg),
43
- "value": RED.util.evaluateNodeProperty(config.questionAnswerThreeValue, config.questionAnswerThreeValueType, node, msg)
42
+ "text": commons.evaluateValue(RED, config.questionAnswerThree, config.questionAnswerThreeType, node, msg),
43
+ "value": commons.evaluateValue(RED, config.questionAnswerThreeValue, config.questionAnswerThreeValueType, node, msg)
44
44
  }
45
45
  ]
46
46
  };
@@ -53,7 +53,7 @@ module.exports = function(RED) {
53
53
  // Sound configured or computed?
54
54
  let sound = config.questionSound
55
55
  if (sound === 'computed') {
56
- sound = RED.util.evaluateNodeProperty(config.questionSoundComputed, config.questionSoundComputedType, node, msg);
56
+ sound = commons.evaluateValue(RED, config.questionSoundComputed, config.questionSoundComputedType, node, msg);
57
57
  }
58
58
 
59
59
  // Call API to send notification
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-remote",
3
- "version": "1.2.3",
3
+ "version": "1.3.3",
4
4
  "description": "Remote services for Node-RED including remote access, push notifications and questions through push notifications.",
5
5
  "author": "Thorsten Heilmann",
6
6
  "license": "GPL-3.0",
@@ -16,6 +16,7 @@
16
16
  "axios": "^0.21.0",
17
17
  "internal-ip": "^6.1.0",
18
18
  "limiter": "^1.1.5",
19
+ "path": "^0.12.7",
19
20
  "qrcode": "^1.4.4"
20
21
  },
21
22
  "main": "remote-config.js",