smart-home-engine 1.1.0 → 1.1.1
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 +4 -1
- package/dist/web/assets/{index-YxGnpZAh.js → index-DttCbWJj.js} +46 -46
- package/dist/web/assets/{tsMode-DAKcfE4c.js → tsMode-BcbP1HsB.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/src/lib/ca.js +2 -2
- package/src/lib/mosquitto-conf.js +21 -13
- package/src/lib/ssh-deploy.js +65 -119
- package/src/web/broker-api.js +76 -50
|
@@ -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-DttCbWJj.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-DttCbWJj.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-DKIgEFlE.css">
|
package/package.json
CHANGED
package/src/lib/ca.js
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* client.crt
|
|
18
18
|
* client.p12
|
|
19
19
|
*
|
|
20
|
-
* CA cert metadata is stored in sheDB at broker
|
|
21
|
-
* Issued cert metadata is stored in sheDB at broker
|
|
20
|
+
* CA cert metadata is stored in sheDB at she/broker/ca.
|
|
21
|
+
* Issued cert metadata is stored in sheDB at she/broker/cert/<serial>.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
const { execFile } = require('child_process');
|
|
@@ -29,20 +29,12 @@ const execFileAsync = promisify(execFile);
|
|
|
29
29
|
const MANAGED_SINGLE_KEYS = new Set(['allow_anonymous', 'persistence', 'persistence_location', 'log_dest', 'log_type', 'plugin', 'plugin_opt_dynsec_config_file']);
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Parse
|
|
32
|
+
* Parse mosquitto.conf text into a structured object.
|
|
33
33
|
*
|
|
34
|
-
* @param {string}
|
|
34
|
+
* @param {string} raw - raw mosquitto.conf content
|
|
35
35
|
* @returns {{ listeners: object[], managed: object, passthrough: string[], raw: string }}
|
|
36
36
|
*/
|
|
37
|
-
function
|
|
38
|
-
let raw = '';
|
|
39
|
-
try {
|
|
40
|
-
raw = fs.readFileSync(filePath, 'utf8');
|
|
41
|
-
} catch (err) {
|
|
42
|
-
if (err.code !== 'ENOENT') throw err;
|
|
43
|
-
return { listeners: [], managed: {}, passthrough: [], raw: '' };
|
|
44
|
-
}
|
|
45
|
-
|
|
37
|
+
function parseText(raw) {
|
|
46
38
|
const lines = raw.split('\n');
|
|
47
39
|
const managed = {};
|
|
48
40
|
const listeners = [];
|
|
@@ -79,7 +71,6 @@ function parse(filePath) {
|
|
|
79
71
|
applyListenerKey(currentListener, key, value);
|
|
80
72
|
} else if (MANAGED_SINGLE_KEYS.has(key)) {
|
|
81
73
|
if (managed[key] !== undefined) {
|
|
82
|
-
// multi-value key (e.g. log_type) — convert to array
|
|
83
74
|
managed[key] = [].concat(managed[key]).concat(value);
|
|
84
75
|
} else {
|
|
85
76
|
managed[key] = value;
|
|
@@ -94,6 +85,23 @@ function parse(filePath) {
|
|
|
94
85
|
return { listeners, managed, passthrough, raw };
|
|
95
86
|
}
|
|
96
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Parse a mosquitto.conf file into a structured object.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} filePath
|
|
92
|
+
* @returns {{ listeners: object[], managed: object, passthrough: string[], raw: string }}
|
|
93
|
+
*/
|
|
94
|
+
function parse(filePath) {
|
|
95
|
+
let raw = '';
|
|
96
|
+
try {
|
|
97
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err.code !== 'ENOENT') throw err;
|
|
100
|
+
return { listeners: [], managed: {}, passthrough: [], raw: '' };
|
|
101
|
+
}
|
|
102
|
+
return parseText(raw);
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
/** Keys that belong to a listener block */
|
|
98
106
|
function isListenerSubkey(key) {
|
|
99
107
|
return [
|
|
@@ -284,4 +292,4 @@ async function restart(brokerConfig) {
|
|
|
284
292
|
return result;
|
|
285
293
|
}
|
|
286
294
|
|
|
287
|
-
module.exports = { parse, serialise, checksum, write, listBackups, restoreBackup, reload, restart };
|
|
295
|
+
module.exports = { parse, parseText, serialise, checksum, write, listBackups, restoreBackup, reload, restart };
|
package/src/lib/ssh-deploy.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* ssh-deploy.js — SSH/
|
|
4
|
+
* ssh-deploy.js — SSH/SCP file deployment helper for remote broker management.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* SSH keypair generation
|
|
6
|
+
* Shells out to the system ssh and scp clients — no npm dependencies required.
|
|
7
|
+
* SSH keypair generation uses the system ssh-keygen binary.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Requires ssh, scp, and ssh-keygen available in PATH on the she host.
|
|
10
|
+
*
|
|
11
|
+
* StrictHostKeyChecking=accept-new trusts new hosts on first connect and
|
|
12
|
+
* verifies the key on subsequent connections, protecting against MITM after
|
|
13
|
+
* the initial handshake without blocking automation.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const { execFile } = require('child_process');
|
|
@@ -21,19 +21,6 @@ const os = require('os');
|
|
|
21
21
|
|
|
22
22
|
const execFileAsync = promisify(execFile);
|
|
23
23
|
|
|
24
|
-
// ssh2 is an optional dependency — only loaded when needed
|
|
25
|
-
let _ssh2 = null;
|
|
26
|
-
function getSsh2() {
|
|
27
|
-
if (!_ssh2) {
|
|
28
|
-
try {
|
|
29
|
-
_ssh2 = require('ssh2');
|
|
30
|
-
} catch {
|
|
31
|
-
throw new Error('ssh2 package not installed — run: npm install ssh2');
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return _ssh2;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
24
|
function expandHome(p) {
|
|
38
25
|
if (typeof p === 'string' && (p.startsWith('~/') || p === '~')) {
|
|
39
26
|
return path.join(os.homedir(), p.slice(2));
|
|
@@ -42,109 +29,82 @@ function expandHome(p) {
|
|
|
42
29
|
}
|
|
43
30
|
|
|
44
31
|
/**
|
|
45
|
-
* Build
|
|
32
|
+
* Build the common ssh argument list (flags only, no target/command).
|
|
46
33
|
* @param {object} sshConfig - config.broker.ssh
|
|
34
|
+
* @returns {string[]}
|
|
47
35
|
*/
|
|
48
|
-
function
|
|
49
|
-
const identityFile = expandHome(sshConfig.identityFile || '~/.she/broker_id_ed25519');
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
function sshArgs(sshConfig) {
|
|
37
|
+
const identityFile = expandHome(sshConfig.identityFile || '~/.she/ssh/broker_id_ed25519');
|
|
38
|
+
return ['-i', identityFile, '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=accept-new', '-p', String(sshConfig.port || 22)];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build the scp argument list prefix.
|
|
43
|
+
* scp uses -P (capital) for port, unlike ssh which uses -p.
|
|
44
|
+
* @param {object} sshConfig
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
47
|
+
function scpArgs(sshConfig) {
|
|
48
|
+
const identityFile = expandHome(sshConfig.identityFile || '~/.she/ssh/broker_id_ed25519');
|
|
49
|
+
return ['-i', identityFile, '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=accept-new', '-P', String(sshConfig.port || 22)];
|
|
50
|
+
}
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
port: sshConfig.port || 22,
|
|
60
|
-
username: sshConfig.user || 'she',
|
|
61
|
-
privateKey,
|
|
62
|
-
readyTimeout: 10000,
|
|
63
|
-
keepaliveInterval: 0,
|
|
64
|
-
};
|
|
52
|
+
function sshTarget(sshConfig) {
|
|
53
|
+
return `${sshConfig.user || 'she'}@${sshConfig.host}`;
|
|
65
54
|
}
|
|
66
55
|
|
|
67
56
|
/**
|
|
68
|
-
*
|
|
57
|
+
* Run a command on the remote host via the system ssh client.
|
|
69
58
|
* @param {object} sshConfig - config.broker.ssh
|
|
70
59
|
* @param {string} command
|
|
71
60
|
* @returns {Promise<{ stdout: string, stderr: string }>}
|
|
72
61
|
*/
|
|
73
|
-
function runCommand(sshConfig, command) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
stream.on('data', (d) => {
|
|
97
|
-
stdout += d.toString();
|
|
98
|
-
});
|
|
99
|
-
stream.stderr.on('data', (d) => {
|
|
100
|
-
stderr += d.toString();
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
conn.on('error', reject);
|
|
106
|
-
conn.connect(opts);
|
|
107
|
-
});
|
|
62
|
+
async function runCommand(sshConfig, command) {
|
|
63
|
+
const args = [...sshArgs(sshConfig), sshTarget(sshConfig), command];
|
|
64
|
+
try {
|
|
65
|
+
const { stdout, stderr } = await execFileAsync('ssh', args, { timeout: 15000 });
|
|
66
|
+
return { stdout: stdout.trim(), stderr: stderr.trim() };
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const detail = (err.stderr || '').trim() || (err.stdout || '').trim() || err.message;
|
|
69
|
+
throw new Error(detail);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Read the content of a file on the remote host via ssh cat.
|
|
75
|
+
* @param {object} sshConfig
|
|
76
|
+
* @param {string} remotePath
|
|
77
|
+
* @returns {Promise<string>}
|
|
78
|
+
*/
|
|
79
|
+
async function readRemoteFile(sshConfig, remotePath) {
|
|
80
|
+
const { stdout } = await runCommand(sshConfig, `cat -- "${remotePath}"`);
|
|
81
|
+
return stdout;
|
|
108
82
|
}
|
|
109
83
|
|
|
110
84
|
/**
|
|
111
|
-
* Upload a local file to the remote host via
|
|
85
|
+
* Upload a local file to the remote host via scp.
|
|
112
86
|
* @param {object} sshConfig
|
|
113
87
|
* @param {string} localPath
|
|
114
88
|
* @param {string} remotePath
|
|
115
89
|
*/
|
|
116
|
-
function uploadFile(sshConfig, localPath, remotePath) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (err) {
|
|
125
|
-
conn.end();
|
|
126
|
-
return reject(err);
|
|
127
|
-
}
|
|
128
|
-
sftp.fastPut(localPath, remotePath, (err2) => {
|
|
129
|
-
conn.end();
|
|
130
|
-
if (err2) reject(err2);
|
|
131
|
-
else resolve();
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
conn.on('error', reject);
|
|
136
|
-
conn.connect(opts);
|
|
137
|
-
});
|
|
90
|
+
async function uploadFile(sshConfig, localPath, remotePath) {
|
|
91
|
+
const args = [...scpArgs(sshConfig), localPath, `${sshTarget(sshConfig)}:${remotePath}`];
|
|
92
|
+
try {
|
|
93
|
+
await execFileAsync('scp', args, { timeout: 30000 });
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const detail = (err.stderr || '').trim() || err.message;
|
|
96
|
+
throw new Error(detail);
|
|
97
|
+
}
|
|
138
98
|
}
|
|
139
99
|
|
|
140
100
|
/**
|
|
141
101
|
* Upload a string as file content to the remote host.
|
|
102
|
+
* Writes to a local temp file then uploads via scp.
|
|
142
103
|
* @param {object} sshConfig
|
|
143
104
|
* @param {string} content
|
|
144
105
|
* @param {string} remotePath
|
|
145
106
|
*/
|
|
146
107
|
async function uploadContent(sshConfig, content, remotePath) {
|
|
147
|
-
// Write to a temp file, then SFTP upload, then delete temp
|
|
148
108
|
const tmp = path.join(os.tmpdir(), `she-ssh-${Date.now()}.tmp`);
|
|
149
109
|
fs.writeFileSync(tmp, content, 'utf8');
|
|
150
110
|
try {
|
|
@@ -162,8 +122,9 @@ async function uploadContent(sshConfig, content, remotePath) {
|
|
|
162
122
|
* Test SSH connectivity. Resolves to { ok: true } on success, or throws.
|
|
163
123
|
* @param {object} sshConfig
|
|
164
124
|
*/
|
|
165
|
-
function testConnection(sshConfig) {
|
|
166
|
-
|
|
125
|
+
async function testConnection(sshConfig) {
|
|
126
|
+
await runCommand(sshConfig, 'echo ok');
|
|
127
|
+
return { ok: true };
|
|
167
128
|
}
|
|
168
129
|
|
|
169
130
|
/**
|
|
@@ -172,11 +133,10 @@ function testConnection(sshConfig) {
|
|
|
172
133
|
* @returns {Promise<string>} the public key text
|
|
173
134
|
*/
|
|
174
135
|
async function generateKeypair(identityFile) {
|
|
175
|
-
const expandedPath = expandHome(identityFile || '~/.she/broker_id_ed25519');
|
|
136
|
+
const expandedPath = expandHome(identityFile || '~/.she/ssh/broker_id_ed25519');
|
|
176
137
|
const dir = path.dirname(expandedPath);
|
|
177
138
|
fs.mkdirSync(dir, { recursive: true });
|
|
178
139
|
|
|
179
|
-
// Remove existing key if present
|
|
180
140
|
try {
|
|
181
141
|
fs.unlinkSync(expandedPath);
|
|
182
142
|
} catch {
|
|
@@ -188,20 +148,7 @@ async function generateKeypair(identityFile) {
|
|
|
188
148
|
/* ok */
|
|
189
149
|
}
|
|
190
150
|
|
|
191
|
-
await execFileAsync(
|
|
192
|
-
'ssh-keygen',
|
|
193
|
-
[
|
|
194
|
-
'-t',
|
|
195
|
-
'ed25519',
|
|
196
|
-
'-f',
|
|
197
|
-
expandedPath,
|
|
198
|
-
'-N',
|
|
199
|
-
'', // no passphrase
|
|
200
|
-
'-C',
|
|
201
|
-
'she-broker',
|
|
202
|
-
],
|
|
203
|
-
{ timeout: 15000 },
|
|
204
|
-
);
|
|
151
|
+
await execFileAsync('ssh-keygen', ['-t', 'ed25519', '-f', expandedPath, '-N', '', '-C', 'she-broker'], { timeout: 15000 });
|
|
205
152
|
|
|
206
153
|
try {
|
|
207
154
|
fs.chmodSync(expandedPath, 0o600);
|
|
@@ -209,8 +156,7 @@ async function generateKeypair(identityFile) {
|
|
|
209
156
|
/* ignore */
|
|
210
157
|
}
|
|
211
158
|
|
|
212
|
-
|
|
213
|
-
return pubkey.trim();
|
|
159
|
+
return fs.readFileSync(expandedPath + '.pub', 'utf8').trim();
|
|
214
160
|
}
|
|
215
161
|
|
|
216
|
-
module.exports = { runCommand, uploadFile, uploadContent, testConnection, generateKeypair };
|
|
162
|
+
module.exports = { runCommand, readRemoteFile, uploadFile, uploadContent, testConnection, generateKeypair };
|
package/src/web/broker-api.js
CHANGED
|
@@ -20,6 +20,10 @@ const dynsec = require('../lib/dynsec');
|
|
|
20
20
|
const mosquittoConf = require('../lib/mosquitto-conf');
|
|
21
21
|
const ca = require('../lib/ca');
|
|
22
22
|
const sshDeploy = require('../lib/ssh-deploy');
|
|
23
|
+
const sheConfig = require('../config');
|
|
24
|
+
|
|
25
|
+
// Default SSH identity file respects the configured data directory
|
|
26
|
+
const DEFAULT_SSH_KEY = path.join(sheConfig['data-dir'], 'ssh', 'broker_id_ed25519');
|
|
23
27
|
|
|
24
28
|
const router = express.Router();
|
|
25
29
|
|
|
@@ -70,7 +74,7 @@ router.get('/status', (req, res) => {
|
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
res.json({ dynsec: ds, sys });
|
|
77
|
+
res.json({ dynsec: ds, sys, sshKeyDefault: DEFAULT_SSH_KEY });
|
|
74
78
|
});
|
|
75
79
|
|
|
76
80
|
// ── mosquitto.conf ─────────────────────────────────────────────────────────────
|
|
@@ -181,10 +185,16 @@ router.post('/config/restore', (req, res) => {
|
|
|
181
185
|
/**
|
|
182
186
|
* POST /she/broker/reload
|
|
183
187
|
* Send SIGHUP / systemctl reload to mosquitto.
|
|
188
|
+
* In remote mode, the command is executed on the broker host via SSH.
|
|
184
189
|
*/
|
|
185
190
|
router.post('/reload', async (req, res) => {
|
|
186
191
|
try {
|
|
187
192
|
const bc = getBrokerConfig(req);
|
|
193
|
+
if (bc.mode === 'remote' && bc.ssh && bc.ssh.host) {
|
|
194
|
+
const cmd = bc.reloadCmd || 'sudo systemctl reload mosquitto';
|
|
195
|
+
const result = await sshDeploy.runCommand(bc.ssh, cmd);
|
|
196
|
+
return res.json({ ok: true, ...result });
|
|
197
|
+
}
|
|
188
198
|
const result = await mosquittoConf.reload(bc);
|
|
189
199
|
res.json({ ok: true, stdout: result.stdout, stderr: result.stderr });
|
|
190
200
|
} catch (err) {
|
|
@@ -195,10 +205,16 @@ router.post('/reload', async (req, res) => {
|
|
|
195
205
|
/**
|
|
196
206
|
* POST /she/broker/restart
|
|
197
207
|
* Full mosquitto service restart.
|
|
208
|
+
* In remote mode, the command is executed on the broker host via SSH.
|
|
198
209
|
*/
|
|
199
210
|
router.post('/restart', async (req, res) => {
|
|
200
211
|
try {
|
|
201
212
|
const bc = getBrokerConfig(req);
|
|
213
|
+
if (bc.mode === 'remote' && bc.ssh && bc.ssh.host) {
|
|
214
|
+
const cmd = bc.restartCmd || 'sudo systemctl restart mosquitto';
|
|
215
|
+
const result = await sshDeploy.runCommand(bc.ssh, cmd);
|
|
216
|
+
return res.json({ ok: true, ...result });
|
|
217
|
+
}
|
|
202
218
|
const result = await mosquittoConf.restart(bc);
|
|
203
219
|
res.json({ ok: true, stdout: result.stdout, stderr: result.stderr });
|
|
204
220
|
} catch (err) {
|
|
@@ -469,7 +485,7 @@ router.get('/ca/certs', async (req, res) => {
|
|
|
469
485
|
const db = req.app.locals.db;
|
|
470
486
|
if (!db) return res.json({ certs: [] });
|
|
471
487
|
const certs = db.query(
|
|
472
|
-
(doc) => doc._id && doc._id.startsWith('broker
|
|
488
|
+
(doc) => doc._id && doc._id.startsWith('she/broker/cert/'),
|
|
473
489
|
(doc) => doc,
|
|
474
490
|
);
|
|
475
491
|
res.json({ certs });
|
|
@@ -488,7 +504,7 @@ router.post('/ca/certs', async (req, res) => {
|
|
|
488
504
|
// Store metadata in sheDB
|
|
489
505
|
const db = req.app.locals.db;
|
|
490
506
|
if (db) {
|
|
491
|
-
db.set(`broker
|
|
507
|
+
db.set(`she/broker/cert/${result.serial}`, {
|
|
492
508
|
cn: result.cn,
|
|
493
509
|
serial: result.serial,
|
|
494
510
|
fingerprint: result.fingerprint,
|
|
@@ -522,7 +538,7 @@ router.delete('/ca/certs/:serial', async (req, res) => {
|
|
|
522
538
|
const bc = getBrokerConfig(req);
|
|
523
539
|
const { serial } = req.params;
|
|
524
540
|
const db = req.app.locals.db;
|
|
525
|
-
const meta = db ? db.get(`broker
|
|
541
|
+
const meta = db ? db.get(`she/broker/cert/${serial}`) : null;
|
|
526
542
|
if (!meta) return res.status(404).json({ error: 'cert not found' });
|
|
527
543
|
|
|
528
544
|
// Find the cert file
|
|
@@ -534,7 +550,7 @@ router.delete('/ca/certs/:serial', async (req, res) => {
|
|
|
534
550
|
// Collect all other revoked certs
|
|
535
551
|
if (db) {
|
|
536
552
|
const allCerts = db.query(
|
|
537
|
-
(doc) => doc._id && doc._id.startsWith('broker
|
|
553
|
+
(doc) => doc._id && doc._id.startsWith('she/broker/cert/') && doc.revoked && doc._id !== `she/broker/cert/${serial}`,
|
|
538
554
|
(doc) => doc,
|
|
539
555
|
);
|
|
540
556
|
for (const c of allCerts) {
|
|
@@ -547,7 +563,7 @@ router.delete('/ca/certs/:serial', async (req, res) => {
|
|
|
547
563
|
|
|
548
564
|
// Mark revoked in sheDB
|
|
549
565
|
if (db) {
|
|
550
|
-
db.extend(`broker
|
|
566
|
+
db.extend(`she/broker/cert/${serial}`, { revoked: true, revokedAt: new Date().toISOString() });
|
|
551
567
|
}
|
|
552
568
|
|
|
553
569
|
res.json({ ok: true });
|
|
@@ -563,7 +579,7 @@ router.get('/ca/certs/:serial/download', async (req, res) => {
|
|
|
563
579
|
const { serial } = req.params;
|
|
564
580
|
const { type = 'p12' } = req.query;
|
|
565
581
|
const db = req.app.locals.db;
|
|
566
|
-
const meta = db ? db.get(`broker
|
|
582
|
+
const meta = db ? db.get(`she/broker/cert/${serial}`) : null;
|
|
567
583
|
if (!meta) return res.status(404).json({ error: 'cert not found' });
|
|
568
584
|
|
|
569
585
|
const paths = ca.clientCertPaths(bc, meta.cn);
|
|
@@ -650,7 +666,7 @@ module.exports = { router };
|
|
|
650
666
|
router.post('/ssh/keygen', async (req, res) => {
|
|
651
667
|
try {
|
|
652
668
|
const bc = getBrokerConfig(req);
|
|
653
|
-
const identityFile = (bc.ssh && bc.ssh.identityFile) ||
|
|
669
|
+
const identityFile = (bc.ssh && bc.ssh.identityFile) || DEFAULT_SSH_KEY;
|
|
654
670
|
const publicKey = await sshDeploy.generateKeypair(identityFile);
|
|
655
671
|
res.json({ ok: true, publicKey });
|
|
656
672
|
} catch (err) {
|
|
@@ -684,13 +700,17 @@ router.post('/wizard/probe', (req, res) => {
|
|
|
684
700
|
/**
|
|
685
701
|
* POST /she/broker/wizard/bootstrap
|
|
686
702
|
* Full bootstrap flow:
|
|
687
|
-
* 1. Generate dynamic-security.json
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
703
|
+
* 1. Generate dynamic-security.json via mosquitto_ctrl
|
|
704
|
+
* - Remote mode: run mosquitto_ctrl on the broker host via SSH
|
|
705
|
+
* - Local mode: run mosquitto_ctrl locally
|
|
706
|
+
* 2. Ensure plugin line exists in mosquitto.conf
|
|
707
|
+
* 3. Return credentials (store in config.json via /she/config)
|
|
708
|
+
*
|
|
709
|
+
* Note: mosquitto_ctrl is part of the mosquitto package and must be installed
|
|
710
|
+
* on the same host as the broker. It cannot be used to manage a remote broker,
|
|
711
|
+
* which is why we invoke it via SSH in remote mode.
|
|
691
712
|
*
|
|
692
713
|
* Body: { adminUsername?, adminPassword?, configDir? }
|
|
693
|
-
* The generated password is returned in the response (store in config.json via /she/config).
|
|
694
714
|
*/
|
|
695
715
|
router.post('/wizard/bootstrap', async (req, res) => {
|
|
696
716
|
try {
|
|
@@ -702,47 +722,53 @@ router.post('/wizard/bootstrap', async (req, res) => {
|
|
|
702
722
|
|
|
703
723
|
const username = req.body.adminUsername || 'she-admin';
|
|
704
724
|
const password = req.body.adminPassword || crypto.randomBytes(18).toString('base64url');
|
|
705
|
-
const configDir = req.body.configDir || bc.configDir || '/etc/mosquitto';
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const dynSecPath =
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
} catch {
|
|
721
|
-
/* ok */
|
|
722
|
-
}
|
|
725
|
+
const configDir = (req.body.configDir || bc.configDir || '/etc/mosquitto').replace(/\\/g, '/');
|
|
726
|
+
const isRemote = bc.mode === 'remote' && bc.ssh && bc.ssh.host;
|
|
727
|
+
|
|
728
|
+
const dynSecPath = `${configDir}/dynamic-security.json`;
|
|
729
|
+
const confFilePath = `${configDir}/mosquitto.conf`;
|
|
730
|
+
|
|
731
|
+
if (isRemote) {
|
|
732
|
+
// mosquitto_ctrl must run on the broker host — invoke it via SSH.
|
|
733
|
+
try {
|
|
734
|
+
await sshDeploy.runCommand(bc.ssh, `mosquitto_ctrl dynsec init "${dynSecPath}" "${username}" "${password}"`);
|
|
735
|
+
} catch (err) {
|
|
736
|
+
return res.status(500).json({
|
|
737
|
+
error: `mosquitto_ctrl failed on remote host: ${err.message}. Ensure mosquitto is installed on the remote broker host.`,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
723
740
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
741
|
+
// Read the remote mosquitto.conf, parse, and add the plugin line if missing.
|
|
742
|
+
let remoteConfRaw = '';
|
|
743
|
+
try {
|
|
744
|
+
remoteConfRaw = await sshDeploy.readRemoteFile(bc.ssh, confFilePath);
|
|
745
|
+
} catch {
|
|
746
|
+
// File may not exist yet — start from an empty config
|
|
747
|
+
}
|
|
748
|
+
const parsed = mosquittoConf.parseText(remoteConfRaw);
|
|
749
|
+
if (!parsed.managed.plugin || !String(parsed.managed.plugin).includes('mosquitto_dynamic_security')) {
|
|
750
|
+
parsed.managed.plugin = 'mosquitto_dynamic_security.so';
|
|
751
|
+
parsed.managed.plugin_opt_dynsec_config_file = dynSecPath;
|
|
752
|
+
const content = mosquittoConf.serialise(parsed);
|
|
753
|
+
await sshDeploy.uploadContent(bc.ssh, content, confFilePath);
|
|
754
|
+
}
|
|
727
755
|
} else {
|
|
756
|
+
// Local mode: run mosquitto_ctrl on this host.
|
|
728
757
|
fs.mkdirSync(configDir, { recursive: true });
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const pluginLine = 'mosquitto_dynamic_security.so';
|
|
737
|
-
const pluginOptLine = dynSecPath;
|
|
758
|
+
try {
|
|
759
|
+
await execFileAsync('mosquitto_ctrl', ['dynsec', 'init', dynSecPath, username, password], { timeout: 10000 });
|
|
760
|
+
} catch (err) {
|
|
761
|
+
return res.status(500).json({
|
|
762
|
+
error: `mosquitto_ctrl failed: ${err.message}. Ensure mosquitto is installed on this host.`,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
738
765
|
|
|
739
|
-
|
|
740
|
-
parsed
|
|
741
|
-
parsed.managed.
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
} else {
|
|
766
|
+
// Ensure plugin line exists in local mosquitto.conf
|
|
767
|
+
const parsed = mosquittoConf.parse(confFilePath);
|
|
768
|
+
if (!parsed.managed.plugin || !String(parsed.managed.plugin).includes('mosquitto_dynamic_security')) {
|
|
769
|
+
parsed.managed.plugin = 'mosquitto_dynamic_security.so';
|
|
770
|
+
parsed.managed.plugin_opt_dynsec_config_file = dynSecPath;
|
|
771
|
+
const content = mosquittoConf.serialise(parsed);
|
|
746
772
|
mosquittoConf.write(confFilePath, content);
|
|
747
773
|
}
|
|
748
774
|
}
|