smart-home-engine 1.1.5 → 1.1.9
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/dist/web/assets/{index-BcELVSd4.js → index-CZNc_m6c.js} +3 -3
- package/dist/web/assets/{tsMode-B3Xbg24N.js → tsMode-Cnr_wxoV.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/src/index.js +3 -0
- package/src/lib/dynsec.js +28 -5
- package/src/web/broker-api.js +93 -16
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-
|
|
1
|
+
import{m as O}from"./monaco-langs-BW2J83t5.js";import{t as I}from"./index-CZNc_m6c.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,7 +172,7 @@
|
|
|
172
172
|
}
|
|
173
173
|
})();
|
|
174
174
|
</script>
|
|
175
|
-
<script type="module" crossorigin src="/assets/index-
|
|
175
|
+
<script type="module" crossorigin src="/assets/index-CZNc_m6c.js"></script>
|
|
176
176
|
<link rel="modulepreload" crossorigin href="/assets/monaco-langs-BW2J83t5.js">
|
|
177
177
|
<link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
|
|
178
178
|
<link rel="stylesheet" crossorigin href="/assets/index-iTrR9H4f.css">
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -119,6 +119,7 @@ if (typeof config.port !== 'undefined') {
|
|
|
119
119
|
log.error('http server start failed:', err.message);
|
|
120
120
|
process.exit(1);
|
|
121
121
|
});
|
|
122
|
+
require('./web/broker-api').setLogger(log);
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
const chokidar = require('chokidar');
|
|
@@ -145,6 +146,7 @@ const scheduler = modules['node-schedule'];
|
|
|
145
146
|
const StateStore = require('./lib/state-store');
|
|
146
147
|
const sandboxModules = [];
|
|
147
148
|
const store = new StateStore();
|
|
149
|
+
if (typeof config.port !== 'undefined') require('./web/broker-api').setStore(store);
|
|
148
150
|
const scripts = {};
|
|
149
151
|
const scriptOrigins = new Map(); // file → 'builtin' | 'user'
|
|
150
152
|
const subscriptions = [];
|
|
@@ -360,6 +362,7 @@ if (config.url) {
|
|
|
360
362
|
log.info('mqtt connected ' + config.url);
|
|
361
363
|
log.debug('mqtt subscribe #');
|
|
362
364
|
mqtt.subscribe('#');
|
|
365
|
+
mqtt.subscribe('$SYS/#');
|
|
363
366
|
mqttEventCallbacks.filter((c) => c.event === 'connect').forEach((c) => c.callback());
|
|
364
367
|
|
|
365
368
|
if (!_started) {
|
package/src/lib/dynsec.js
CHANGED
|
@@ -39,11 +39,20 @@ function _drain() {
|
|
|
39
39
|
const { command, payload, resolve, reject } = _queue.shift();
|
|
40
40
|
_inflight = true;
|
|
41
41
|
|
|
42
|
+
const safePayload = { ...payload };
|
|
43
|
+
if ('password' in safePayload) safePayload.password = '***';
|
|
44
|
+
if (_log) _log.debug(`dynsec: → sending "${command}"`, JSON.stringify(safePayload));
|
|
45
|
+
|
|
42
46
|
const timer = setTimeout(() => {
|
|
43
47
|
_inflight = false;
|
|
44
48
|
_inflightResolve = null;
|
|
49
|
+
if (_log) _log.warn(`dynsec: timeout waiting for response to "${command}" (${_timeout}ms) — is the dynsec plugin loaded and the admin user configured?`);
|
|
45
50
|
reject(new Error(`dynsec timeout waiting for response to "${command}"`));
|
|
46
|
-
|
|
51
|
+
// Fail-fast: reject all remaining queued commands since the broker is not responding
|
|
52
|
+
while (_queue.length > 0) {
|
|
53
|
+
const queued = _queue.shift();
|
|
54
|
+
queued.reject(new Error(`dynsec: aborting "${queued.command}" — previous command timed out`));
|
|
55
|
+
}
|
|
47
56
|
}, _timeout);
|
|
48
57
|
|
|
49
58
|
_inflightResolve = (responses) => {
|
|
@@ -52,8 +61,10 @@ function _drain() {
|
|
|
52
61
|
_inflightResolve = null;
|
|
53
62
|
const r = responses.find((resp) => resp.command === command);
|
|
54
63
|
if (r && r.error) {
|
|
64
|
+
if (_log) _log.debug(`dynsec: ✕ "${command}" error: ${r.error}`);
|
|
55
65
|
reject(new Error(r.error));
|
|
56
66
|
} else {
|
|
67
|
+
if (_log) _log.debug(`dynsec: ✓ "${command}" ok`);
|
|
57
68
|
resolve(r || {});
|
|
58
69
|
}
|
|
59
70
|
_drain();
|
|
@@ -67,8 +78,12 @@ function _request(command, payload = {}) {
|
|
|
67
78
|
return Promise.reject(new Error('she.broker: dynsec not configured — set broker.dynsec in config.json'));
|
|
68
79
|
}
|
|
69
80
|
if (!_connected) {
|
|
81
|
+
if (_log) _log.debug(`dynsec: request "${command}" rejected — not connected (queue length: ${_queue.length})`);
|
|
70
82
|
return Promise.reject(new Error('she.broker: dynsec not connected'));
|
|
71
83
|
}
|
|
84
|
+
const safePayload = { ...payload };
|
|
85
|
+
if ('password' in safePayload) safePayload.password = '***';
|
|
86
|
+
if (_log) _log.debug(`dynsec: queuing "${command}" (queue length: ${_queue.length}, inflight: ${_inflight})`, JSON.stringify(safePayload));
|
|
72
87
|
return new Promise((resolve, reject) => {
|
|
73
88
|
_queue.push({ command, payload, resolve, reject });
|
|
74
89
|
_drain();
|
|
@@ -111,11 +126,15 @@ function init(config, log) {
|
|
|
111
126
|
_client = mqtt.connect(config.url, opts);
|
|
112
127
|
|
|
113
128
|
_client.on('connect', () => {
|
|
114
|
-
|
|
115
|
-
_log.info('dynsec: connected as', dynsecCfg.adminUsername);
|
|
129
|
+
_log.info('dynsec: MQTT connect event, subscribing to response topic');
|
|
116
130
|
_client.subscribe(RESPONSE_TOPIC, (err) => {
|
|
117
|
-
if (err)
|
|
118
|
-
|
|
131
|
+
if (err) {
|
|
132
|
+
_log.error('dynsec: failed to subscribe to response topic:', err.message);
|
|
133
|
+
} else {
|
|
134
|
+
_connected = true;
|
|
135
|
+
_log.info('dynsec: ready — subscribed as', dynsecCfg.adminUsername);
|
|
136
|
+
_drain(); // flush any requests queued before connection
|
|
137
|
+
}
|
|
119
138
|
});
|
|
120
139
|
});
|
|
121
140
|
|
|
@@ -139,8 +158,12 @@ function init(config, log) {
|
|
|
139
158
|
_log.error('dynsec: invalid JSON on response topic');
|
|
140
159
|
return;
|
|
141
160
|
}
|
|
161
|
+
const cmds = Array.isArray(msg.responses) ? msg.responses.map((r) => r.command).join(', ') : 'none';
|
|
162
|
+
if (_log) _log.debug(`dynsec: ← response received, commands: [${cmds}]`);
|
|
142
163
|
if (_inflightResolve && Array.isArray(msg.responses)) {
|
|
143
164
|
_inflightResolve(msg.responses);
|
|
165
|
+
} else if (!_inflightResolve) {
|
|
166
|
+
if (_log) _log.debug(`dynsec: unexpected response (no inflight request), commands: [${cmds}]`);
|
|
144
167
|
}
|
|
145
168
|
});
|
|
146
169
|
}
|
package/src/web/broker-api.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
const express = require('express');
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const fs = require('fs');
|
|
19
|
+
const crypto = require('crypto');
|
|
19
20
|
const dynsec = require('../lib/dynsec');
|
|
20
21
|
const mosquittoConf = require('../lib/mosquitto-conf');
|
|
21
22
|
const ca = require('../lib/ca');
|
|
@@ -27,6 +28,19 @@ const DEFAULT_SSH_KEY = path.join(sheConfig['data-dir'], 'ssh', 'broker_id_ed255
|
|
|
27
28
|
|
|
28
29
|
const router = express.Router();
|
|
29
30
|
|
|
31
|
+
let _log = null;
|
|
32
|
+
let _store = null;
|
|
33
|
+
|
|
34
|
+
/** Must be called once from index.js so broker-api can emit debug-level log lines. */
|
|
35
|
+
function setLogger(log) {
|
|
36
|
+
_log = log;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Must be called once from index.js to give broker-api access to the MQTT state store. */
|
|
40
|
+
function setStore(store) {
|
|
41
|
+
_store = store;
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
31
45
|
|
|
32
46
|
/** Get broker config from live config.json */
|
|
@@ -64,13 +78,14 @@ function handleError(res, err) {
|
|
|
64
78
|
*/
|
|
65
79
|
router.get('/status', (req, res) => {
|
|
66
80
|
const ds = dynsec.getStatus();
|
|
67
|
-
const mqttState = req.app.locals.mqttState || {};
|
|
68
81
|
|
|
69
|
-
const sysPrefixes = ['$SYS/broker/version', '$SYS/broker/clients/', '$SYS/broker/uptime'];
|
|
70
82
|
const sys = {};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
if (_store) {
|
|
84
|
+
const sysPrefixes = ['$SYS/broker/version', '$SYS/broker/uptime', '$SYS/broker/clients/', '$SYS/broker/messages/'];
|
|
85
|
+
for (const [topic, entry] of _store.mqttEntries()) {
|
|
86
|
+
if (sysPrefixes.some((p) => topic.startsWith(p))) {
|
|
87
|
+
sys[topic] = entry;
|
|
88
|
+
}
|
|
74
89
|
}
|
|
75
90
|
}
|
|
76
91
|
|
|
@@ -82,14 +97,22 @@ router.get('/status', (req, res) => {
|
|
|
82
97
|
/**
|
|
83
98
|
* GET /she/broker/config
|
|
84
99
|
* Returns parsed config structure + raw text + checksum.
|
|
100
|
+
* In remote mode, reads mosquitto.conf from the broker host via SSH.
|
|
85
101
|
*/
|
|
86
|
-
router.get('/config', (req, res) => {
|
|
102
|
+
router.get('/config', async (req, res) => {
|
|
87
103
|
try {
|
|
88
104
|
const bc = getBrokerConfig(req);
|
|
89
105
|
const fp = confPath(bc);
|
|
106
|
+
const backups = mosquittoConf.listBackups(fp).map((b) => path.basename(b));
|
|
107
|
+
if (bc.ssh && bc.ssh.host) {
|
|
108
|
+
_log?.debug(`broker: reading remote config from ${bc.ssh.host}:${fp}`);
|
|
109
|
+
const raw = await sshDeploy.readRemoteFile(bc.ssh, fp);
|
|
110
|
+
const parsed = mosquittoConf.parseText(raw);
|
|
111
|
+
const cs = crypto.createHash('sha256').update(raw).digest('hex');
|
|
112
|
+
return res.json({ ...parsed, checksum: cs, backups });
|
|
113
|
+
}
|
|
90
114
|
const parsed = mosquittoConf.parse(fp);
|
|
91
115
|
const cs = mosquittoConf.checksum(fp);
|
|
92
|
-
const backups = mosquittoConf.listBackups(fp).map((b) => path.basename(b));
|
|
93
116
|
res.json({ ...parsed, checksum: cs, backups });
|
|
94
117
|
} catch (err) {
|
|
95
118
|
handleError(res, err);
|
|
@@ -100,17 +123,25 @@ router.get('/config', (req, res) => {
|
|
|
100
123
|
* PUT /she/broker/config
|
|
101
124
|
* Write structured config (body: { listeners, managed, passthrough, checksum? }).
|
|
102
125
|
* body.checksum is the client's last-known checksum for external-modify detection.
|
|
126
|
+
* In remote mode, the file is also deployed to the broker host via SCP.
|
|
103
127
|
*/
|
|
104
|
-
router.put('/config', (req, res) => {
|
|
128
|
+
router.put('/config', async (req, res) => {
|
|
105
129
|
try {
|
|
106
130
|
const bc = getBrokerConfig(req);
|
|
107
131
|
const fp = confPath(bc);
|
|
108
132
|
const { listeners, managed, passthrough, checksum: clientChecksum } = req.body;
|
|
109
133
|
const content = mosquittoConf.serialise({ listeners, managed, passthrough });
|
|
110
|
-
const
|
|
134
|
+
const remote = bc.ssh && bc.ssh.host;
|
|
135
|
+
_log?.info(`broker: writing config to ${remote ? `remote ${bc.ssh.host}:${fp}` : `local ${fp}`}`);
|
|
136
|
+
// In remote mode skip local conflict check — remote is the source of truth
|
|
137
|
+
const result = mosquittoConf.write(fp, content, remote ? null : (clientChecksum ?? null));
|
|
111
138
|
if (!result.ok) {
|
|
112
139
|
return res.status(409).json({ error: 'external_modify', message: 'mosquitto.conf was modified externally since last read' });
|
|
113
140
|
}
|
|
141
|
+
if (remote) {
|
|
142
|
+
_log?.debug(`broker: uploading config to ${bc.ssh.host}:${fp}`);
|
|
143
|
+
await sshDeploy.uploadContent(bc.ssh, content, fp);
|
|
144
|
+
}
|
|
114
145
|
_lastWriteChecksum.set(fp, mosquittoConf.checksum(fp));
|
|
115
146
|
res.json({ ok: true, backupPath: result.backupPath ? path.basename(result.backupPath) : null });
|
|
116
147
|
} catch (err) {
|
|
@@ -121,8 +152,9 @@ router.put('/config', (req, res) => {
|
|
|
121
152
|
/**
|
|
122
153
|
* PUT /she/broker/config/raw
|
|
123
154
|
* Write raw mosquitto.conf text directly (used by the Advanced editor).
|
|
155
|
+
* In remote mode, the file is also deployed to the broker host via SCP.
|
|
124
156
|
*/
|
|
125
|
-
router.put('/config/raw', (req, res) => {
|
|
157
|
+
router.put('/config/raw', async (req, res) => {
|
|
126
158
|
try {
|
|
127
159
|
const bc = getBrokerConfig(req);
|
|
128
160
|
const fp = confPath(bc);
|
|
@@ -130,10 +162,17 @@ router.put('/config/raw', (req, res) => {
|
|
|
130
162
|
if (typeof content !== 'string') {
|
|
131
163
|
return res.status(400).json({ error: 'content must be a string' });
|
|
132
164
|
}
|
|
133
|
-
const
|
|
165
|
+
const remote = bc.ssh && bc.ssh.host;
|
|
166
|
+
_log?.info(`broker: writing raw config to ${remote ? `remote ${bc.ssh.host}:${fp}` : `local ${fp}`}`);
|
|
167
|
+
// In remote mode skip local conflict check — remote is the source of truth
|
|
168
|
+
const result = mosquittoConf.write(fp, content, remote ? null : (clientChecksum ?? null));
|
|
134
169
|
if (!result.ok) {
|
|
135
170
|
return res.status(409).json({ error: 'external_modify', message: 'mosquitto.conf was modified externally since last read' });
|
|
136
171
|
}
|
|
172
|
+
if (remote) {
|
|
173
|
+
_log?.debug(`broker: uploading raw config to ${bc.ssh.host}:${fp}`);
|
|
174
|
+
await sshDeploy.uploadContent(bc.ssh, content, fp);
|
|
175
|
+
}
|
|
137
176
|
_lastWriteChecksum.set(fp, mosquittoConf.checksum(fp));
|
|
138
177
|
res.json({ ok: true, backupPath: result.backupPath ? path.basename(result.backupPath) : null });
|
|
139
178
|
} catch (err) {
|
|
@@ -191,12 +230,18 @@ router.post('/reload', async (req, res) => {
|
|
|
191
230
|
try {
|
|
192
231
|
const bc = getBrokerConfig(req);
|
|
193
232
|
if (bc.ssh && bc.ssh.host) {
|
|
233
|
+
const cmd = bc.reloadCmd || 'sudo systemctl reload mosquitto';
|
|
234
|
+
_log?.debug(`broker: remote reload on ${bc.ssh.host}: ${cmd}`);
|
|
194
235
|
const result = await sshDeploy.runCommand(bc.ssh, cmd);
|
|
236
|
+
_log?.debug(`broker: remote reload stdout=${result.stdout} stderr=${result.stderr}`);
|
|
195
237
|
return res.json({ ok: true, ...result });
|
|
196
238
|
}
|
|
239
|
+
_log?.debug('broker: local reload mosquitto');
|
|
197
240
|
const result = await mosquittoConf.reload(bc);
|
|
241
|
+
_log?.debug(`broker: local reload stdout=${result.stdout} stderr=${result.stderr}`);
|
|
198
242
|
res.json({ ok: true, stdout: result.stdout, stderr: result.stderr });
|
|
199
243
|
} catch (err) {
|
|
244
|
+
_log?.debug(`broker: reload error: ${err.message}`);
|
|
200
245
|
handleError(res, err);
|
|
201
246
|
}
|
|
202
247
|
});
|
|
@@ -211,12 +256,17 @@ router.post('/restart', async (req, res) => {
|
|
|
211
256
|
const bc = getBrokerConfig(req);
|
|
212
257
|
if (bc.ssh && bc.ssh.host) {
|
|
213
258
|
const cmd = bc.restartCmd || 'sudo systemctl restart mosquitto';
|
|
259
|
+
_log?.debug(`broker: remote restart on ${bc.ssh.host}: ${cmd}`);
|
|
214
260
|
const result = await sshDeploy.runCommand(bc.ssh, cmd);
|
|
261
|
+
_log?.debug(`broker: remote restart stdout=${result.stdout} stderr=${result.stderr}`);
|
|
215
262
|
return res.json({ ok: true, ...result });
|
|
216
263
|
}
|
|
264
|
+
_log?.debug('broker: local restart mosquitto');
|
|
217
265
|
const result = await mosquittoConf.restart(bc);
|
|
266
|
+
_log?.debug(`broker: local restart stdout=${result.stdout} stderr=${result.stderr}`);
|
|
218
267
|
res.json({ ok: true, stdout: result.stdout, stderr: result.stderr });
|
|
219
268
|
} catch (err) {
|
|
269
|
+
_log?.debug(`broker: restart error: ${err.message}`);
|
|
220
270
|
handleError(res, err);
|
|
221
271
|
}
|
|
222
272
|
});
|
|
@@ -655,7 +705,7 @@ router.delete('/ca/trusted/:fingerprint', async (req, res) => {
|
|
|
655
705
|
}
|
|
656
706
|
});
|
|
657
707
|
|
|
658
|
-
module.exports = { router };
|
|
708
|
+
module.exports = { router, setLogger, setStore };
|
|
659
709
|
|
|
660
710
|
// ── SSH routes ─────────────────────────────────────────────────────────────────
|
|
661
711
|
// Note: these routes are mounted on the same router but defined after module.exports
|
|
@@ -680,9 +730,12 @@ router.post('/ssh/keygen', async (req, res) => {
|
|
|
680
730
|
try {
|
|
681
731
|
const bc = getBrokerConfig(req);
|
|
682
732
|
const identityFile = (bc.ssh && bc.ssh.identityFile) || DEFAULT_SSH_KEY;
|
|
733
|
+
_log?.debug(`broker: generating SSH keypair at ${identityFile}`);
|
|
683
734
|
const publicKey = await sshDeploy.generateKeypair(identityFile);
|
|
735
|
+
_log?.debug('broker: SSH keypair generated ok');
|
|
684
736
|
res.json({ ok: true, publicKey });
|
|
685
737
|
} catch (err) {
|
|
738
|
+
_log?.debug(`broker: SSH keygen error: ${err.message}`);
|
|
686
739
|
handleError(res, err);
|
|
687
740
|
}
|
|
688
741
|
});
|
|
@@ -692,9 +745,14 @@ router.post('/ssh/test', async (req, res) => {
|
|
|
692
745
|
try {
|
|
693
746
|
const bc = getBrokerConfig(req);
|
|
694
747
|
if (!bc.ssh || !bc.ssh.host) return res.status(400).json({ error: 'broker.ssh.host not configured' });
|
|
748
|
+
const user = (bc.ssh && bc.ssh.user) || require('os').userInfo().username;
|
|
749
|
+
const key = sshDeploy.expandHome((bc.ssh && bc.ssh.identityFile) || DEFAULT_SSH_KEY);
|
|
750
|
+
_log?.debug(`broker: testing SSH to ${user}@${bc.ssh.host}:${bc.ssh.port || 22} key=${key}`);
|
|
695
751
|
await sshDeploy.testConnection(bc.ssh);
|
|
752
|
+
_log?.debug(`broker: SSH connection to ${bc.ssh.host} ok`);
|
|
696
753
|
res.json({ ok: true });
|
|
697
754
|
} catch (err) {
|
|
755
|
+
_log?.debug(`broker: SSH test to ${bc.ssh && bc.ssh.host} failed: ${err.message}`);
|
|
698
756
|
res.json({ ok: false, error: err.message });
|
|
699
757
|
}
|
|
700
758
|
});
|
|
@@ -741,11 +799,17 @@ router.post('/wizard/bootstrap', async (req, res) => {
|
|
|
741
799
|
const dynSecPath = `${configDir}/dynamic-security.json`;
|
|
742
800
|
const confFilePath = `${configDir}/mosquitto.conf`;
|
|
743
801
|
|
|
802
|
+
_log?.debug(`broker: wizard bootstrap mode=${isRemote ? 'remote' : 'local'} configDir=${configDir} adminUser=${username}`);
|
|
803
|
+
|
|
744
804
|
if (isRemote) {
|
|
745
805
|
// mosquitto_ctrl must run on the broker host — invoke it via SSH.
|
|
806
|
+
const ctrlCmd = `mosquitto_ctrl dynsec init "${dynSecPath}" "${username}" "${password}"`;
|
|
807
|
+
_log?.debug(`broker: SSH mosquitto_ctrl on ${bc.ssh.host}: mosquitto_ctrl dynsec init "${dynSecPath}" "${username}" ***`);
|
|
746
808
|
try {
|
|
747
|
-
await sshDeploy.runCommand(bc.ssh,
|
|
809
|
+
const r = await sshDeploy.runCommand(bc.ssh, ctrlCmd);
|
|
810
|
+
_log?.debug(`broker: mosquitto_ctrl ok stdout=${r.stdout} stderr=${r.stderr}`);
|
|
748
811
|
} catch (err) {
|
|
812
|
+
_log?.debug(`broker: mosquitto_ctrl SSH failed: ${err.message}`);
|
|
749
813
|
return res.status(500).json({
|
|
750
814
|
error: `mosquitto_ctrl failed on remote host: ${err.message}. Ensure mosquitto is installed on the remote broker host.`,
|
|
751
815
|
});
|
|
@@ -754,23 +818,32 @@ router.post('/wizard/bootstrap', async (req, res) => {
|
|
|
754
818
|
// Read the remote mosquitto.conf, parse, and add the plugin line if missing.
|
|
755
819
|
let remoteConfRaw = '';
|
|
756
820
|
try {
|
|
821
|
+
_log?.debug(`broker: reading remote conf ${bc.ssh.host}:${confFilePath}`);
|
|
757
822
|
remoteConfRaw = await sshDeploy.readRemoteFile(bc.ssh, confFilePath);
|
|
758
|
-
|
|
759
|
-
|
|
823
|
+
_log?.debug(`broker: remote conf read ok (${remoteConfRaw.length} bytes)`);
|
|
824
|
+
} catch (e) {
|
|
825
|
+
_log?.debug(`broker: remote conf read failed (${e.message}), starting from empty config`);
|
|
760
826
|
}
|
|
761
827
|
const parsed = mosquittoConf.parseText(remoteConfRaw);
|
|
762
828
|
if (!parsed.managed.plugin || !String(parsed.managed.plugin).includes('mosquitto_dynamic_security')) {
|
|
763
829
|
parsed.managed.plugin = 'mosquitto_dynamic_security.so';
|
|
764
830
|
parsed.managed.plugin_opt_dynsec_config_file = dynSecPath;
|
|
765
831
|
const content = mosquittoConf.serialise(parsed);
|
|
832
|
+
_log?.debug(`broker: uploading updated conf to ${bc.ssh.host}:${confFilePath}`);
|
|
766
833
|
await sshDeploy.uploadContent(bc.ssh, content, confFilePath);
|
|
834
|
+
_log?.debug('broker: conf upload ok');
|
|
835
|
+
} else {
|
|
836
|
+
_log?.debug('broker: plugin line already present in remote conf, skipping upload');
|
|
767
837
|
}
|
|
768
838
|
} else {
|
|
769
839
|
// Local mode: run mosquitto_ctrl on this host.
|
|
770
840
|
fs.mkdirSync(configDir, { recursive: true });
|
|
841
|
+
_log?.debug(`broker: local mosquitto_ctrl dynsec init ${dynSecPath} ${username} ***`);
|
|
771
842
|
try {
|
|
772
|
-
await execFileAsync('mosquitto_ctrl', ['dynsec', 'init', dynSecPath, username, password], { timeout: 10000 });
|
|
843
|
+
const r = await execFileAsync('mosquitto_ctrl', ['dynsec', 'init', dynSecPath, username, password], { timeout: 10000 });
|
|
844
|
+
_log?.debug(`broker: mosquitto_ctrl ok stdout=${r.stdout} stderr=${r.stderr}`);
|
|
773
845
|
} catch (err) {
|
|
846
|
+
_log?.debug(`broker: local mosquitto_ctrl failed: ${err.message}`);
|
|
774
847
|
return res.status(500).json({
|
|
775
848
|
error: `mosquitto_ctrl failed: ${err.message}. Ensure mosquitto is installed on this host.`,
|
|
776
849
|
});
|
|
@@ -782,7 +855,11 @@ router.post('/wizard/bootstrap', async (req, res) => {
|
|
|
782
855
|
parsed.managed.plugin = 'mosquitto_dynamic_security.so';
|
|
783
856
|
parsed.managed.plugin_opt_dynsec_config_file = dynSecPath;
|
|
784
857
|
const content = mosquittoConf.serialise(parsed);
|
|
858
|
+
_log?.debug(`broker: writing updated local conf to ${confFilePath}`);
|
|
785
859
|
mosquittoConf.write(confFilePath, content);
|
|
860
|
+
_log?.debug('broker: local conf write ok');
|
|
861
|
+
} else {
|
|
862
|
+
_log?.debug('broker: plugin line already present in local conf, skipping write');
|
|
786
863
|
}
|
|
787
864
|
}
|
|
788
865
|
|