node-red-contrib-remote 1.4.0 → 1.5.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Remote-RED
2
2
 
3
- Remote-RED is an ecosystem to bring remote access and push notifications to Node-RED. It consists of several parts:
3
+ Remote-RED is an ecosystem to bring remote access, push notification and geofencing to Node-RED. There are also additional functions like directly answer on a notification and homescreen widgets. It consists of several parts:
4
4
  - node-red-contrib-remote: The Node-RED nodes of Remote-RED. Published in this repro.
5
5
  - Remote-RED servers: Several servers used for proxying the remote access traffic and sending push notifications.
6
6
  - Remote-RED apps: Apps to access the services published by the Remote-RED remote-access node and receiving the push notifications of the remote-notification node. Available or Android, iOS and iPadOS.
@@ -36,6 +36,17 @@ 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.5.1
40
+ - Added support for running Node-RED with a self signed https certificate.
41
+ - Notifications and Questions will automatically convert values to string if a string is needed (e.g. title and body).
42
+
43
+ Version 1.5.0
44
+ - Geofencing! You can add geofences in the apps. Entering and leaving them will trigger a message on the new output of the remote access node.
45
+ - Adding server location in Asia.
46
+ - New instances will have the base url '/ui' again.
47
+ - Error with "Multiple requests" after a missing internet connection fixed.
48
+ - iOS App: Links with target '_blank' will be redirected to iOS.
49
+
39
50
  Version 1.4.0
40
51
  - In addition to the QR code, a link is now generated to add an instance in the app.
41
52
  - Sending notifications and questions is prevented if msg.playload.send === false.
@@ -17,7 +17,8 @@
17
17
  "commerror": "Fehler bei der Kommunikation mit dem Server.",
18
18
  "noconfig": "Keine Konfiguration gefunden.",
19
19
  "incompleteconfig": "Konfiguration unvollständig.",
20
- "backupconfig": "Konfiguration aus Backup."
20
+ "backupconfig": "Konfiguration aus Backup.",
21
+ "banned": "Die Instanz wurde deaktiviert. Möglicherweise aufgrund einer DDOS-Attacke. Wenn Sie dieses System nicht missbrauchen, erstelle bitte eine neuen Konfig Node, um Remote-RED wieder zu verwenden. Andernfalls suchen Sie nach falschen Weiterleitungen oder Proxy Fehlern. Diese IP wird für 10 Minuten gesperrt."
21
22
  },
22
23
  "help": {
23
24
  "title": "Hilfe",
@@ -12,6 +12,7 @@
12
12
  "region": "Standort Server",
13
13
  "region_us": "Amerika (USA)",
14
14
  "region_de": "Europa (Deutschland)",
15
+ "region_sg": "Asien (Singapur)",
15
16
  "region_dev": "Entwicklung",
16
17
  "region_desc": "Der Standort kann nicht mehr geändert werden, sobald eine App registriert wurde.",
17
18
  "change_info": "Wenn Du Einstellungen änderst, musst Du die App neu verbinden.",
@@ -17,7 +17,8 @@
17
17
  "commerror": "Error communicating with server.",
18
18
  "noconfig": "No configuration found.",
19
19
  "incompleteconfig": "Configuration incomplete.",
20
- "backupconfig": "Configuration from backup."
20
+ "backupconfig": "Configuration from backup.",
21
+ "banned": "Instance is deactivated. Maybe due DDOS attacks. If you are not missusing this system, please create new config node to use Remote-RED again. Otherwise look for incorrect redirects or such errors. Banning IP for 10 minutes."
21
22
  },
22
23
  "help": {
23
24
  "title": "Help",
@@ -12,6 +12,7 @@
12
12
  "region": "Server location",
13
13
  "region_us": "America (USA)",
14
14
  "region_de": "Europe (Germany)",
15
+ "region_sg": "Asia (Singapur)",
15
16
  "region_dev": "Development host",
16
17
  "region_desc": "The region can´t be changed after registering an app.",
17
18
  "change_info": "If you change settings, you need to reconnect the app.",
@@ -8,10 +8,8 @@
8
8
  verbose: {value:0}
9
9
  },
10
10
  inputs: 0,
11
- outputs: 1,
12
- outputLabels: ["action output"],
13
- // outputs: 2,
14
- // outputLabels: ["action output", "geofence output"],
11
+ outputs: 2,
12
+ outputLabels: ["action output", "geofence output"],
15
13
  icon: "font-awesome/fa-mobile",
16
14
  label: function() {
17
15
  return this.name || "remote access";
@@ -4,6 +4,7 @@ module.exports = function(RED) {
4
4
  const os = require("os");
5
5
  let sshprocess = undefined
6
6
  let statustext = ''
7
+ let instanceIsBanned = false
7
8
 
8
9
  function startSSH(node, server, port) {
9
10
  try {
@@ -18,7 +19,7 @@ module.exports = function(RED) {
18
19
  if ( host === 'localhost' && os.platform() === 'win32' ) {
19
20
  host = '127.0.0.1'
20
21
  }
21
- //node.log('ssh -o StrictHostKeyChecking=no -R ' + port.toString() + ':' + host + ':' + node.confignode.port.toString() + ' forward@proxy-' + server + ' -N');
22
+ // node.log('ssh -o StrictHostKeyChecking=no -R ' + port.toString() + ':' + host + ':' + node.confignode.port.toString() + ' forward@proxy-' + server + ' -N');
22
23
  var sshparameters = ['-o StrictHostKeyChecking=no', '-R', port.toString() + ':' + host + ':' + node.confignode.port.toString(), 'forward@proxy-' + server, '-N'];
23
24
  if ( node.verbose ) {
24
25
  sshparameters.push('-v');
@@ -36,6 +37,9 @@ module.exports = function(RED) {
36
37
 
37
38
  sshprocess.stderr.on('data', (data) => {
38
39
  logSSHBuffer(node, data, 'ssh process stderr');
40
+ if ( data.toString().includes('Connection timed out')) {
41
+ node.error(`SSH connection timeout. Please check if port 22 is open for outgoing connections.`);
42
+ }
39
43
  });
40
44
 
41
45
  sshprocess.on('close', (code, signal) => {
@@ -89,6 +93,11 @@ module.exports = function(RED) {
89
93
  }
90
94
 
91
95
  function requestInstanceSlot(node) {
96
+ // Is this instance banned?
97
+ if ( instanceIsBanned ) {
98
+ return;
99
+ }
100
+
92
101
  // Call API to retrive server and port.
93
102
  const axiosInstance = commons.createAxiosInstance();
94
103
  axiosInstance.post(`https://api-${node.confignode.server}/instanceSlotRequest`, {
@@ -105,10 +114,22 @@ module.exports = function(RED) {
105
114
  startSSH(node, node.confignode.server, port);
106
115
  })
107
116
  .catch((error) => {
108
- node.error('requestInstanceSlot: ' + error);
109
- if ( error.response && error.response.data && error.response.data.message ) {
110
- node.error(`${error.response.data.message}`);
117
+ // Log error
118
+ node.error('requestInstanceSlot: ' + commons.getNetworkErrorString(error))
119
+ if ( commons.getNetworkErrorCustomString(error) !== undefined) {
120
+ node.error(commons.getNetworkErrorCustomString(error));
111
121
  }
122
+
123
+ // Wenn gebannt -> Merken
124
+ if ( error.response && error.response.status ) {
125
+ if ( error.response.status === 403 ) {
126
+ setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.banned"});
127
+ instanceIsBanned = true;
128
+ return;
129
+ }
130
+ }
131
+
132
+ // Set status
112
133
  setStatus(node, {fill:"red",shape:"dot",text:"remote-access.status.commerror"});
113
134
  return;
114
135
  });
@@ -183,10 +204,10 @@ module.exports = function(RED) {
183
204
  }
184
205
  })
185
206
  .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}`);
207
+ // Error on api call > Log error
208
+ node.error('heartbeat: ' + commons.getNetworkErrorString(error))
209
+ if ( commons.getNetworkErrorCustomString(error) !== undefined) {
210
+ node.error(commons.getNetworkErrorCustomString(error));
190
211
  }
191
212
  });
192
213
  }
@@ -8,6 +8,7 @@ module.exports = {
8
8
  createAxiosInstance: function() {
9
9
  var filepath = path.join(__dirname, 'resources', 'ca.cer');
10
10
  const httpsAgent = new https.Agent({
11
+ rejectUnauthorized: false,
11
12
  ca: fs.readFileSync(filepath),
12
13
  checkServerIdentity: function(host, cert) {
13
14
  const pubkeyPinned = 'YHGEo54+LKxdpCuSAFt+Zwx/RVSHER96vM/Rh0/zcQ4=';
@@ -17,7 +18,7 @@ module.exports = {
17
18
  }
18
19
  }
19
20
  });
20
- return axios.create({ httpsAgent: httpsAgent });
21
+ return axios.create({ httpsAgent: httpsAgent, timeout: 5000 });
21
22
  },
22
23
 
23
24
  getNodeVersion: function() {
@@ -26,15 +27,40 @@ module.exports = {
26
27
  return pjson.version;
27
28
  },
28
29
 
29
- evaluateValue: function(RED, value, type, node, msg) {
30
+ evaluateValue: function(RED, value, type, node, msg, tostring) {
30
31
  // Evaluates the value
31
32
  let evalValue = ''
32
33
  try {
33
34
  evalValue = RED.util.evaluateNodeProperty(value, type, node, msg);
35
+ if (tostring) {
36
+ if (evalValue == undefined) evalValue = '';
37
+ evalValue = String(evalValue);
38
+ }
34
39
  } catch (e) {
35
40
  node.error(`Error evaluating value: ${e}`)
36
41
  }
37
42
  return evalValue;
43
+ },
44
+
45
+ getNetworkErrorString: function(error) {
46
+ // Trys to get a error message
47
+ let errorString = `` + error;
48
+ if ( error.response && error.response.data && error.response.data.message ) {
49
+ errorString = `${error.response.data.message}`;
50
+ }
51
+ return errorString;
52
+ },
53
+
54
+ getNetworkErrorCustomString: function(error) {
55
+ // Custom error messages for common errors
56
+ let errorString = undefined;
57
+ if ( error.code === 'ECONNABORTED' ) {
58
+ errorString = `Error during https call. Please check Internet connection on your Node-RED server. Please check if a firewall blocks outgoing port 443.`;
59
+ }
60
+ if ( error.code === 'EAI_AGAIN' ) {
61
+ errorString = `Error during DNS queries. Please check Internet connection on your Node-RED server. Please check if a firewall blocks your DNS queries.`;
62
+ }
63
+ return errorString;
38
64
  }
39
65
 
40
66
  }
@@ -103,6 +103,9 @@
103
103
  } else {
104
104
  // Show error
105
105
  alert('Error while registering instance: ' + data.error);
106
+
107
+ // Enable button again
108
+ document.getElementById('registerRemoteAppButton').disabled = false;
106
109
  }
107
110
  });
108
111
  }
@@ -214,6 +217,7 @@
214
217
  <select id="node-config-input-region">
215
218
  <option value="us" data-i18n="remote-config.label.region_us"></option>
216
219
  <option value="de" data-i18n="remote-config.label.region_de"></option>
220
+ <option value="sg" data-i18n="remote-config.label.region_sg"></option>
217
221
  <!-- <option value="dev" data-i18n="remote-config.label.region_dev"></option> -->
218
222
  </select>
219
223
  </div>
@@ -27,7 +27,7 @@ module.exports = function(RED) {
27
27
  // Return the internal IP and the mountpath
28
28
  const ipData = {
29
29
  'ipv4': internalIp.v4.sync(),
30
- 'baseurl': RED.httpNode.mountpath + ((RED.settings.ui !== undefined && RED.settings.ui.path !== undefined) ? RED.settings.ui.path : ''),
30
+ 'baseurl': RED.httpNode.mountpath + ((RED.settings.ui !== undefined && RED.settings.ui.path !== undefined) ? RED.settings.ui.path : 'ui'),
31
31
  'port': (RED.settings.uiPort !== undefined) ? String(RED.settings.uiPort) : '1880'
32
32
  }
33
33
  console.log(`${JSON.stringify(ipData)}`);
@@ -48,6 +48,9 @@ module.exports = function(RED) {
48
48
  if ( error.response && error.response.data && error.response.data.message ) {
49
49
  errorMessage = errorMessage + " / " + error.response.data.message;
50
50
  }
51
+ if ( commons.getNetworkErrorCustomString(error) !== undefined) {
52
+ errorMessage = commons.getNetworkErrorCustomString(error);
53
+ }
51
54
  res.json({ 'error': errorMessage });
52
55
  });
53
56
  });
@@ -97,6 +100,9 @@ module.exports = function(RED) {
97
100
  if ( error.response && error.response.data && error.response.data.message ) {
98
101
  errorMessage = errorMessage + " / " + error.response.data.message;
99
102
  }
103
+ if ( commons.getNetworkErrorCustomString(error) !== undefined) {
104
+ errorMessage = commons.getNetworkErrorCustomString(error);
105
+ }
100
106
  res.json({ 'error': errorMessage });
101
107
  });
102
108
  });
@@ -18,16 +18,14 @@ module.exports = function(RED) {
18
18
  // Act on incomming messages
19
19
  node.on('input', function(msg) {
20
20
  if (node.confignode != undefined) {
21
- if (msg.payload.send === false) {
21
+ if (msg.payload !== undefined && msg.payload.send === false) {
22
22
  node.log(`Notification not send (msg.payload.send = false).`)
23
23
  return;
24
24
  }
25
25
  node.limiter.removeTokens(1, function(err, remainingRequests) {
26
26
  if (remainingRequests >= 0) {
27
- let title = commons.evaluateValue(RED, config.notificationTitle, config.notificationTitleType, node, msg);
28
- let body = commons.evaluateValue(RED, config.notificationBody, config.notificationBodyType, node, msg);
29
- if (title == undefined) title = '';
30
- if (body == undefined) body = '';
27
+ let title = commons.evaluateValue(RED, config.notificationTitle, config.notificationTitleType, node, msg, true);
28
+ let body = commons.evaluateValue(RED, config.notificationBody, config.notificationBodyType, node, msg, true);
31
29
  const notificationEmpty = (title == '' && body == '');
32
30
  if (!notificationEmpty) {
33
31
  // Title and/or body are filled
@@ -40,7 +38,7 @@ module.exports = function(RED) {
40
38
  // Sound configured or computed?
41
39
  let sound = config.notificationSound
42
40
  if (sound === 'computed') {
43
- sound = commons.evaluateValue(RED, config.notificationSoundComputed, config.notificationSoundComputedType, node, msg);
41
+ sound = commons.evaluateValue(RED, config.notificationSoundComputed, config.notificationSoundComputedType, node, msg, true);
44
42
  }
45
43
 
46
44
  // Call API to send notification
@@ -63,7 +61,11 @@ module.exports = function(RED) {
63
61
  }
64
62
  })
65
63
  .catch((error) => {
66
- node.error("ERROR: " + error);
64
+ // Log error
65
+ node.error('sendNotification: ' + commons.getNetworkErrorString(error))
66
+ if ( commons.getNetworkErrorCustomString(error) !== undefined) {
67
+ node.error(commons.getNetworkErrorCustomString(error));
68
+ }
67
69
 
68
70
  // Output status if configured so
69
71
  if ( config.output == 2 ) {
@@ -73,7 +75,7 @@ module.exports = function(RED) {
73
75
  });
74
76
  } else {
75
77
  // To big...
76
- node.error("The message exceeded 3600 bytes. Can´t send.");
78
+ node.error("xxxThe message exceeded 3600 bytes. Can´t send.");
77
79
 
78
80
  // Output status if configured so
79
81
  if ( config.output == 2 ) {
@@ -18,16 +18,14 @@ module.exports = function(RED) {
18
18
  // Act on incomming messages
19
19
  node.on('input', function(msg) {
20
20
  if (node.confignode != undefined) {
21
- if (msg.payload.send === false) {
21
+ if (msg.payload !== undefined && msg.payload.send === false) {
22
22
  node.log(`Question not send (msg.payload.send = false).`)
23
23
  return;
24
24
  }
25
25
  node.limiter.removeTokens(1, function(err, remainingRequests) {
26
26
  if (remainingRequests >= 0) {
27
- let title = commons.evaluateValue(RED, config.questionTitle, config.questionTitleType, node, msg);
28
- let body = commons.evaluateValue(RED, config.questionBody, config.questionBodyType, node, msg);
29
- if (title == undefined) title = '';
30
- if (body == undefined) body = '';
27
+ let title = commons.evaluateValue(RED, config.questionTitle, config.questionTitleType, node, msg, true);
28
+ let body = commons.evaluateValue(RED, config.questionBody, config.questionBodyType, node, msg, true);
31
29
  const notificationEmpty = (title == '' && body == '');
32
30
  if (!notificationEmpty) {
33
31
  // Title and/or body are filled, generate question data
@@ -37,19 +35,20 @@ module.exports = function(RED) {
37
35
  "questiontype" : 1,
38
36
  "answers": [
39
37
  {
40
- "text": commons.evaluateValue(RED, config.questionAnswerOne, config.questionAnswerOneType, node, msg),
41
- "value": commons.evaluateValue(RED, config.questionAnswerOneValue, config.questionAnswerOneValueType, node, msg)
38
+ "text": commons.evaluateValue(RED, config.questionAnswerOne, config.questionAnswerOneType, node, msg, true),
39
+ "value": commons.evaluateValue(RED, config.questionAnswerOneValue, config.questionAnswerOneValueType, node, msg, false)
42
40
  },
43
41
  {
44
- "text": commons.evaluateValue(RED, config.questionAnswerTwo, config.questionAnswerTwoType, node, msg),
45
- "value": commons.evaluateValue(RED, config.questionAnswerTwoValue, config.questionAnswerTwoValueType, node, msg)
42
+ "text": commons.evaluateValue(RED, config.questionAnswerTwo, config.questionAnswerTwoType, node, msg, true),
43
+ "value": commons.evaluateValue(RED, config.questionAnswerTwoValue, config.questionAnswerTwoValueType, node, msg, false)
46
44
  },
47
45
  {
48
- "text": commons.evaluateValue(RED, config.questionAnswerThree, config.questionAnswerThreeType, node, msg),
49
- "value": commons.evaluateValue(RED, config.questionAnswerThreeValue, config.questionAnswerThreeValueType, node, msg)
46
+ "text": commons.evaluateValue(RED, config.questionAnswerThree, config.questionAnswerThreeType, node, msg, true),
47
+ "value": commons.evaluateValue(RED, config.questionAnswerThreeValue, config.questionAnswerThreeValueType, node, msg, false)
50
48
  }
51
49
  ]
52
50
  };
51
+ node.log(`questionData: ${JSON.stringify(questionData)}`)
53
52
 
54
53
  // Content smaller than 3600 bytes? FCM max size (complete payload) is 4000 bytes.
55
54
  if (title.length + body.length + JSON.stringify(questionData).length <= 3600) {
@@ -59,7 +58,7 @@ module.exports = function(RED) {
59
58
  // Sound configured or computed?
60
59
  let sound = config.questionSound
61
60
  if (sound === 'computed') {
62
- sound = commons.evaluateValue(RED, config.questionSoundComputed, config.questionSoundComputedType, node, msg);
61
+ sound = commons.evaluateValue(RED, config.questionSoundComputed, config.questionSoundComputedType, node, msg, true);
63
62
  }
64
63
 
65
64
  // Call API to send notification
@@ -77,7 +76,11 @@ module.exports = function(RED) {
77
76
  node.debug(`Question send successfull`)
78
77
  })
79
78
  .catch((error) => {
80
- node.error("ERROR: " + error);
79
+ // Log error
80
+ node.error('sendNotification: ' + commons.getNetworkErrorString(error))
81
+ if ( commons.getNetworkErrorCustomString(error) !== undefined) {
82
+ node.error(commons.getNetworkErrorCustomString(error));
83
+ }
81
84
 
82
85
  // Output Error
83
86
  const msg = {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "node-red-contrib-remote",
3
- "version": "1.4.0",
4
- "description": "Remote services for Node-RED including remote access, push notifications and questions through push notifications.",
3
+ "version": "1.5.1",
4
+ "description": "Remote-RED is an ecosystem to bring remote access, push notification and geofencing to Node-RED. There are also additional functions like directly answer on a notification and homescreen widgets.",
5
5
  "author": "Thorsten Heilmann",
6
6
  "license": "GPL-3.0",
7
7
  "node-red": {