node-red-contrib-aedes 0.15.1 → 1.2.0
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/.github/workflows/nodejs.yml +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +11 -3
- package/aedes.html +44 -19
- package/aedes.js +412 -210
- package/locales/de/aedes.html +110 -0
- package/locales/de/aedes.json +54 -0
- package/locales/en-US/aedes.html +103 -12
- package/locales/en-US/aedes.json +15 -2
- package/package.json +6 -4
- package/test/aedes_last_will_spec.js +25 -48
- package/test/aedes_persist_spec.js +777 -0
- package/test/aedes_qos_spec.js +76 -77
- package/test/aedes_retain_spec.js +62 -189
- package/test/aedes_spec.js +257 -67
- package/test/aedes_ws_spec.js +107 -38
- package/test/test-utils.js +17 -0
- package/docs/DEV-SETUP.md +0 -86
- package/docs/MIGRATION-PLAN-0.51-TO-1.0.md +0 -349
- package/docs/MIGRATION-PLAN-NODE-RED-4.md +0 -107
package/aedes.js
CHANGED
|
@@ -17,17 +17,17 @@
|
|
|
17
17
|
module.exports = function (RED) {
|
|
18
18
|
'use strict';
|
|
19
19
|
const MongoPersistence = require('aedes-persistence-mongodb');
|
|
20
|
-
// const { Level } = require('level');
|
|
21
|
-
// const LevelPersistence = require('aedes-persistence-level');
|
|
22
|
-
const aedes = require('aedes');
|
|
23
20
|
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
24
22
|
const net = require('net');
|
|
25
23
|
const tls = require('tls');
|
|
26
24
|
const http = require('http');
|
|
27
25
|
const https = require('https');
|
|
28
|
-
const
|
|
26
|
+
const { WebSocketServer, createWebSocketStream } = require('ws');
|
|
29
27
|
|
|
30
28
|
let serverUpgradeAdded = false;
|
|
29
|
+
let wsPathNodeCount = 0;
|
|
30
|
+
let boundHandleServerUpgrade = null;
|
|
31
31
|
const listenerNodes = {};
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -40,222 +40,220 @@ module.exports = function (RED) {
|
|
|
40
40
|
function handleServerUpgrade (request, socket, head) {
|
|
41
41
|
const pathname = new URL(request.url, 'http://example.org').pathname;
|
|
42
42
|
if (Object.prototype.hasOwnProperty.call(listenerNodes, pathname)) {
|
|
43
|
-
listenerNodes[pathname].
|
|
43
|
+
listenerNodes[pathname]._wsPathServer.handleUpgrade(
|
|
44
44
|
request,
|
|
45
45
|
socket,
|
|
46
46
|
head,
|
|
47
47
|
function done (conn) {
|
|
48
|
-
listenerNodes[pathname].
|
|
48
|
+
listenerNodes[pathname]._wsPathServer.emit('connection', conn, request);
|
|
49
49
|
}
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
function checkWritable (dirPath, node) {
|
|
55
|
+
try {
|
|
56
|
+
fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
57
|
+
return true;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
node.warn('aedes: userDir is not writable (' + dirPath + ') – file persistence disabled: ' + err.message);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
function readSnapshotSync (filePath, node) {
|
|
65
|
+
if (!fs.existsSync(filePath)) {
|
|
66
|
+
node.debug('aedes: no snapshot found at ' + filePath);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
72
|
+
} catch (readErr) {
|
|
73
|
+
node.warn(
|
|
74
|
+
'aedes: could not read snapshot, starting with empty state: ' +
|
|
75
|
+
readErr.message
|
|
76
|
+
);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
65
79
|
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
let data;
|
|
81
|
+
try {
|
|
82
|
+
data = JSON.parse(raw);
|
|
83
|
+
} catch (parseErr) {
|
|
84
|
+
node.warn(
|
|
85
|
+
'aedes: snapshot file is corrupt, starting with empty state: ' +
|
|
86
|
+
parseErr.message
|
|
87
|
+
);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
68
90
|
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
92
|
+
node.warn(
|
|
93
|
+
'aedes: snapshot file has unexpected format, starting with empty state'
|
|
94
|
+
);
|
|
95
|
+
return null;
|
|
73
96
|
}
|
|
97
|
+
return data;
|
|
98
|
+
}
|
|
74
99
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
100
|
+
async function restoreRetained (broker, data, node) {
|
|
101
|
+
if (!data) {
|
|
102
|
+
node.debug('aedes: no snapshot data to restore');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
node.debug('aedes: restoring snapshot - retained messages: ' + Object.keys(data.retained || {}).length);
|
|
106
|
+
if (data.retained && typeof data.retained === 'object') {
|
|
107
|
+
const topics = Object.keys(data.retained);
|
|
81
108
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
for (let i = 0; i < topics.length; i++) {
|
|
110
|
+
const packet = data.retained[topics[i]];
|
|
111
|
+
if (!packet.topic) continue;
|
|
112
|
+
await broker.persistence.storeRetained({
|
|
113
|
+
topic: packet.topic,
|
|
114
|
+
payload: Buffer.from(packet.payload || '', 'base64'),
|
|
115
|
+
qos: packet.qos || 0,
|
|
116
|
+
retain: true,
|
|
117
|
+
cmd: 'publish'
|
|
118
|
+
});
|
|
90
119
|
}
|
|
91
120
|
} catch (err) {
|
|
92
|
-
|
|
93
|
-
this.error(err.toString());
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
if (this.credentials) {
|
|
98
|
-
this.cert = this.credentials.certdata || '';
|
|
99
|
-
this.key = this.credentials.keydata || '';
|
|
100
|
-
this.ca = this.credentials.cadata || '';
|
|
121
|
+
node.warn('aedes: failed to restore retained messages from snapshot: ' + err.message);
|
|
101
122
|
}
|
|
123
|
+
node.debug('aedes: snapshot restore complete');
|
|
102
124
|
}
|
|
103
|
-
|
|
104
|
-
this.username = this.credentials.username;
|
|
105
|
-
this.password = this.credentials.password;
|
|
106
|
-
}
|
|
125
|
+
}
|
|
107
126
|
|
|
108
|
-
|
|
109
|
-
|
|
127
|
+
async function saveSnapshot (broker, filePath, node) {
|
|
128
|
+
try {
|
|
129
|
+
node.debug('aedes: saving snapshot to ' + filePath);
|
|
130
|
+
// 1. Collect retained messages via public stream API
|
|
131
|
+
const retained = {};
|
|
132
|
+
const stream = broker.persistence.createRetainedStreamCombi(['#']);
|
|
133
|
+
node.debug('aedes: snapshot - collecting retained messages');
|
|
134
|
+
node.debug('aedes: snapshot - stream is readable: ' + stream.readable);
|
|
135
|
+
for await (const packet of stream) {
|
|
136
|
+
node.debug('aedes: snapshot - processing retained message: ' + packet.topic);
|
|
137
|
+
if (!packet.payload || packet.payload.length === 0) continue;
|
|
138
|
+
retained[packet.topic] = {
|
|
139
|
+
topic: packet.topic,
|
|
140
|
+
payload: Buffer.from(packet.payload).toString('base64'),
|
|
141
|
+
qos: packet.qos,
|
|
142
|
+
retain: true,
|
|
143
|
+
cmd: 'publish'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 2. Atomic write: temp file + rename
|
|
148
|
+
const tmpFile = filePath + '.tmp';
|
|
149
|
+
await fs.promises.writeFile(
|
|
150
|
+
tmpFile,
|
|
151
|
+
JSON.stringify({ retained }, null, 2),
|
|
152
|
+
'utf8'
|
|
153
|
+
);
|
|
154
|
+
await fs.promises.rename(tmpFile, filePath);
|
|
155
|
+
node.debug('aedes: snapshot saved to ' + filePath);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
node.warn('aedes: could not save snapshot: ' + err.message);
|
|
110
158
|
}
|
|
159
|
+
}
|
|
111
160
|
|
|
112
|
-
|
|
161
|
+
async function createBroker (node, config, aedesSettings, serverOptions) {
|
|
162
|
+
const { Aedes } = await import('aedes');
|
|
163
|
+
const broker = await Aedes.createBroker(aedesSettings);
|
|
164
|
+
if (node._closing) { broker.close(); return; }
|
|
165
|
+
node._broker = broker;
|
|
113
166
|
|
|
114
|
-
|
|
115
|
-
|
|
167
|
+
if (config.persistence_bind !== 'mongodb' && config.persist_to_file === true) {
|
|
168
|
+
const persistFile = path.join(RED.settings.userDir, 'aedes-persist-' + node.id + '.json');
|
|
169
|
+
node._persistFile = persistFile;
|
|
116
170
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
url: config.dburl
|
|
120
|
-
});
|
|
121
|
-
node.log('Start persistence to MongeDB');
|
|
122
|
-
/*
|
|
123
|
-
} else if (config.persistence_bind === 'level') {
|
|
124
|
-
aedesSettings.persistence = LevelPersistence(new Level('leveldb'));
|
|
125
|
-
node.log('Start persistence to LevelDB');
|
|
126
|
-
*/
|
|
127
|
-
}
|
|
171
|
+
if (checkWritable(RED.settings.userDir, node)) {
|
|
172
|
+
node._persistEnabled = true;
|
|
128
173
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
174
|
+
// Restore retained messages from snapshot data (already read synchronously in constructor)
|
|
175
|
+
if (node._snapshotData) {
|
|
176
|
+
await restoreRetained(broker, node._snapshotData, node);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Periodic save every 60 seconds (with guard against concurrent saves)
|
|
180
|
+
node._saving = false;
|
|
181
|
+
node._snapshotInterval = setInterval(function () {
|
|
182
|
+
if (node._saving) return;
|
|
183
|
+
node._saving = true;
|
|
184
|
+
saveSnapshot(node._broker, persistFile, node)
|
|
185
|
+
.finally(function () { node._saving = false; });
|
|
186
|
+
}, 60000);
|
|
187
|
+
}
|
|
133
188
|
}
|
|
134
189
|
|
|
135
|
-
const broker = aedes.createBroker(aedesSettings);
|
|
136
190
|
let server;
|
|
137
|
-
if (
|
|
191
|
+
if (node.usetls) {
|
|
138
192
|
server = tls.createServer(serverOptions, broker.handle);
|
|
139
193
|
} else {
|
|
140
194
|
server = net.createServer(broker.handle);
|
|
141
195
|
}
|
|
196
|
+
node._netServer = server;
|
|
142
197
|
|
|
143
|
-
|
|
144
|
-
let httpServer = null;
|
|
145
|
-
|
|
146
|
-
if (this.mqtt_ws_port) {
|
|
147
|
-
// Awkward check since http or ws do not fire an error event in case the port is in use
|
|
148
|
-
const testServer = net.createServer();
|
|
149
|
-
testServer.once('error', function (err) {
|
|
150
|
-
if (err.code === 'EADDRINUSE') {
|
|
151
|
-
node.error(
|
|
152
|
-
'Error: Port ' + config.mqtt_ws_port + ' is already in use'
|
|
153
|
-
);
|
|
154
|
-
} else {
|
|
155
|
-
node.error(
|
|
156
|
-
'Error creating net server on port ' +
|
|
157
|
-
config.mqtt_ws_port +
|
|
158
|
-
', ' +
|
|
159
|
-
err.toString()
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
testServer.once('listening', function () {
|
|
164
|
-
testServer.close();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
testServer.once('close', function () {
|
|
168
|
-
if (node.usetls) {
|
|
169
|
-
httpServer = https.createServer(serverOptions);
|
|
170
|
-
} else {
|
|
171
|
-
httpServer = http.createServer();
|
|
172
|
-
}
|
|
173
|
-
wss = ws.createServer(
|
|
174
|
-
{
|
|
175
|
-
server: httpServer
|
|
176
|
-
},
|
|
177
|
-
broker.handle
|
|
178
|
-
);
|
|
179
|
-
httpServer.listen(config.mqtt_ws_port, function () {
|
|
180
|
-
node.log(
|
|
181
|
-
'Binding aedes mqtt server on ws port: ' + config.mqtt_ws_port
|
|
182
|
-
);
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
testServer.listen(config.mqtt_ws_port, function () {
|
|
186
|
-
node.log('Checking ws port: ' + config.mqtt_ws_port);
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (this.mqtt_ws_path !== '') {
|
|
198
|
+
if (node.mqtt_ws_path !== '') {
|
|
191
199
|
if (!serverUpgradeAdded) {
|
|
192
|
-
|
|
200
|
+
boundHandleServerUpgrade = handleServerUpgrade;
|
|
201
|
+
RED.server.on('upgrade', boundHandleServerUpgrade);
|
|
193
202
|
serverUpgradeAdded = true;
|
|
194
203
|
}
|
|
204
|
+
wsPathNodeCount++;
|
|
195
205
|
|
|
196
|
-
let
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
(
|
|
206
|
+
let pathStr = RED.settings.httpNodeRoot || '/';
|
|
207
|
+
pathStr =
|
|
208
|
+
pathStr +
|
|
209
|
+
(pathStr.slice(-1) === '/' ? '' : '/') +
|
|
200
210
|
(node.mqtt_ws_path.charAt(0) === '/'
|
|
201
211
|
? node.mqtt_ws_path.substring(1)
|
|
202
212
|
: node.mqtt_ws_path);
|
|
203
|
-
node.fullPath =
|
|
213
|
+
node.fullPath = pathStr;
|
|
204
214
|
|
|
205
|
-
if (Object.prototype.hasOwnProperty.call(listenerNodes,
|
|
215
|
+
if (Object.prototype.hasOwnProperty.call(listenerNodes, pathStr)) {
|
|
206
216
|
node.error(
|
|
207
217
|
RED._('websocket.errors.duplicate-path', { path: node.mqtt_ws_path })
|
|
208
218
|
);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const serverOptions_ = {
|
|
213
|
-
noServer: true
|
|
214
|
-
};
|
|
215
|
-
if (RED.settings.webSocketNodeVerifyClient) {
|
|
216
|
-
serverOptions_.verifyClient = RED.settings.webSocketNodeVerifyClient;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
node.server = ws.createServer(
|
|
220
|
-
{
|
|
219
|
+
} else {
|
|
220
|
+
listenerNodes[node.fullPath] = node;
|
|
221
|
+
const serverOptions_ = {
|
|
221
222
|
noServer: true
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
};
|
|
224
|
+
if (RED.settings.webSocketNodeVerifyClient) {
|
|
225
|
+
serverOptions_.verifyClient = RED.settings.webSocketNodeVerifyClient;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
node._wsPathServer = new WebSocketServer(serverOptions_);
|
|
229
|
+
node._wsPathServer.on('connection', function (websocket, req) {
|
|
230
|
+
const stream = createWebSocketStream(websocket);
|
|
231
|
+
broker.handle(stream, req);
|
|
232
|
+
});
|
|
225
233
|
|
|
226
|
-
|
|
234
|
+
node.log('Binding aedes mqtt server on ws path: ' + node.fullPath);
|
|
235
|
+
}
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
server.once('error', function (err) {
|
|
230
239
|
if (err.code === 'EADDRINUSE') {
|
|
231
|
-
node.error(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
shape: 'ring',
|
|
235
|
-
text: 'node-red:common.status.disconnected'
|
|
236
|
-
});
|
|
240
|
+
node.error(
|
|
241
|
+
RED._('aedes-mqtt-broker.error.port-in-use', { port: node.mqtt_port })
|
|
242
|
+
);
|
|
237
243
|
} else {
|
|
238
|
-
node.error(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
shape: 'ring',
|
|
242
|
-
text: 'node-red:common.status.disconnected'
|
|
243
|
-
});
|
|
244
|
+
node.error(
|
|
245
|
+
RED._('aedes-mqtt-broker.error.server-error', { port: node.mqtt_port, error: err.toString() })
|
|
246
|
+
);
|
|
244
247
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
node.log('Binding aedes mqtt server on port: ' + config.mqtt_port);
|
|
250
|
-
node.status({
|
|
251
|
-
fill: 'green',
|
|
252
|
-
shape: 'dot',
|
|
253
|
-
text: 'node-red:common.status.connected'
|
|
254
|
-
});
|
|
248
|
+
node.status({
|
|
249
|
+
fill: 'red',
|
|
250
|
+
shape: 'ring',
|
|
251
|
+
text: 'aedes-mqtt-broker.status.error'
|
|
255
252
|
});
|
|
256
|
-
}
|
|
253
|
+
});
|
|
257
254
|
|
|
258
|
-
|
|
255
|
+
// Set up authentication handler BEFORE starting the server
|
|
256
|
+
if (node.credentials && node.username && node.password) {
|
|
259
257
|
broker.authenticate = function (client, username, password, callback) {
|
|
260
258
|
const authorized =
|
|
261
259
|
username === node.username &&
|
|
@@ -285,6 +283,7 @@ module.exports = function (RED) {
|
|
|
285
283
|
client
|
|
286
284
|
}
|
|
287
285
|
};
|
|
286
|
+
node.send([msg, null]);
|
|
288
287
|
node.status({
|
|
289
288
|
fill: 'green',
|
|
290
289
|
shape: 'dot',
|
|
@@ -292,7 +291,6 @@ module.exports = function (RED) {
|
|
|
292
291
|
count: broker.connectedClients
|
|
293
292
|
})
|
|
294
293
|
});
|
|
295
|
-
node.send([msg, null]);
|
|
296
294
|
});
|
|
297
295
|
|
|
298
296
|
broker.on('clientDisconnect', function (client) {
|
|
@@ -383,7 +381,7 @@ module.exports = function (RED) {
|
|
|
383
381
|
}
|
|
384
382
|
});
|
|
385
383
|
|
|
386
|
-
if (
|
|
384
|
+
if (node.wires && node.wires[1] && node.wires[1].length > 0) {
|
|
387
385
|
node.log('Publish output wired. Enable broker publish event messages.');
|
|
388
386
|
broker.on('publish', function (packet, client) {
|
|
389
387
|
const msg = {
|
|
@@ -400,54 +398,258 @@ module.exports = function (RED) {
|
|
|
400
398
|
broker.on('closed', function () {
|
|
401
399
|
node.debug('Closed event');
|
|
402
400
|
});
|
|
401
|
+
}
|
|
403
402
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
403
|
+
async function startListening (node, config, serverOptions) {
|
|
404
|
+
if (node.mqtt_ws_port) {
|
|
405
|
+
// Awkward check since http or ws do not fire an error event in case the port is in use
|
|
406
|
+
const testServer = net.createServer();
|
|
407
|
+
testServer.once('error', function (err) {
|
|
408
|
+
if (err.code === 'EADDRINUSE') {
|
|
409
|
+
node.error(
|
|
410
|
+
RED._('aedes-mqtt-broker.error.port-in-use', { port: config.mqtt_ws_port })
|
|
411
|
+
);
|
|
412
|
+
} else {
|
|
413
|
+
node.error(
|
|
414
|
+
RED._('aedes-mqtt-broker.error.server-error', { port: config.mqtt_ws_port, error: err.toString() })
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
node.status({ fill: 'red', shape: 'ring', text: 'aedes-mqtt-broker.status.error' });
|
|
418
|
+
});
|
|
419
|
+
testServer.once('listening', function () {
|
|
420
|
+
testServer.close();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
testServer.once('close', function () {
|
|
424
|
+
let httpServer;
|
|
425
|
+
if (node.usetls) {
|
|
426
|
+
httpServer = https.createServer(serverOptions);
|
|
427
|
+
} else {
|
|
428
|
+
httpServer = http.createServer();
|
|
429
|
+
}
|
|
430
|
+
node._wsHttpServer = httpServer;
|
|
431
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
432
|
+
wss.on('connection', function (websocket, req) {
|
|
433
|
+
const stream = createWebSocketStream(websocket);
|
|
434
|
+
node._broker.handle(stream, req);
|
|
435
|
+
});
|
|
436
|
+
node._wsServer = wss;
|
|
437
|
+
httpServer.listen(config.mqtt_ws_port, function () {
|
|
438
|
+
node.log(
|
|
439
|
+
'Binding aedes mqtt server on ws port: ' + config.mqtt_ws_port
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
testServer.listen(config.mqtt_ws_port, function () {
|
|
444
|
+
node.log('Checking ws port: ' + config.mqtt_ws_port);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (node.mqtt_port) {
|
|
449
|
+
node._netServer.listen(node.mqtt_port, function () {
|
|
450
|
+
node.log('Binding aedes mqtt server on port: ' + node.mqtt_port);
|
|
451
|
+
node.status({
|
|
452
|
+
fill: 'green',
|
|
453
|
+
shape: 'dot',
|
|
454
|
+
text: 'node-red:common.status.connected'
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function shutdownBroker (node, done) {
|
|
461
|
+
try {
|
|
462
|
+
await node._initPromise;
|
|
463
|
+
// Stop periodic snapshot interval
|
|
464
|
+
if (node._snapshotInterval) {
|
|
465
|
+
clearInterval(node._snapshotInterval);
|
|
466
|
+
node._snapshotInterval = null;
|
|
409
467
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
468
|
+
|
|
469
|
+
// Save final snapshot on shutdown (wait if an interval save is in progress)
|
|
470
|
+
if (node._persistEnabled && node._broker) {
|
|
471
|
+
// Wait for any in-progress interval save to complete
|
|
472
|
+
const waitForSave = new Promise(function (resolve) {
|
|
473
|
+
const check = setInterval(function () {
|
|
474
|
+
if (!node._saving) {
|
|
475
|
+
clearInterval(check);
|
|
476
|
+
resolve();
|
|
477
|
+
}
|
|
478
|
+
}, 50);
|
|
479
|
+
});
|
|
480
|
+
await waitForSave;
|
|
481
|
+
await saveSnapshot(node._broker, node._persistFile, node);
|
|
482
|
+
}
|
|
483
|
+
closeBroker(node, done);
|
|
484
|
+
} catch (e) {
|
|
485
|
+
done();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function AedesBrokerNode (config) {
|
|
490
|
+
RED.nodes.createNode(this, config);
|
|
491
|
+
this.mqtt_port = parseInt(config.mqtt_port, 10);
|
|
492
|
+
this.mqtt_ws_port = parseInt(config.mqtt_ws_port, 10);
|
|
493
|
+
this.mqtt_ws_path = '' + config.mqtt_ws_path;
|
|
494
|
+
this.mqtt_ws_bind = config.mqtt_ws_bind;
|
|
495
|
+
this.usetls = config.usetls;
|
|
496
|
+
|
|
497
|
+
const certPath = config.cert ? config.cert.trim() : '';
|
|
498
|
+
const keyPath = config.key ? config.key.trim() : '';
|
|
499
|
+
const caPath = config.ca ? config.ca.trim() : '';
|
|
500
|
+
|
|
501
|
+
this.uselocalfiles = config.uselocalfiles;
|
|
502
|
+
this.dburl = config.dburl;
|
|
503
|
+
|
|
504
|
+
if (this.mqtt_ws_bind === 'path') {
|
|
505
|
+
this.mqtt_ws_port = 0;
|
|
506
|
+
} else {
|
|
507
|
+
this.mqtt_ws_path = '';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (certPath.length > 0 || keyPath.length > 0 || caPath.length > 0) {
|
|
511
|
+
if ((certPath.length > 0) !== (keyPath.length > 0)) {
|
|
512
|
+
this.valid = false;
|
|
513
|
+
this.error(RED._('tls.error.missing-file'));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
if (certPath) {
|
|
518
|
+
this.cert = fs.readFileSync(certPath);
|
|
426
519
|
}
|
|
520
|
+
if (keyPath) {
|
|
521
|
+
this.key = fs.readFileSync(keyPath);
|
|
522
|
+
}
|
|
523
|
+
if (caPath) {
|
|
524
|
+
this.ca = fs.readFileSync(caPath);
|
|
525
|
+
}
|
|
526
|
+
} catch (err) {
|
|
527
|
+
this.valid = false;
|
|
528
|
+
this.error(err.toString());
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
if (this.credentials) {
|
|
533
|
+
this.cert = this.credentials.certdata || '';
|
|
534
|
+
this.key = this.credentials.keydata || '';
|
|
535
|
+
this.ca = this.credentials.cadata || '';
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (this.credentials) {
|
|
539
|
+
this.username = this.credentials.username;
|
|
540
|
+
this.password = this.credentials.password;
|
|
541
|
+
}
|
|
427
542
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
543
|
+
if (typeof this.usetls === 'undefined') {
|
|
544
|
+
this.usetls = false;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const node = this;
|
|
548
|
+
|
|
549
|
+
const aedesSettings = {};
|
|
550
|
+
const serverOptions = {};
|
|
551
|
+
|
|
552
|
+
if (config.persistence_bind === 'mongodb' && config.dburl) {
|
|
553
|
+
aedesSettings.persistence = MongoPersistence({
|
|
554
|
+
url: config.dburl
|
|
555
|
+
});
|
|
556
|
+
node.log('Start persistence to MongoDB');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// File persistence (only for in-memory mode with persist_to_file enabled)
|
|
560
|
+
|
|
561
|
+
if (this.cert && this.key && this.usetls) {
|
|
562
|
+
serverOptions.cert = this.cert;
|
|
563
|
+
serverOptions.key = this.key;
|
|
564
|
+
serverOptions.ca = this.ca;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
node._closing = false;
|
|
568
|
+
node._broker = null;
|
|
569
|
+
node._netServer = null;
|
|
570
|
+
node._wsServer = null;
|
|
571
|
+
node._wsHttpServer = null;
|
|
572
|
+
node._persistEnabled = false;
|
|
573
|
+
node._snapshotInterval = null;
|
|
574
|
+
node._persistFile = null;
|
|
575
|
+
node._snapshotData = null;
|
|
576
|
+
node._trackedSubs = null;
|
|
577
|
+
node._saving = false;
|
|
578
|
+
|
|
579
|
+
// Read snapshot file synchronously before async initialization
|
|
580
|
+
if (config.persistence_bind !== 'mongodb' && config.persist_to_file === true) {
|
|
581
|
+
const persistFile = path.join(RED.settings.userDir, 'aedes-persist-' + node.id + '.json');
|
|
582
|
+
if (checkWritable(RED.settings.userDir, node)) {
|
|
583
|
+
node._snapshotData = readSnapshotSync(persistFile, node);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
node._initPromise = (async function () {
|
|
588
|
+
await createBroker(node, config, aedesSettings, serverOptions);
|
|
589
|
+
await startListening(node, config, serverOptions);
|
|
590
|
+
}());
|
|
591
|
+
node._initPromise.catch(function (err) {
|
|
592
|
+
node.error(RED._('aedes-mqtt-broker.error.init-failed', { error: err.toString() }));
|
|
593
|
+
node.status({ fill: 'red', shape: 'ring', text: 'aedes-mqtt-broker.status.init-failed' });
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
this.on('close', function (removed, done) {
|
|
597
|
+
node._closing = true;
|
|
598
|
+
node.debug(removed ? 'Node removed or disabled' : 'Node restarting');
|
|
599
|
+
shutdownBroker(node, done);
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function closeBroker (node, done) {
|
|
604
|
+
process.nextTick(function () {
|
|
605
|
+
function wsClose () {
|
|
606
|
+
if (node._wsServer) {
|
|
607
|
+
// Terminate all existing WebSocket connections so close() callback fires promptly
|
|
608
|
+
node.log('Unbinding aedes mqtt server from ws port: ' + node.mqtt_ws_port);
|
|
609
|
+
node._wsServer.clients.forEach(function (ws) {
|
|
610
|
+
ws.terminate();
|
|
611
|
+
});
|
|
612
|
+
node._wsServer.close(function () {
|
|
613
|
+
if (node._wsHttpServer) {
|
|
614
|
+
node._wsHttpServer.close(function () { done(); });
|
|
615
|
+
} else { done(); }
|
|
616
|
+
});
|
|
617
|
+
} else { done(); }
|
|
618
|
+
}
|
|
619
|
+
function serverClose () {
|
|
620
|
+
if (node._netServer) {
|
|
621
|
+
node.log('Unbinding aedes mqtt server from port: ' + node.mqtt_port);
|
|
622
|
+
node.status({
|
|
623
|
+
fill: 'red',
|
|
624
|
+
shape: 'ring',
|
|
625
|
+
text: 'node-red:common.status.disconnected'
|
|
626
|
+
});
|
|
627
|
+
node._netServer.close(function () {
|
|
628
|
+
if (node.mqtt_ws_path !== '' && node.fullPath) {
|
|
629
|
+
node.log('Unbinding aedes mqtt server from ws path: ' + node.fullPath);
|
|
630
|
+
delete listenerNodes[node.fullPath];
|
|
631
|
+
// Remove upgrade listener if this is the last WS-path node
|
|
632
|
+
wsPathNodeCount--;
|
|
633
|
+
if (wsPathNodeCount <= 0 && serverUpgradeAdded && boundHandleServerUpgrade) {
|
|
634
|
+
RED.server.removeListener('upgrade', boundHandleServerUpgrade);
|
|
635
|
+
serverUpgradeAdded = false;
|
|
636
|
+
boundHandleServerUpgrade = null;
|
|
637
|
+
wsPathNodeCount = 0;
|
|
445
638
|
}
|
|
446
|
-
|
|
639
|
+
if (node._wsPathServer) {
|
|
640
|
+
// Terminate all existing WebSocket connections so close() callback fires promptly
|
|
641
|
+
node._wsPathServer.clients.forEach(function (ws) {
|
|
642
|
+
ws.terminate();
|
|
643
|
+
});
|
|
644
|
+
node._wsPathServer.close(function () { wsClose(); });
|
|
645
|
+
} else { wsClose(); }
|
|
646
|
+
} else { wsClose(); }
|
|
447
647
|
});
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
|
|
648
|
+
} else { wsClose(); }
|
|
649
|
+
}
|
|
650
|
+
if (node._broker) {
|
|
651
|
+
node._broker.close(function () { serverClose(); });
|
|
652
|
+
} else { serverClose(); }
|
|
451
653
|
});
|
|
452
654
|
}
|
|
453
655
|
|