smart-home-engine 1.10.2 → 1.11.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/README.md +26 -10
- package/dist/web/assets/{index-BpeQz77V.js → index-D7HdR3Pe.js} +44 -42
- package/dist/web/assets/{index-B7BKqQNL.css → index-Dn-sgi3l.css} +1 -1
- package/dist/web/assets/{tsMode-BVHy5rED.js → tsMode-CVSpCHxf.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +3 -1
- package/src/lib/ca.js +64 -15
- package/src/lib/dynsec.js +10 -8
- package/src/lib/mosquitto-conf.js +47 -17
- package/src/lib/parse-payload.js +1 -1
- package/src/web/broker-api.js +29 -27
- package/src/web/mqtt-api.js +1 -1
- package/src/web/scripts-api.js +15 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-
|
|
1
|
+
import{m as O}from"./monaco-langs-Dt8a9eeo.js";import{t as I}from"./index-D7HdR3Pe.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|
package/dist/web/index.html
CHANGED
|
@@ -172,10 +172,10 @@
|
|
|
172
172
|
}
|
|
173
173
|
})();
|
|
174
174
|
</script>
|
|
175
|
-
<script type="module" crossorigin src="/assets/index-
|
|
175
|
+
<script type="module" crossorigin src="/assets/index-D7HdR3Pe.js"></script>
|
|
176
176
|
<link rel="modulepreload" crossorigin href="/assets/monaco-langs-Dt8a9eeo.js">
|
|
177
177
|
<link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
|
|
178
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
178
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Dn-sgi3l.css">
|
|
179
179
|
</head>
|
|
180
180
|
<body>
|
|
181
181
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-home-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Node.js based script runner for use in MQTT based Smart Home environments",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
"test:verbose": "cross-env NODE_OPTIONS=\"--no-warnings\" jest --verbose --forceExit",
|
|
12
12
|
"lint": "eslint --cache --cache-location .cache/.eslintcache .",
|
|
13
13
|
"lint:fix": "eslint --cache --cache-location .cache/.eslintcache --fix .",
|
|
14
|
+
"lint:ci": "eslint --no-cache .",
|
|
14
15
|
"format": "prettier --cache --cache-location .cache/.prettiercache --write .",
|
|
15
16
|
"format:check": "prettier --cache --cache-location .cache/.prettiercache --check .",
|
|
16
17
|
"build:web": "cd web && npm install && npm run build",
|
|
18
|
+
"prepare": "git config core.hooksPath .githooks || true",
|
|
17
19
|
"postinstall": "node -e \"console.log('\\n smart-home-engine installed.\\n To run as a systemd service: sudo she --install\\n')\""
|
|
18
20
|
},
|
|
19
21
|
"bin": {
|
package/src/lib/ca.js
CHANGED
|
@@ -141,8 +141,16 @@ async function importCA(config, { certPem, keyPem, chainPem = null }) {
|
|
|
141
141
|
await openssl(['x509', '-in', tmpCert, '-noout']);
|
|
142
142
|
await openssl(['pkey', '-in', tmpKey, '-noout']);
|
|
143
143
|
} catch (e) {
|
|
144
|
-
try {
|
|
145
|
-
|
|
144
|
+
try {
|
|
145
|
+
fs.unlinkSync(tmpCert);
|
|
146
|
+
} catch {
|
|
147
|
+
/* ignore */
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
fs.unlinkSync(tmpKey);
|
|
151
|
+
} catch {
|
|
152
|
+
/* ignore */
|
|
153
|
+
}
|
|
146
154
|
throw new Error(`Invalid certificate or key: ${e.message}`);
|
|
147
155
|
}
|
|
148
156
|
|
|
@@ -153,7 +161,11 @@ async function importCA(config, { certPem, keyPem, chainPem = null }) {
|
|
|
153
161
|
|
|
154
162
|
fs.renameSync(tmpCert, crtPath);
|
|
155
163
|
fs.renameSync(tmpKey, keyPath);
|
|
156
|
-
try {
|
|
164
|
+
try {
|
|
165
|
+
fs.chmodSync(keyPath, 0o600);
|
|
166
|
+
} catch {
|
|
167
|
+
/* ignore */
|
|
168
|
+
}
|
|
157
169
|
|
|
158
170
|
if (!fs.existsSync(srlPath)) {
|
|
159
171
|
fs.writeFileSync(srlPath, '01\n', 'utf8');
|
|
@@ -165,7 +177,11 @@ async function importCA(config, { certPem, keyPem, chainPem = null }) {
|
|
|
165
177
|
try {
|
|
166
178
|
await openssl(['x509', '-in', tmpChain, '-noout']);
|
|
167
179
|
} catch (e) {
|
|
168
|
-
try {
|
|
180
|
+
try {
|
|
181
|
+
fs.unlinkSync(tmpChain);
|
|
182
|
+
} catch {
|
|
183
|
+
/* ignore */
|
|
184
|
+
}
|
|
169
185
|
throw new Error(`Invalid chain certificate: ${e.message}`);
|
|
170
186
|
}
|
|
171
187
|
fs.renameSync(tmpChain, chainPath);
|
|
@@ -195,7 +211,7 @@ async function extractFromP12(p12Buffer, passphrase) {
|
|
|
195
211
|
const passArg = `pass:${passphrase}`;
|
|
196
212
|
const tryExtract = async (extraFlags = []) => {
|
|
197
213
|
const { stdout: certOut } = await openssl(['pkcs12', '-in', tmpP12, '-clcerts', '-nokeys', '-passin', passArg, '-nodes', ...extraFlags]);
|
|
198
|
-
const { stdout: keyOut }
|
|
214
|
+
const { stdout: keyOut } = await openssl(['pkcs12', '-in', tmpP12, '-nocerts', '-nodes', '-passin', passArg, ...extraFlags]);
|
|
199
215
|
return { certOut, keyOut };
|
|
200
216
|
};
|
|
201
217
|
let certOut, keyOut;
|
|
@@ -205,12 +221,16 @@ async function extractFromP12(p12Buffer, passphrase) {
|
|
|
205
221
|
({ certOut, keyOut } = await tryExtract(['-legacy']));
|
|
206
222
|
}
|
|
207
223
|
const certMatch = certOut.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/);
|
|
208
|
-
const keyMatch
|
|
224
|
+
const keyMatch = keyOut.match(/-----BEGIN [\w ]+ KEY-----[\s\S]+?-----END [\w ]+ KEY-----/);
|
|
209
225
|
if (!certMatch) throw new Error('No certificate found in P12 file');
|
|
210
|
-
if (!keyMatch)
|
|
226
|
+
if (!keyMatch) throw new Error('No private key found in P12 file');
|
|
211
227
|
return { certPem: certMatch[0] + '\n', keyPem: keyMatch[0] + '\n' };
|
|
212
228
|
} finally {
|
|
213
|
-
try {
|
|
229
|
+
try {
|
|
230
|
+
fs.unlinkSync(tmpP12);
|
|
231
|
+
} catch {
|
|
232
|
+
/* ignore */
|
|
233
|
+
}
|
|
214
234
|
}
|
|
215
235
|
}
|
|
216
236
|
|
|
@@ -311,7 +331,11 @@ async function generateServerCSR(config, { cn, san = [] } = {}) {
|
|
|
311
331
|
|
|
312
332
|
// Generate key
|
|
313
333
|
await openssl(['genpkey', '-algorithm', 'ed25519', '-out', keyPath]);
|
|
314
|
-
try {
|
|
334
|
+
try {
|
|
335
|
+
fs.chmodSync(keyPath, 0o600);
|
|
336
|
+
} catch {
|
|
337
|
+
/* ignore */
|
|
338
|
+
}
|
|
315
339
|
|
|
316
340
|
// Build subject and optional SAN extension for the CSR
|
|
317
341
|
const sanEntries = [cn, ...san].filter(Boolean).map((s, i) => {
|
|
@@ -353,7 +377,11 @@ async function importServerCert(config, { certPem, keyPem = null } = {}) {
|
|
|
353
377
|
try {
|
|
354
378
|
await openssl(['x509', '-in', tmpCrt, '-noout']);
|
|
355
379
|
} catch (e) {
|
|
356
|
-
try {
|
|
380
|
+
try {
|
|
381
|
+
fs.unlinkSync(tmpCrt);
|
|
382
|
+
} catch {
|
|
383
|
+
/* ignore */
|
|
384
|
+
}
|
|
357
385
|
throw new Error(`Invalid certificate: ${e.message}`);
|
|
358
386
|
}
|
|
359
387
|
|
|
@@ -363,14 +391,30 @@ async function importServerCert(config, { certPem, keyPem = null } = {}) {
|
|
|
363
391
|
try {
|
|
364
392
|
await openssl(['pkey', '-in', tmpKey, '-noout']);
|
|
365
393
|
} catch (e) {
|
|
366
|
-
try {
|
|
367
|
-
|
|
394
|
+
try {
|
|
395
|
+
fs.unlinkSync(tmpCrt);
|
|
396
|
+
} catch {
|
|
397
|
+
/* ignore */
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
fs.unlinkSync(tmpKey);
|
|
401
|
+
} catch {
|
|
402
|
+
/* ignore */
|
|
403
|
+
}
|
|
368
404
|
throw new Error(`Invalid private key: ${e.message}`);
|
|
369
405
|
}
|
|
370
406
|
fs.renameSync(tmpKey, keyPath);
|
|
371
|
-
try {
|
|
407
|
+
try {
|
|
408
|
+
fs.chmodSync(keyPath, 0o600);
|
|
409
|
+
} catch {
|
|
410
|
+
/* ignore */
|
|
411
|
+
}
|
|
372
412
|
} else if (!fs.existsSync(keyPath)) {
|
|
373
|
-
try {
|
|
413
|
+
try {
|
|
414
|
+
fs.unlinkSync(tmpCrt);
|
|
415
|
+
} catch {
|
|
416
|
+
/* ignore */
|
|
417
|
+
}
|
|
374
418
|
throw new Error('No private key provided and no existing server.key on disk');
|
|
375
419
|
}
|
|
376
420
|
|
|
@@ -438,7 +482,12 @@ async function issueClientCert(config, { cn, days = 365 } = {}) {
|
|
|
438
482
|
p12CertfileArg = combinedChainTmp;
|
|
439
483
|
}
|
|
440
484
|
await openssl(['pkcs12', '-export', '-in', crtPath, '-inkey', keyPath, '-certfile', p12CertfileArg, '-out', p12Path, '-passout', `pass:${passphrase}`, '-legacy']);
|
|
441
|
-
if (combinedChainTmp)
|
|
485
|
+
if (combinedChainTmp)
|
|
486
|
+
try {
|
|
487
|
+
fs.unlinkSync(combinedChainTmp);
|
|
488
|
+
} catch {
|
|
489
|
+
/* ignore */
|
|
490
|
+
}
|
|
442
491
|
|
|
443
492
|
const fingerprint = await certFingerprint(crtPath);
|
|
444
493
|
const expires = await certExpiry(crtPath);
|
package/src/lib/dynsec.js
CHANGED
|
@@ -145,13 +145,15 @@ function init(config, log) {
|
|
|
145
145
|
_log.info('dynsec: ready — subscribed as', dynsecCfg.adminUsername);
|
|
146
146
|
_drain(); // flush any requests queued before connection
|
|
147
147
|
// Probe whether the plugin is actually loaded and responding
|
|
148
|
-
_request('getDefaultACLAccess')
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
_request('getDefaultACLAccess')
|
|
149
|
+
.then(() => {
|
|
150
|
+
_dynsecReady = true;
|
|
151
|
+
_log.info('dynsec: plugin confirmed active');
|
|
152
|
+
})
|
|
153
|
+
.catch((err) => {
|
|
154
|
+
_dynsecReady = false;
|
|
155
|
+
_log.warn('dynsec: plugin probe failed — is the dynamic-security plugin loaded in mosquitto.conf?', err.message);
|
|
156
|
+
});
|
|
155
157
|
}
|
|
156
158
|
});
|
|
157
159
|
// The admin role has subscribePattern + publishClientReceive for $SYS/#.
|
|
@@ -179,7 +181,7 @@ function init(config, log) {
|
|
|
179
181
|
const val = payload.toString();
|
|
180
182
|
const now = Date.now();
|
|
181
183
|
const prev = _sysData[topic];
|
|
182
|
-
_sysData[topic] = { val, ts: now, lc: prev && prev.val !== val ? now :
|
|
184
|
+
_sysData[topic] = { val, ts: now, lc: prev && prev.val !== val ? now : prev ? prev.lc : now };
|
|
183
185
|
return;
|
|
184
186
|
}
|
|
185
187
|
if (topic !== RESPONSE_TOPIC) return;
|
|
@@ -30,17 +30,32 @@ const execFileAsync = promisify(execFile);
|
|
|
30
30
|
// when read from an old conf it is normalised to 'plugin_opt_config_file' on
|
|
31
31
|
// the same line that recognises it (see parseText below).
|
|
32
32
|
const MANAGED_SINGLE_KEYS = new Set([
|
|
33
|
-
'per_listener_settings',
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
33
|
+
'per_listener_settings',
|
|
34
|
+
'allow_anonymous',
|
|
35
|
+
'persistence',
|
|
36
|
+
'persistence_location',
|
|
37
|
+
'log_dest',
|
|
38
|
+
'log_type',
|
|
39
|
+
'plugin',
|
|
40
|
+
'plugin_opt_config_file',
|
|
41
|
+
'plugin_opt_dynsec_config_file',
|
|
37
42
|
// Connection limits
|
|
38
|
-
'max_connections',
|
|
39
|
-
'
|
|
43
|
+
'max_connections',
|
|
44
|
+
'max_inflight_messages',
|
|
45
|
+
'max_queued_messages',
|
|
46
|
+
'max_packet_size',
|
|
47
|
+
'message_size_limit',
|
|
48
|
+
'max_keepalive',
|
|
40
49
|
// Sessions
|
|
41
|
-
'persistent_client_expiration',
|
|
50
|
+
'persistent_client_expiration',
|
|
51
|
+
'retain_available',
|
|
42
52
|
// Performance / misc
|
|
43
|
-
'set_tcp_nodelay',
|
|
53
|
+
'set_tcp_nodelay',
|
|
54
|
+
'connection_messages',
|
|
55
|
+
// $SYS reporting
|
|
56
|
+
'sys_interval',
|
|
57
|
+
// Include dirs
|
|
58
|
+
'include_dir',
|
|
44
59
|
]);
|
|
45
60
|
|
|
46
61
|
/**
|
|
@@ -205,14 +220,26 @@ function serialise(conf) {
|
|
|
205
220
|
const { managed = {}, listeners = [], passthrough = [] } = conf;
|
|
206
221
|
|
|
207
222
|
const keyOrder = [
|
|
208
|
-
'per_listener_settings',
|
|
209
|
-
'
|
|
210
|
-
'
|
|
211
|
-
'
|
|
212
|
-
'
|
|
213
|
-
'
|
|
214
|
-
'
|
|
215
|
-
'
|
|
223
|
+
'per_listener_settings',
|
|
224
|
+
'allow_anonymous',
|
|
225
|
+
'persistence',
|
|
226
|
+
'persistence_location',
|
|
227
|
+
'max_connections',
|
|
228
|
+
'max_inflight_messages',
|
|
229
|
+
'max_queued_messages',
|
|
230
|
+
'max_packet_size',
|
|
231
|
+
'message_size_limit',
|
|
232
|
+
'max_keepalive',
|
|
233
|
+
'persistent_client_expiration',
|
|
234
|
+
'retain_available',
|
|
235
|
+
'set_tcp_nodelay',
|
|
236
|
+
'connection_messages',
|
|
237
|
+
'sys_interval',
|
|
238
|
+
'log_dest',
|
|
239
|
+
'log_type',
|
|
240
|
+
'plugin',
|
|
241
|
+
'plugin_opt_config_file',
|
|
242
|
+
'include_dir',
|
|
216
243
|
];
|
|
217
244
|
for (const key of keyOrder) {
|
|
218
245
|
if (managed[key] === undefined) continue;
|
|
@@ -220,7 +247,10 @@ function serialise(conf) {
|
|
|
220
247
|
if (Array.isArray(val)) {
|
|
221
248
|
const written = new Set();
|
|
222
249
|
for (const v of val) {
|
|
223
|
-
if (!written.has(v)) {
|
|
250
|
+
if (!written.has(v)) {
|
|
251
|
+
written.add(v);
|
|
252
|
+
lines.push(`${key} ${v}`);
|
|
253
|
+
}
|
|
224
254
|
}
|
|
225
255
|
} else {
|
|
226
256
|
lines.push(`${key} ${val}`);
|
package/src/lib/parse-payload.js
CHANGED
package/src/web/broker-api.js
CHANGED
|
@@ -94,10 +94,14 @@ router.get('/status', (req, res) => {
|
|
|
94
94
|
// received by the dynsec client (e.g. when dynsec is not configured).
|
|
95
95
|
if (_store) {
|
|
96
96
|
const sysPrefixes = [
|
|
97
|
-
'$SYS/broker/version',
|
|
98
|
-
'$SYS/broker/
|
|
99
|
-
'$SYS/broker/
|
|
100
|
-
'$SYS/broker/
|
|
97
|
+
'$SYS/broker/version',
|
|
98
|
+
'$SYS/broker/uptime',
|
|
99
|
+
'$SYS/broker/clients/',
|
|
100
|
+
'$SYS/broker/messages/',
|
|
101
|
+
'$SYS/broker/subscriptions/',
|
|
102
|
+
'$SYS/broker/retained messages/',
|
|
103
|
+
'$SYS/broker/bytes/',
|
|
104
|
+
'$SYS/broker/heap/',
|
|
101
105
|
];
|
|
102
106
|
for (const [topic, entry] of _store.mqttEntries()) {
|
|
103
107
|
if (!sys[topic] && sysPrefixes.some((p) => topic.startsWith(p))) {
|
|
@@ -963,16 +967,12 @@ router.get('/acl-check', async (req, res) => {
|
|
|
963
967
|
|
|
964
968
|
const mqttWildcard = require('../lib/mqtt-wildcards');
|
|
965
969
|
|
|
966
|
-
const [roles, groups, defaultAcls] = await Promise.all([
|
|
967
|
-
dynsec.listRoles(true),
|
|
968
|
-
dynsec.listGroups(true),
|
|
969
|
-
dynsec.getDefaultACLAccess(),
|
|
970
|
-
]);
|
|
970
|
+
const [roles, groups, defaultAcls] = await Promise.all([dynsec.listRoles(true), dynsec.listGroups(true), dynsec.getDefaultACLAccess()]);
|
|
971
971
|
|
|
972
972
|
// Build index: rolename → groups that carry it (with their member lists)
|
|
973
973
|
const groupsByRole = new Map();
|
|
974
974
|
for (const g of groups) {
|
|
975
|
-
for (const r of
|
|
975
|
+
for (const r of g.roles ?? []) {
|
|
976
976
|
if (!groupsByRole.has(r.rolename)) groupsByRole.set(r.rolename, []);
|
|
977
977
|
groupsByRole.get(r.rolename).push({
|
|
978
978
|
groupname: g.groupname,
|
|
@@ -981,9 +981,9 @@ router.get('/acl-check', async (req, res) => {
|
|
|
981
981
|
}
|
|
982
982
|
}
|
|
983
983
|
|
|
984
|
-
const SEND_TYPES
|
|
984
|
+
const SEND_TYPES = new Set(['publishClientSend']);
|
|
985
985
|
const SUBSCRIBE_TYPES = new Set(['subscribePattern', 'subscribeLiteral']);
|
|
986
|
-
const RECEIVE_TYPES
|
|
986
|
+
const RECEIVE_TYPES = new Set(['publishClientReceive']);
|
|
987
987
|
|
|
988
988
|
function aclMatches(acltype, aclTopic) {
|
|
989
989
|
if (aclTopic.includes('%u') || aclTopic.includes('%c')) return 'dynamic';
|
|
@@ -996,7 +996,7 @@ router.get('/acl-check', async (req, res) => {
|
|
|
996
996
|
function matchRoles(typeSet) {
|
|
997
997
|
const result = [];
|
|
998
998
|
for (const role of roles) {
|
|
999
|
-
for (const acl of
|
|
999
|
+
for (const acl of role.acls ?? []) {
|
|
1000
1000
|
if (!typeSet.has(acl.acltype)) continue;
|
|
1001
1001
|
const m = aclMatches(acl.acltype, acl.topic);
|
|
1002
1002
|
if (m !== 'no-match') {
|
|
@@ -1021,9 +1021,9 @@ router.get('/acl-check', async (req, res) => {
|
|
|
1021
1021
|
|
|
1022
1022
|
res.json({
|
|
1023
1023
|
topic,
|
|
1024
|
-
send:
|
|
1024
|
+
send: { roles: matchRoles(SEND_TYPES), default: defaultAllow('publishClientSend') },
|
|
1025
1025
|
subscribe: { roles: matchRoles(SUBSCRIBE_TYPES), default: defaultAllow('subscribePattern') },
|
|
1026
|
-
receive:
|
|
1026
|
+
receive: { roles: matchRoles(RECEIVE_TYPES), default: defaultAllow('publishClientReceive') },
|
|
1027
1027
|
});
|
|
1028
1028
|
} catch (err) {
|
|
1029
1029
|
handleError(res, err);
|
|
@@ -1068,9 +1068,7 @@ router.get('/ip-addresses', async (req, res) => {
|
|
|
1068
1068
|
const result = await sshDeploy.runCommand(bc.ssh, 'ip a 2>/dev/null || ip addr 2>/dev/null');
|
|
1069
1069
|
stdout = result.stdout;
|
|
1070
1070
|
} else {
|
|
1071
|
-
const result = await execFileAsync('ip', ['a'], { timeout: 5000 }).catch(() =>
|
|
1072
|
-
execFileAsync('ip', ['addr'], { timeout: 5000 }),
|
|
1073
|
-
);
|
|
1071
|
+
const result = await execFileAsync('ip', ['a'], { timeout: 5000 }).catch(() => execFileAsync('ip', ['addr'], { timeout: 5000 }));
|
|
1074
1072
|
stdout = result.stdout;
|
|
1075
1073
|
}
|
|
1076
1074
|
// Extract IPv4 and IPv6 addresses; skip loopback
|
|
@@ -1121,8 +1119,8 @@ router.post('/ssh/keygen', async (req, res) => {
|
|
|
1121
1119
|
|
|
1122
1120
|
/** POST /she/broker/ssh/test — Test SSH connection */
|
|
1123
1121
|
router.post('/ssh/test', async (req, res) => {
|
|
1122
|
+
const bc = getBrokerConfig(req);
|
|
1124
1123
|
try {
|
|
1125
|
-
const bc = getBrokerConfig(req);
|
|
1126
1124
|
if (!bc.ssh || !bc.ssh.host) return res.status(400).json({ error: 'broker.ssh.host not configured' });
|
|
1127
1125
|
const user = (bc.ssh && bc.ssh.user) || require('os').userInfo().username;
|
|
1128
1126
|
const key = sshDeploy.expandHome((bc.ssh && bc.ssh.identityFile) || DEFAULT_SSH_KEY);
|
|
@@ -1449,7 +1447,6 @@ router.post('/wizard/reinit', (req, res) => {
|
|
|
1449
1447
|
router.get('/wizard/diagnose', async (req, res) => {
|
|
1450
1448
|
try {
|
|
1451
1449
|
const bc = getBrokerConfig(req);
|
|
1452
|
-
const configPath = req.app.locals.configPath;
|
|
1453
1450
|
const adminUsername = bc.dynsec && bc.dynsec.adminUsername;
|
|
1454
1451
|
const configDir = bc.configDir || '/etc/mosquitto';
|
|
1455
1452
|
const dynSecPath = `${configDir}/dynamic-security.json`;
|
|
@@ -1491,16 +1488,21 @@ router.get('/wizard/diagnose', async (req, res) => {
|
|
|
1491
1488
|
const hasAdminRole = adminRoles.includes('admin');
|
|
1492
1489
|
|
|
1493
1490
|
const adminRoleDef = roles.find((r) => r.rolename === 'admin');
|
|
1494
|
-
const adminRoleAcls = adminRoleDef ?
|
|
1495
|
-
const hasControlSendAcl = adminRoleAcls.some(
|
|
1496
|
-
(a) => a.acltype === 'publishClientSend' && (a.topic || '').includes('$CONTROL/dynamic-security')
|
|
1497
|
-
);
|
|
1491
|
+
const adminRoleAcls = adminRoleDef ? adminRoleDef.acls || [] : [];
|
|
1492
|
+
const hasControlSendAcl = adminRoleAcls.some((a) => a.acltype === 'publishClientSend' && (a.topic || '').includes('$CONTROL/dynamic-security'));
|
|
1498
1493
|
|
|
1499
1494
|
const issues = [];
|
|
1500
1495
|
if (!adminUsername) issues.push('broker.dynsec.adminUsername not set in config.json');
|
|
1501
|
-
if (adminUsername && !adminClientExists)
|
|
1502
|
-
|
|
1503
|
-
|
|
1496
|
+
if (adminUsername && !adminClientExists)
|
|
1497
|
+
issues.push(
|
|
1498
|
+
`User "${adminUsername}" not found in dynamic-security.json — mosquitto_ctrl init may have silently failed (file exists and is not writable by the SSH user). Delete ${dynSecPath} on the broker and re-run the wizard.`,
|
|
1499
|
+
);
|
|
1500
|
+
if (adminClientExists && !hasAdminRole)
|
|
1501
|
+
issues.push(`User "${adminUsername}" exists but does not have the "admin" role — ACL for $CONTROL/dynamic-security/# may be missing.`);
|
|
1502
|
+
if (hasAdminRole && !hasControlSendAcl)
|
|
1503
|
+
issues.push(
|
|
1504
|
+
`The "admin" role exists but is missing a publishClientSend ACL for $CONTROL/dynamic-security/#. Re-run mosquitto_ctrl dynsec init to regenerate the file.`,
|
|
1505
|
+
);
|
|
1504
1506
|
|
|
1505
1507
|
res.json({
|
|
1506
1508
|
ok: issues.length === 0,
|
package/src/web/mqtt-api.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
const express = require('express');
|
|
19
19
|
const { broadcast, broadcastBrokerLog } = require('./log-ws');
|
|
20
20
|
|
|
21
|
-
const BROKER_LOG_TOPICS = new Set(['D', 'I', 'N', 'W', 'E'].map(l => `$SYS/broker/log/${l}`));
|
|
21
|
+
const BROKER_LOG_TOPICS = new Set(['D', 'I', 'N', 'W', 'E'].map((l) => `$SYS/broker/log/${l}`));
|
|
22
22
|
|
|
23
23
|
const router = express.Router();
|
|
24
24
|
|
package/src/web/scripts-api.js
CHANGED
|
@@ -173,7 +173,7 @@ router.use((req, res) => {
|
|
|
173
173
|
|
|
174
174
|
if (mode === 'files') {
|
|
175
175
|
const lq = q.toLowerCase();
|
|
176
|
-
const all = allFiles.map(f => f.path).filter(p => p.toLowerCase().includes(lq));
|
|
176
|
+
const all = allFiles.map((f) => f.path).filter((p) => p.toLowerCase().includes(lq));
|
|
177
177
|
return res.json({ results: all.slice(0, 200), truncated: all.length > 200 });
|
|
178
178
|
}
|
|
179
179
|
|
|
@@ -181,8 +181,11 @@ router.use((req, res) => {
|
|
|
181
181
|
const MAX = 500;
|
|
182
182
|
let pattern = null;
|
|
183
183
|
if (isRegex) {
|
|
184
|
-
try {
|
|
185
|
-
|
|
184
|
+
try {
|
|
185
|
+
pattern = new RegExp(q, caseSensitive ? '' : 'i');
|
|
186
|
+
} catch (e) {
|
|
187
|
+
return res.status(400).json({ error: 'Invalid regex: ' + e.message });
|
|
188
|
+
}
|
|
186
189
|
}
|
|
187
190
|
const results = [];
|
|
188
191
|
let total = 0;
|
|
@@ -192,12 +195,19 @@ router.use((req, res) => {
|
|
|
192
195
|
const abs = safePath(root, file.path);
|
|
193
196
|
if (!abs) continue;
|
|
194
197
|
let buf;
|
|
195
|
-
try {
|
|
198
|
+
try {
|
|
199
|
+
buf = fs.readFileSync(abs);
|
|
200
|
+
} catch {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
196
203
|
if (buf.slice(0, 8192).indexOf(0) !== -1) continue; // binary
|
|
197
204
|
const lines = buf.toString('utf8').split('\n');
|
|
198
205
|
const fileMatches = [];
|
|
199
206
|
for (let i = 0; i < lines.length; i++) {
|
|
200
|
-
if (total >= MAX) {
|
|
207
|
+
if (total >= MAX) {
|
|
208
|
+
truncated = true;
|
|
209
|
+
break outer;
|
|
210
|
+
}
|
|
201
211
|
const line = lines[i];
|
|
202
212
|
let col = -1;
|
|
203
213
|
if (pattern) {
|