frugal-iot-server 2.0.5 → 2.0.6
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/frugal-iot-server.js +41 -3
- package/lib/enrol.js +7 -0
- package/lib/retained.js +58 -0
- package/package.json +3 -3
- package/public/service-worker.js +1 -1
package/frugal-iot-server.js
CHANGED
|
@@ -110,9 +110,9 @@ import { createAPIRouter, createAPIErrorHandler } from './lib/api-routes.js';
|
|
|
110
110
|
import { buildConfigFor, hasPermissions } from './lib/config-for-user.js';
|
|
111
111
|
import { ensureSecrets, addEnrolmentSecret, removeEnrolmentSecret } from './lib/secrets.js';
|
|
112
112
|
import { syncUser, syncUserById, syncLoggers, syncNode, dropNode, syncBridge } from './lib/dynsec-server.js';
|
|
113
|
-
import { enrol, ENROL, enrolmentSecretsFor, makeRateLimiter, forgetNode,
|
|
113
|
+
import { enrol, ENROL, enrolmentSecretsFor, makeRateLimiter, forgetNode, formerProjects,
|
|
114
114
|
makeAttemptLog, setGrant, clearGrant, readGrants } from './lib/enrol.js';
|
|
115
|
-
import { deleteRetained } from './lib/retained.js';
|
|
115
|
+
import { deleteRetained, clearRetainedUnder } from './lib/retained.js';
|
|
116
116
|
import { replicaFor, bridgeForToken, noteBridgePull, startReplica } from './lib/replica.js';
|
|
117
117
|
import { addMissingColumns } from './lib/schema-columns.js';
|
|
118
118
|
import { createLoggerClient } from './lib/logger-client.js';
|
|
@@ -780,6 +780,38 @@ function addLoggedNodesToConfig() {
|
|
|
780
780
|
});
|
|
781
781
|
});
|
|
782
782
|
}
|
|
783
|
+
/*
|
|
784
|
+
* A node has enrolled in a new project, so its old address is retired: enrolment, broker account,
|
|
785
|
+
* retained messages, and what the logger and /config.json remember of it.
|
|
786
|
+
*
|
|
787
|
+
* Done here, once it has arrived, rather than when the move is asked for. A sleeping node only gets
|
|
788
|
+
* the move when it next wakes, and it needs its old credential to receive it - removed earlier, the
|
|
789
|
+
* node would be refused, enrol again in the OLD project, and never hear of the move.
|
|
790
|
+
*/
|
|
791
|
+
function retireFormerHome(org, project, nodeid, cb) {
|
|
792
|
+
waterfall([
|
|
793
|
+
(cb1) => forgetNode(db, org, project, nodeid, (err) => cb1(err)),
|
|
794
|
+
// Best effort, as for /node_reset - frugal-iot-diagnostic reports anything left behind.
|
|
795
|
+
(cb1) => dropNode(config, org, project, nodeid, () => cb1(null)),
|
|
796
|
+
(cb1) => clearRetainedUnder(config, org, `${org}/${project}/${nodeid}`, (err, result) => {
|
|
797
|
+
if (err) { console.error("Retained messages for", `${org}/${project}/${nodeid}`, "not cleared:", err.message); }
|
|
798
|
+
cb1(null, result);
|
|
799
|
+
}),
|
|
800
|
+
(result, cb1) => {
|
|
801
|
+
// After the retained messages, or the logger would hear them again and remember the node.
|
|
802
|
+
mqttLogger.forgetNode(org, project, nodeid);
|
|
803
|
+
const nodes = config.organizations?.[org]?.projects?.[project]?.nodes;
|
|
804
|
+
if (nodes) { delete nodes[nodeid]; }
|
|
805
|
+
console.log("Node", nodeid, "moved out of", `${org}/${project}`, "-",
|
|
806
|
+
result ? `${result.deleted} retained topic(s) cleared` : "retained topics left");
|
|
807
|
+
cb1(null);
|
|
808
|
+
},
|
|
809
|
+
], (err) => {
|
|
810
|
+
if (err) { console.error("Could not retire", `${org}/${project}/${nodeid}:`, err.message); }
|
|
811
|
+
cb(null);
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
783
815
|
// See lib/config-for-user.js - kept there so it can be tested without starting a server.
|
|
784
816
|
function unsafeCopyConfigFor(user) {
|
|
785
817
|
return buildConfigFor(config, user);
|
|
@@ -906,7 +938,13 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
|
|
|
906
938
|
req.body, (err, cred) => {
|
|
907
939
|
if (!err) {
|
|
908
940
|
console.log("Enrolled", cred.username, req.body.lora ? "(LoRa-capable)" : "");
|
|
909
|
-
|
|
941
|
+
res.status(200).json(cred);
|
|
942
|
+
const { org, project, nodeid } = req.body;
|
|
943
|
+
formerProjects(db, org, project, nodeid, (ferr, olds) => {
|
|
944
|
+
if (ferr) { return console.error("Could not look for", nodeid, "elsewhere:", ferr.message); }
|
|
945
|
+
each(olds, (old, cb) => retireFormerHome(org, old, nodeid, cb), () => {});
|
|
946
|
+
});
|
|
947
|
+
return;
|
|
910
948
|
}
|
|
911
949
|
const status = {
|
|
912
950
|
[ENROL.BAD_REQUEST]: 400,
|
package/lib/enrol.js
CHANGED
|
@@ -389,3 +389,10 @@ export function forgetNode(db, org, project, nodeid, cb) {
|
|
|
389
389
|
db.run('DELETE FROM nodes WHERE org = ? AND project = ? AND nodeid = ?',
|
|
390
390
|
[org, project, nodeid], function (err) { cb(err, this && this.changes); });
|
|
391
391
|
}
|
|
392
|
+
|
|
393
|
+
// Where else this node is enrolled. A node id is one device, so any other project is somewhere it
|
|
394
|
+
// used to be - see retireFormerHome in frugal-iot-server.js.
|
|
395
|
+
export function formerProjects(db, org, project, nodeid, cb) {
|
|
396
|
+
db.all('SELECT project FROM nodes WHERE org = ? AND nodeid = ? AND project != ?',
|
|
397
|
+
[org, nodeid, project], (err, rows) => cb(err, (rows || []).map((r) => r.project)));
|
|
398
|
+
}
|
package/lib/retained.js
CHANGED
|
@@ -108,3 +108,61 @@ function refusal(status, message) {
|
|
|
108
108
|
}
|
|
109
109
|
function err500(message) { return refusal(500, message); }
|
|
110
110
|
function refuse(cb, status, message) { return cb(refusal(status, message)); }
|
|
111
|
+
|
|
112
|
+
// Reading needs a different account from deleting: the admin account may publish anywhere in the
|
|
113
|
+
// organization but subscribe nowhere. The logger's reads the whole organization.
|
|
114
|
+
function readCredentialFor(config, org) {
|
|
115
|
+
const o = (config && config.organizations && config.organizations[org]) || {};
|
|
116
|
+
if (o.logger_userid) return { username: o.logger_userid, password: o.logger_password };
|
|
117
|
+
if (o.mqtt_password) return { username: org, password: o.mqtt_password };
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Retained topics arrive straight after the subscription is granted; this is how long to wait for
|
|
122
|
+
// the last of them, as frugal-iot-clearretained does.
|
|
123
|
+
const COLLECT_MS = 3000;
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
* Delete every retained message under `prefix` - one node's whole subtree, when it has moved.
|
|
127
|
+
*
|
|
128
|
+
* Takes a prefix, not a pattern, so it cannot be widened to more than the caller named: the
|
|
129
|
+
* wildcard is added here.
|
|
130
|
+
*/
|
|
131
|
+
export function clearRetainedUnder(config, org, prefix, cb) {
|
|
132
|
+
if (!NAME.test(String(org || ''))) return refuse(cb, 400, 'Not an organization name');
|
|
133
|
+
if (!String(prefix || '').startsWith(`${org}/`) || /[+#]/.test(prefix) || prefix.split('/').length < 3) {
|
|
134
|
+
return refuse(cb, 400, `${prefix} is not a node within ${org}`);
|
|
135
|
+
}
|
|
136
|
+
const broker = config && config.mqtt && config.mqtt.broker;
|
|
137
|
+
if (!broker) return refuse(cb, 500, 'No mqtt.broker in the configuration');
|
|
138
|
+
const cred = readCredentialFor(config, org);
|
|
139
|
+
if (!cred) return refuse(cb, 500, `No broker credential to read ${org}`);
|
|
140
|
+
|
|
141
|
+
const client = mqtt.connect(broker, {
|
|
142
|
+
username: cred.username, password: cred.password,
|
|
143
|
+
connectTimeout: 5000, reconnectPeriod: 0, clean: true,
|
|
144
|
+
});
|
|
145
|
+
const topics = new Set();
|
|
146
|
+
let settled = false;
|
|
147
|
+
const finish = (err) => {
|
|
148
|
+
if (settled) return;
|
|
149
|
+
settled = true;
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
client.end(true, () => {
|
|
152
|
+
if (err) return cb(err);
|
|
153
|
+
if (!topics.size) return cb(null, { deleted: 0, failed: 0 });
|
|
154
|
+
deleteRetained(config, org, [...topics], cb);
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
const timer = setTimeout(() => finish(err500('The broker did not answer')), 30000);
|
|
158
|
+
client.on('error', (err) => finish(refusal(502, `Could not reach the broker: ${err.message}`)));
|
|
159
|
+
client.on('message', (topic, message, packet) => {
|
|
160
|
+
if (packet && packet.retain && message.length) topics.add(topic);
|
|
161
|
+
});
|
|
162
|
+
client.on('connect', () => {
|
|
163
|
+
client.subscribe(`${prefix}/#`, { qos: 0 }, (err) => {
|
|
164
|
+
if (err) return finish(refusal(502, `Could not subscribe to ${prefix}/#: ${err.message}`));
|
|
165
|
+
setTimeout(() => finish(null), COLLECT_MS);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
package/package.json
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"cookie-parser": "^1.4.7",
|
|
13
13
|
"express": "^5.2.1",
|
|
14
14
|
"express-session": "^1.19.0",
|
|
15
|
-
"frugal-iot-client": "^2.0.
|
|
16
|
-
"frugal-iot-logger": "^2.0.
|
|
15
|
+
"frugal-iot-client": "^2.0.6",
|
|
16
|
+
"frugal-iot-logger": "^2.0.6",
|
|
17
17
|
"hash-wasm": "^4.12.0",
|
|
18
18
|
"js-yaml": "^4.2.0",
|
|
19
19
|
"morgan": "^1.11.0",
|
|
@@ -89,5 +89,5 @@
|
|
|
89
89
|
"check-files": "node scripts/check-published-files.js"
|
|
90
90
|
},
|
|
91
91
|
"type": "module",
|
|
92
|
-
"version": "2.0.
|
|
92
|
+
"version": "2.0.6"
|
|
93
93
|
}
|
package/public/service-worker.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// it, an installed browser keeps serving the files it already cached - and since the list now covers
|
|
7
7
|
// the dashboard's own code, that means old JS behind freshly fetched HTML. Any edit to the list
|
|
8
8
|
// needs this bumped, or it does not reach anyone who already has the old one.
|
|
9
|
-
const CACHE_NAME = 'frugal-iot-cache-2.0.
|
|
9
|
+
const CACHE_NAME = 'frugal-iot-cache-2.0.6';
|
|
10
10
|
/*
|
|
11
11
|
* Everything the app needs, so that a phone on a poor or expensive link fetches it once.
|
|
12
12
|
*
|