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 +14 -1
- package/nodes/locales/de/remote-access.json +3 -0
- package/nodes/locales/en-US/remote-access.json +3 -0
- package/nodes/remote-access.js +83 -10
- package/nodes/remote-commons.js +17 -2
- package/nodes/remote-notification.js +3 -3
- package/nodes/remote-question.js +9 -9
- package/package.json +2 -1
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
|
|
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.",
|
package/nodes/remote-access.js
CHANGED
|
@@ -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
|
-
|
|
26
|
+
sshprocess = child_process.spawn("ssh", sshparameters);
|
|
19
27
|
|
|
20
28
|
// Set serving.. if not working, the process will exit or close
|
|
21
|
-
node
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) {
|
package/nodes/remote-commons.js
CHANGED
|
@@ -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(
|
|
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
|
|
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 =
|
|
24
|
-
const body =
|
|
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 =
|
|
37
|
+
sound = commons.evaluateValue(RED, config.notificationSoundComputed, config.notificationSoundComputedType, node, msg);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// Call API to send notification
|
package/nodes/remote-question.js
CHANGED
|
@@ -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 =
|
|
24
|
-
const body =
|
|
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":
|
|
35
|
-
"value":
|
|
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":
|
|
39
|
-
"value":
|
|
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":
|
|
43
|
-
"value":
|
|
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 =
|
|
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.
|
|
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",
|