@goodandready/dsh-remote-workspace 0.3.9 → 0.3.10
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/lib/index.js +1 -0
- package/lib/routes/docker.js +9 -9
- package/lib/routes/env.js +7 -7
- package/lib/routes/files.js +11 -11
- package/lib/routes/helpers.js +99 -11
- package/lib/routes/profiles.js +18 -18
- package/lib/routes/sync.js +5 -5
- package/lib/routes/terminal.js +10 -10
- package/lib/routes/tunnels.js +7 -7
- package/lib/routes.js +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
package/lib/routes/docker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
|
|
1
|
+
import { writeJson, readBody, isTrustedSettingsRequest, authorizeRequest } from './helpers.js';
|
|
2
2
|
|
|
3
3
|
export function registerDockerRoutes(sctx, webServer, { dockerService, healthService, store }) {
|
|
4
4
|
// 21. GET /dsh-remote-workspace/health
|
|
@@ -7,7 +7,7 @@ export function registerDockerRoutes(sctx, webServer, { dockerService, healthSer
|
|
|
7
7
|
path: '/dsh-remote-workspace/health',
|
|
8
8
|
handler: async (req, res) => {
|
|
9
9
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
10
|
-
if (!
|
|
10
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
11
11
|
try {
|
|
12
12
|
const url = new URL(req.url, 'http://localhost');
|
|
13
13
|
const profileId = url.searchParams.get('profileId');
|
|
@@ -17,7 +17,7 @@ export function registerDockerRoutes(sctx, webServer, { dockerService, healthSer
|
|
|
17
17
|
const health = await healthService.getHealth(profile);
|
|
18
18
|
writeJson(res, 200, { ok: true, health });
|
|
19
19
|
} catch (err) {
|
|
20
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
20
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
}), 'dsh-remote-workspace: /health');
|
|
@@ -28,7 +28,7 @@ export function registerDockerRoutes(sctx, webServer, { dockerService, healthSer
|
|
|
28
28
|
path: '/dsh-remote-workspace/docker/list',
|
|
29
29
|
handler: async (req, res) => {
|
|
30
30
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
31
|
-
if (!
|
|
31
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
32
32
|
try {
|
|
33
33
|
const url = new URL(req.url, 'http://localhost');
|
|
34
34
|
const profileId = url.searchParams.get('profileId');
|
|
@@ -38,7 +38,7 @@ export function registerDockerRoutes(sctx, webServer, { dockerService, healthSer
|
|
|
38
38
|
const result = await dockerService.listContainers(profile, true);
|
|
39
39
|
writeJson(res, 200, { ok: true, ...result });
|
|
40
40
|
} catch (err) {
|
|
41
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
41
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
}), 'dsh-remote-workspace: /docker/list');
|
|
@@ -49,7 +49,7 @@ export function registerDockerRoutes(sctx, webServer, { dockerService, healthSer
|
|
|
49
49
|
path: '/dsh-remote-workspace/docker/action',
|
|
50
50
|
handler: async (req, res) => {
|
|
51
51
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
52
|
-
if (!
|
|
52
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
53
53
|
try {
|
|
54
54
|
const { profileId, containerId, action } = await readBody(req);
|
|
55
55
|
const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
|
|
@@ -59,7 +59,7 @@ export function registerDockerRoutes(sctx, webServer, { dockerService, healthSer
|
|
|
59
59
|
const result = await dockerService.containerAction(profile, containerId, action);
|
|
60
60
|
writeJson(res, 200, { ok: result.success, ...result });
|
|
61
61
|
} catch (err) {
|
|
62
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
62
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
}), 'dsh-remote-workspace: /docker/action');
|
|
@@ -70,7 +70,7 @@ export function registerDockerRoutes(sctx, webServer, { dockerService, healthSer
|
|
|
70
70
|
path: '/dsh-remote-workspace/docker/logs',
|
|
71
71
|
handler: async (req, res) => {
|
|
72
72
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
73
|
-
if (!
|
|
73
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
74
74
|
try {
|
|
75
75
|
const { profileId, containerId, tail } = await readBody(req);
|
|
76
76
|
const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
|
|
@@ -80,7 +80,7 @@ export function registerDockerRoutes(sctx, webServer, { dockerService, healthSer
|
|
|
80
80
|
const result = await dockerService.containerLogs(profile, containerId, tail || 100);
|
|
81
81
|
writeJson(res, 200, { ok: result.success, ...result });
|
|
82
82
|
} catch (err) {
|
|
83
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
83
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
}), 'dsh-remote-workspace: /docker/logs');
|
package/lib/routes/env.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
|
|
1
|
+
import { writeJson, readBody, isTrustedSettingsRequest, authorizeRequest } from './helpers.js';
|
|
2
2
|
|
|
3
3
|
export function registerEnvRoutes(sctx, webServer, { envService, diagnoseService, sshService, store }) {
|
|
4
4
|
// 10. POST /dsh-remote-workspace/env/view
|
|
@@ -7,7 +7,7 @@ export function registerEnvRoutes(sctx, webServer, { envService, diagnoseService
|
|
|
7
7
|
path: '/dsh-remote-workspace/env/view',
|
|
8
8
|
handler: async (req, res) => {
|
|
9
9
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
10
|
-
if (!
|
|
10
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
11
11
|
try {
|
|
12
12
|
const { profileId, remotePath } = await readBody(req);
|
|
13
13
|
const profile = store.getProfile(profileId);
|
|
@@ -15,7 +15,7 @@ export function registerEnvRoutes(sctx, webServer, { envService, diagnoseService
|
|
|
15
15
|
const result = await envService.readRemoteEnv(sshService, profile, remotePath);
|
|
16
16
|
writeJson(res, 200, { ok: true, ...result });
|
|
17
17
|
} catch (err) {
|
|
18
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
18
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
}), 'dsh-remote-workspace: /env/view');
|
|
@@ -26,7 +26,7 @@ export function registerEnvRoutes(sctx, webServer, { envService, diagnoseService
|
|
|
26
26
|
path: '/dsh-remote-workspace/env/save',
|
|
27
27
|
handler: async (req, res) => {
|
|
28
28
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
29
|
-
if (!
|
|
29
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
30
30
|
try {
|
|
31
31
|
const { profileId, remotePath, content } = await readBody(req);
|
|
32
32
|
const profile = store.getProfile(profileId);
|
|
@@ -34,7 +34,7 @@ export function registerEnvRoutes(sctx, webServer, { envService, diagnoseService
|
|
|
34
34
|
const result = await envService.saveRemoteEnv(sshService, profile, remotePath, content);
|
|
35
35
|
writeJson(res, 200, { ok: true, ...result });
|
|
36
36
|
} catch (err) {
|
|
37
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
37
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}), 'dsh-remote-workspace: /env/save');
|
|
@@ -45,7 +45,7 @@ export function registerEnvRoutes(sctx, webServer, { envService, diagnoseService
|
|
|
45
45
|
path: '/dsh-remote-workspace/diagnose',
|
|
46
46
|
handler: async (req, res) => {
|
|
47
47
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
48
|
-
if (!
|
|
48
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
49
49
|
try {
|
|
50
50
|
const { profileId, checks } = await readBody(req);
|
|
51
51
|
const profile = store.getProfile(profileId);
|
|
@@ -53,7 +53,7 @@ export function registerEnvRoutes(sctx, webServer, { envService, diagnoseService
|
|
|
53
53
|
const results = await diagnoseService.diagnose(sshService, profile, checks);
|
|
54
54
|
writeJson(res, 200, { ok: true, results });
|
|
55
55
|
} catch (err) {
|
|
56
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
56
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
}), 'dsh-remote-workspace: /diagnose');
|
package/lib/routes/files.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
|
|
1
|
+
import { writeJson, readBody, isTrustedSettingsRequest, authorizeRequest } from './helpers.js';
|
|
2
2
|
import { contentDisposition, pipeWithProgress, MAX_BYTES } from '../file-transfer.js';
|
|
3
3
|
|
|
4
4
|
|
|
@@ -27,7 +27,7 @@ export function registerFileRoutes(sctx, webServer, { remoteFs, store, sshServic
|
|
|
27
27
|
path: '/dsh-remote-workspace/browse',
|
|
28
28
|
handler: async (req, res) => {
|
|
29
29
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
30
|
-
if (!
|
|
30
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
31
31
|
try {
|
|
32
32
|
const body = await readBody(req);
|
|
33
33
|
let profile = null;
|
|
@@ -48,7 +48,7 @@ export function registerFileRoutes(sctx, webServer, { remoteFs, store, sshServic
|
|
|
48
48
|
const entries = await remoteFs.listDir(profile, targetPath);
|
|
49
49
|
writeJson(res, 200, { ok: true, entries, items: entries, currentPath: targetPath });
|
|
50
50
|
} catch (err) {
|
|
51
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
51
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
}), 'dsh-remote-workspace: /browse');
|
|
@@ -59,7 +59,7 @@ export function registerFileRoutes(sctx, webServer, { remoteFs, store, sshServic
|
|
|
59
59
|
path: '/dsh-remote-workspace/file/view',
|
|
60
60
|
handler: async (req, res) => {
|
|
61
61
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
62
|
-
if (!
|
|
62
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
63
63
|
try {
|
|
64
64
|
const { profileId, filePath } = await readBody(req);
|
|
65
65
|
const profile = store.getProfile(profileId);
|
|
@@ -71,7 +71,7 @@ export function registerFileRoutes(sctx, webServer, { remoteFs, store, sshServic
|
|
|
71
71
|
const buf = await remoteFs.readFile(profile, filePath);
|
|
72
72
|
writeJson(res, 200, { ok: true, content: buf.toString('utf8'), size: stat.size });
|
|
73
73
|
} catch (err) {
|
|
74
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
74
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
}), 'dsh-remote-workspace: /file/view');
|
|
@@ -82,7 +82,7 @@ export function registerFileRoutes(sctx, webServer, { remoteFs, store, sshServic
|
|
|
82
82
|
path: '/dsh-remote-workspace/file/save',
|
|
83
83
|
handler: async (req, res) => {
|
|
84
84
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
85
|
-
if (!
|
|
85
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
86
86
|
try {
|
|
87
87
|
const { profileId, filePath, content } = await readBody(req);
|
|
88
88
|
const profile = store.getProfile(profileId);
|
|
@@ -90,7 +90,7 @@ export function registerFileRoutes(sctx, webServer, { remoteFs, store, sshServic
|
|
|
90
90
|
await remoteFs.writeFile(profile, filePath, Buffer.from(content, 'utf8'));
|
|
91
91
|
writeJson(res, 200, { ok: true });
|
|
92
92
|
} catch (err) {
|
|
93
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
93
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
}), 'dsh-remote-workspace: /file/save');
|
|
@@ -100,7 +100,7 @@ export function registerFileRoutes(sctx, webServer, { remoteFs, store, sshServic
|
|
|
100
100
|
path: '/dsh-remote-workspace/file/download',
|
|
101
101
|
handler: async (req, res) => {
|
|
102
102
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
103
|
-
if (!
|
|
103
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
104
104
|
try {
|
|
105
105
|
const { profile, filePath } = transferTarget(req, store);
|
|
106
106
|
const stat = await remoteFs.stat(profile, filePath);
|
|
@@ -128,7 +128,7 @@ export function registerFileRoutes(sctx, webServer, { remoteFs, store, sshServic
|
|
|
128
128
|
path: '/dsh-remote-workspace/file/upload',
|
|
129
129
|
handler: async (req, res) => {
|
|
130
130
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
131
|
-
if (!
|
|
131
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
132
132
|
let upload = null;
|
|
133
133
|
try {
|
|
134
134
|
const { profile, filePath } = transferTarget(req, store);
|
|
@@ -156,7 +156,7 @@ export function registerFileRoutes(sctx, webServer, { remoteFs, store, sshServic
|
|
|
156
156
|
path: '/dsh-remote-workspace/transfer',
|
|
157
157
|
handler: async (req, res) => {
|
|
158
158
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
159
|
-
if (!
|
|
159
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
160
160
|
try {
|
|
161
161
|
const { sourceProfileId, sourcePath, destProfileId, destPath } = await readBody(req);
|
|
162
162
|
const srcProfile = sourceProfileId ? store.getProfile(sourceProfileId) : store.getActiveProfile();
|
|
@@ -169,7 +169,7 @@ export function registerFileRoutes(sctx, webServer, { remoteFs, store, sshServic
|
|
|
169
169
|
const result = await sshService.transferFileBetweenHosts(srcProfile, sourcePath, destProfile, destPath);
|
|
170
170
|
writeJson(res, 200, { ok: result.success, ...result });
|
|
171
171
|
} catch (err) {
|
|
172
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
172
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
}), 'dsh-remote-workspace: /transfer');
|
package/lib/routes/helpers.js
CHANGED
|
@@ -10,18 +10,54 @@ export function writeJson(res, statusCode, body) {
|
|
|
10
10
|
res.end(JSON.stringify(body));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export
|
|
13
|
+
export const MAX_BODY_BYTES = 5 * 1024 * 1024; // 5 MiB
|
|
14
|
+
|
|
15
|
+
export function readBody(req, maxBytes = MAX_BODY_BYTES) {
|
|
14
16
|
return new Promise((resolve, reject) => {
|
|
17
|
+
const rawCl = req?.headers?.['content-length'];
|
|
18
|
+
const contentLength = rawCl !== undefined ? parseInt(Array.isArray(rawCl) ? rawCl[0] : rawCl, 10) : NaN;
|
|
19
|
+
if (!isNaN(contentLength) && contentLength > maxBytes) {
|
|
20
|
+
if (typeof req.destroy === 'function') req.destroy();
|
|
21
|
+
const error = new Error('Payload too large');
|
|
22
|
+
error.statusCode = 413;
|
|
23
|
+
return reject(error);
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
let body = '';
|
|
16
|
-
|
|
27
|
+
let bytesReceived = 0;
|
|
28
|
+
let exceeded = false;
|
|
29
|
+
|
|
30
|
+
function onData(chunk) {
|
|
31
|
+
if (exceeded) return;
|
|
32
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
33
|
+
bytesReceived += buffer.length;
|
|
34
|
+
if (bytesReceived > maxBytes) {
|
|
35
|
+
exceeded = true;
|
|
36
|
+
if (typeof req.removeListener === 'function') {
|
|
37
|
+
req.removeListener('data', onData);
|
|
38
|
+
} else if (typeof req.off === 'function') {
|
|
39
|
+
req.off('data', onData);
|
|
40
|
+
}
|
|
41
|
+
if (typeof req.destroy === 'function') req.destroy();
|
|
42
|
+
const error = new Error('Payload too large');
|
|
43
|
+
error.statusCode = 413;
|
|
44
|
+
return reject(error);
|
|
45
|
+
}
|
|
46
|
+
body += buffer.toString('utf8');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
req.on('data', onData);
|
|
17
50
|
req.on('end', () => {
|
|
51
|
+
if (exceeded) return;
|
|
18
52
|
try {
|
|
19
53
|
resolve(body ? JSON.parse(body) : {});
|
|
20
54
|
} catch (err) {
|
|
21
55
|
reject(err);
|
|
22
56
|
}
|
|
23
57
|
});
|
|
24
|
-
req.on('error',
|
|
58
|
+
req.on('error', (err) => {
|
|
59
|
+
if (!exceeded) reject(err);
|
|
60
|
+
});
|
|
25
61
|
});
|
|
26
62
|
}
|
|
27
63
|
|
|
@@ -39,16 +75,24 @@ export function parseCookies(cookieHeader) {
|
|
|
39
75
|
return cookies;
|
|
40
76
|
}
|
|
41
77
|
|
|
78
|
+
export function isLoopbackAddress(address) {
|
|
79
|
+
if (!address || typeof address !== 'string') return false;
|
|
80
|
+
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1' || address.startsWith('127.');
|
|
81
|
+
}
|
|
82
|
+
|
|
42
83
|
export function isTrustedSettingsRequest(req) {
|
|
43
|
-
const
|
|
84
|
+
const ip = req?.socket?.remoteAddress || '';
|
|
85
|
+
const isLoopback = isLoopbackAddress(ip);
|
|
86
|
+
|
|
87
|
+
const secFetch = req?.headers?.['sec-fetch-site'];
|
|
44
88
|
// Fail-closed: unconditionally reject cross-site and cross-origin requests
|
|
45
89
|
if (secFetch && secFetch !== 'same-origin') {
|
|
46
90
|
return false;
|
|
47
91
|
}
|
|
48
92
|
|
|
49
93
|
// If origin is present, ensure it matches the request host
|
|
50
|
-
const origin = req
|
|
51
|
-
const host = req
|
|
94
|
+
const origin = req?.headers?.['origin'];
|
|
95
|
+
const host = req?.headers?.['host'];
|
|
52
96
|
if (origin && host) {
|
|
53
97
|
try {
|
|
54
98
|
const url = new URL(origin);
|
|
@@ -60,8 +104,8 @@ export function isTrustedSettingsRequest(req) {
|
|
|
60
104
|
}
|
|
61
105
|
}
|
|
62
106
|
|
|
63
|
-
const rawAuth = req
|
|
64
|
-
const cookieHeader = req
|
|
107
|
+
const rawAuth = req?.headers?.['authorization'];
|
|
108
|
+
const cookieHeader = req?.headers?.['cookie'];
|
|
65
109
|
const expectedToken = process.env.DSH_AUTH_TOKEN || process.env.DSH_TOKEN;
|
|
66
110
|
|
|
67
111
|
if (expectedToken) {
|
|
@@ -74,9 +118,53 @@ export function isTrustedSettingsRequest(req) {
|
|
|
74
118
|
const token = cookies['token'] || cookies['dsh_token'];
|
|
75
119
|
if (token && token === expectedToken) return true;
|
|
76
120
|
}
|
|
121
|
+
return false;
|
|
77
122
|
}
|
|
123
|
+
|
|
124
|
+
// Non-loopback requests without valid token must not pass simply on Sec-Fetch-Site: same-origin
|
|
125
|
+
if (!isLoopback) return false;
|
|
78
126
|
if (secFetch === 'same-origin') return true;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getConnection(ctx) {
|
|
131
|
+
try {
|
|
132
|
+
return ctx?.reflect?.get ? ctx.reflect.get('connection') : (ctx?.get ? ctx.get('connection') : Reflect.get(ctx || {}, 'connection'));
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function authorizeRequest(sctx, req, res) {
|
|
139
|
+
let connection;
|
|
140
|
+
try {
|
|
141
|
+
connection = getConnection(sctx);
|
|
142
|
+
} catch {
|
|
143
|
+
writeJson(res, 503, { ok: false, error: 'DSH browser authentication is unavailable' });
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!connection || typeof connection.requestRejection !== 'function') {
|
|
148
|
+
writeJson(res, 503, { ok: false, error: 'DSH browser authentication is unavailable' });
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let rejection;
|
|
153
|
+
try {
|
|
154
|
+
rejection = connection.requestRejection(req);
|
|
155
|
+
} catch {
|
|
156
|
+
writeJson(res, 503, { ok: false, error: 'DSH browser authentication is unavailable' });
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (rejection !== undefined) {
|
|
161
|
+
const status = rejection === 401 ? 401 : 403;
|
|
162
|
+
writeJson(res, status, {
|
|
163
|
+
ok: false,
|
|
164
|
+
error: status === 401 ? 'DSH browser authentication is required' : 'Forbidden'
|
|
165
|
+
});
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return true;
|
|
82
170
|
}
|
package/lib/routes/profiles.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
|
|
1
|
+
import { writeJson, readBody, isTrustedSettingsRequest, authorizeRequest } from './helpers.js';
|
|
2
2
|
import { SshConfigParser } from '../ssh-config-parser.js';
|
|
3
3
|
import { testProfileGroup } from '../profile-groups.js';
|
|
4
4
|
import { filterClusterProfiles, runCluster } from '../cluster-service.js';
|
|
@@ -19,7 +19,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
19
19
|
path: '/dsh-remote-workspace/state',
|
|
20
20
|
handler: async (req, res) => {
|
|
21
21
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
22
|
-
if (!
|
|
22
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
23
23
|
try {
|
|
24
24
|
const profiles = store.getProfiles();
|
|
25
25
|
const activeId = store.getActiveId() || (profiles[0]?.id || '');
|
|
@@ -31,7 +31,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
31
31
|
const terminalFontFamily = store.getTerminalFont ? store.getTerminalFont() : '';
|
|
32
32
|
writeJson(res, 200, { ok: true, activeId, profiles: sanitized, tunnels, autoSync, alerts, authPrompts, terminalFontFamily });
|
|
33
33
|
} catch (err) {
|
|
34
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
34
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}), 'dsh-remote-workspace: /state');
|
|
@@ -42,7 +42,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
42
42
|
path: '/dsh-remote-workspace/profiles/save',
|
|
43
43
|
handler: async (req, res) => {
|
|
44
44
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
45
|
-
if (!
|
|
45
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
46
46
|
try {
|
|
47
47
|
const profile = await readBody(req);
|
|
48
48
|
if (!profile.id || !profile.host) {
|
|
@@ -53,7 +53,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
53
53
|
const sanitized = store.vault ? store.vault.sanitizeProfile(profile) : maskProfile(profile);
|
|
54
54
|
writeJson(res, 200, { ok: true, profile: sanitized });
|
|
55
55
|
} catch (err) {
|
|
56
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
56
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
}), 'dsh-remote-workspace: /profiles/save');
|
|
@@ -64,7 +64,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
64
64
|
path: '/dsh-remote-workspace/profiles/delete',
|
|
65
65
|
handler: async (req, res) => {
|
|
66
66
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
67
|
-
if (!
|
|
67
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
68
68
|
try {
|
|
69
69
|
const { id } = await readBody(req);
|
|
70
70
|
if (!id) return writeJson(res, 400, { ok: false, error: 'ID is required' });
|
|
@@ -72,7 +72,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
72
72
|
await store.deleteProfile(id);
|
|
73
73
|
writeJson(res, 200, { ok: true });
|
|
74
74
|
} catch (err) {
|
|
75
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
75
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
}), 'dsh-remote-workspace: /profiles/delete');
|
|
@@ -83,14 +83,14 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
83
83
|
path: '/dsh-remote-workspace/profiles/active',
|
|
84
84
|
handler: async (req, res) => {
|
|
85
85
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
86
|
-
if (!
|
|
86
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
87
87
|
try {
|
|
88
88
|
const { id } = await readBody(req);
|
|
89
89
|
if (!id) return writeJson(res, 400, { ok: false, error: 'ID is required' });
|
|
90
90
|
await store.setActiveId(id);
|
|
91
91
|
writeJson(res, 200, { ok: true, activeId: id });
|
|
92
92
|
} catch (err) {
|
|
93
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
93
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
}), 'dsh-remote-workspace: /profiles/active');
|
|
@@ -101,7 +101,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
101
101
|
path: '/dsh-remote-workspace/profiles/import-ssh-config',
|
|
102
102
|
handler: async (req, res) => {
|
|
103
103
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
104
|
-
if (!
|
|
104
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
105
105
|
try {
|
|
106
106
|
const body = await readBody(req);
|
|
107
107
|
const content = typeof body.content === 'string' && body.content.trim()
|
|
@@ -119,7 +119,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
119
119
|
skipped: report.skipped
|
|
120
120
|
});
|
|
121
121
|
} catch (err) {
|
|
122
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
122
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
}), 'dsh-remote-workspace: /profiles/import-ssh-config');
|
|
@@ -130,7 +130,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
130
130
|
path: '/dsh-remote-workspace/auth/keyboard',
|
|
131
131
|
handler: async (req, res) => {
|
|
132
132
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
133
|
-
if (!
|
|
133
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
134
134
|
try {
|
|
135
135
|
const body = await readBody(req);
|
|
136
136
|
const answers = Array.isArray(body.answers) ? body.answers.slice(0, 20).map((item) => String(item ?? '').slice(0, 256)) : [];
|
|
@@ -138,7 +138,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
138
138
|
if (!accepted) return writeJson(res, 404, { ok: false, error: 'No authentication prompt is waiting' });
|
|
139
139
|
writeJson(res, 200, { ok: true });
|
|
140
140
|
} catch (err) {
|
|
141
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
141
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}), 'dsh-remote-workspace: /auth/keyboard');
|
|
@@ -149,7 +149,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
149
149
|
path: '/dsh-remote-workspace/profiles/test-group',
|
|
150
150
|
handler: async (req, res) => {
|
|
151
151
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
152
|
-
if (!
|
|
152
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
153
153
|
try {
|
|
154
154
|
const body = await readBody(req);
|
|
155
155
|
const ids = Array.isArray(body.ids) ? body.ids.map((item) => String(item)) : [];
|
|
@@ -157,7 +157,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
157
157
|
const results = await testProfileGroup(selected, (profile) => sshService.testConnection(profile));
|
|
158
158
|
writeJson(res, 200, { ok: true, results });
|
|
159
159
|
} catch (err) {
|
|
160
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
160
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
}), 'dsh-remote-workspace: /profiles/test-group');
|
|
@@ -168,7 +168,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
168
168
|
path: '/dsh-remote-workspace/cluster',
|
|
169
169
|
handler: async (req, res) => {
|
|
170
170
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
171
|
-
if (!
|
|
171
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
172
172
|
try {
|
|
173
173
|
const body = await readBody(req);
|
|
174
174
|
if (!body.command || typeof body.command !== 'string') return writeJson(res, 400, { ok: false, error: 'Command is required' });
|
|
@@ -183,7 +183,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
183
183
|
});
|
|
184
184
|
writeJson(res, 200, { ok: true, results });
|
|
185
185
|
} catch (err) {
|
|
186
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
186
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
}), 'dsh-remote-workspace: /cluster');
|
|
@@ -194,7 +194,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
194
194
|
path: '/dsh-remote-workspace/test',
|
|
195
195
|
handler: async (req, res) => {
|
|
196
196
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
197
|
-
if (!
|
|
197
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
198
198
|
try {
|
|
199
199
|
const profile = await readBody(req);
|
|
200
200
|
if (!profile.host) return writeJson(res, 400, { ok: false, error: 'Host is required' });
|
package/lib/routes/sync.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
|
|
1
|
+
import { writeJson, readBody, isTrustedSettingsRequest, authorizeRequest } from './helpers.js';
|
|
2
2
|
|
|
3
3
|
export function registerSyncRoutes(sctx, webServer, { mirrorSync, tarSyncService, watcherService, store }) {
|
|
4
4
|
// 17. POST /dsh-remote-workspace/sync
|
|
@@ -7,7 +7,7 @@ export function registerSyncRoutes(sctx, webServer, { mirrorSync, tarSyncService
|
|
|
7
7
|
path: '/dsh-remote-workspace/sync',
|
|
8
8
|
handler: async (req, res) => {
|
|
9
9
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
10
|
-
if (!
|
|
10
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
11
11
|
try {
|
|
12
12
|
const { profileId, direction, force, mode } = await readBody(req);
|
|
13
13
|
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
@@ -28,7 +28,7 @@ export function registerSyncRoutes(sctx, webServer, { mirrorSync, tarSyncService
|
|
|
28
28
|
: await mirrorSync.pull(profile, profile.remoteWorkspace, profile.localMirrorPath);
|
|
29
29
|
writeJson(res, 200, { ok: resSync.success, ...resSync });
|
|
30
30
|
} catch (err) {
|
|
31
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
31
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
}), 'dsh-remote-workspace: /sync');
|
|
@@ -39,13 +39,13 @@ export function registerSyncRoutes(sctx, webServer, { mirrorSync, tarSyncService
|
|
|
39
39
|
path: '/dsh-remote-workspace/watcher/toggle',
|
|
40
40
|
handler: async (req, res) => {
|
|
41
41
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
42
|
-
if (!
|
|
42
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
43
43
|
try {
|
|
44
44
|
const { enabled } = await readBody(req);
|
|
45
45
|
await store.setAutoSyncEnabled(Boolean(enabled));
|
|
46
46
|
writeJson(res, 200, { ok: true, autoSync: store.isAutoSyncEnabled() });
|
|
47
47
|
} catch (err) {
|
|
48
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
48
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
}), 'dsh-remote-workspace: /watcher/toggle');
|
package/lib/routes/terminal.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
|
|
1
|
+
import { writeJson, readBody, isTrustedSettingsRequest, authorizeRequest } from './helpers.js';
|
|
2
2
|
import { normalizeTerminalFont } from '../terminal-font.js';
|
|
3
3
|
|
|
4
4
|
export function registerTerminalRoutes(sctx, webServer, { sshService, store }) {
|
|
@@ -8,7 +8,7 @@ export function registerTerminalRoutes(sctx, webServer, { sshService, store }) {
|
|
|
8
8
|
path: '/dsh-remote-workspace/terminal/font',
|
|
9
9
|
handler: async (req, res) => {
|
|
10
10
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
11
|
-
if (!
|
|
11
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
12
12
|
try {
|
|
13
13
|
const body = await readBody(req);
|
|
14
14
|
const fontFamily = await store.setTerminalFont(body.fontFamily);
|
|
@@ -26,7 +26,7 @@ export function registerTerminalRoutes(sctx, webServer, { sshService, store }) {
|
|
|
26
26
|
path: '/dsh-remote-workspace/terminal/create',
|
|
27
27
|
handler: async (req, res) => {
|
|
28
28
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
29
|
-
if (!
|
|
29
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
30
30
|
try {
|
|
31
31
|
const { profileId, cols, rows } = await readBody(req);
|
|
32
32
|
const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
|
|
@@ -34,7 +34,7 @@ export function registerTerminalRoutes(sctx, webServer, { sshService, store }) {
|
|
|
34
34
|
const session = await sshService.createTerminalSession(profile, { cols: cols || 80, rows: rows || 24 });
|
|
35
35
|
writeJson(res, 200, { ok: true, sessionId: session.id });
|
|
36
36
|
} catch (err) {
|
|
37
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
37
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}), 'dsh-remote-workspace: /terminal/create');
|
|
@@ -45,7 +45,7 @@ export function registerTerminalRoutes(sctx, webServer, { sshService, store }) {
|
|
|
45
45
|
path: '/dsh-remote-workspace/terminal/input',
|
|
46
46
|
handler: async (req, res) => {
|
|
47
47
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
48
|
-
if (!
|
|
48
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
49
49
|
try {
|
|
50
50
|
const { sessionId, data, resize } = await readBody(req);
|
|
51
51
|
const session = sshService.getTerminalSession(sessionId);
|
|
@@ -58,7 +58,7 @@ export function registerTerminalRoutes(sctx, webServer, { sshService, store }) {
|
|
|
58
58
|
}
|
|
59
59
|
writeJson(res, 200, { ok: true });
|
|
60
60
|
} catch (err) {
|
|
61
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
61
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
}), 'dsh-remote-workspace: /terminal/input');
|
|
@@ -69,7 +69,7 @@ export function registerTerminalRoutes(sctx, webServer, { sshService, store }) {
|
|
|
69
69
|
path: '/dsh-remote-workspace/terminal/stream',
|
|
70
70
|
handler: async (req, res) => {
|
|
71
71
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
72
|
-
if (!
|
|
72
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
73
73
|
try {
|
|
74
74
|
const url = new URL(req.url, 'http://localhost');
|
|
75
75
|
const sessionId = url.searchParams.get('sessionId');
|
|
@@ -99,7 +99,7 @@ export function registerTerminalRoutes(sctx, webServer, { sshService, store }) {
|
|
|
99
99
|
session.subscribers.delete(sub);
|
|
100
100
|
});
|
|
101
101
|
} catch (err) {
|
|
102
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
102
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
}), 'dsh-remote-workspace: /terminal/stream');
|
|
@@ -110,13 +110,13 @@ export function registerTerminalRoutes(sctx, webServer, { sshService, store }) {
|
|
|
110
110
|
path: '/dsh-remote-workspace/terminal/close',
|
|
111
111
|
handler: async (req, res) => {
|
|
112
112
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
113
|
-
if (!
|
|
113
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
114
114
|
try {
|
|
115
115
|
const { sessionId } = await readBody(req);
|
|
116
116
|
sshService.closeTerminalSession(sessionId);
|
|
117
117
|
writeJson(res, 200, { ok: true });
|
|
118
118
|
} catch (err) {
|
|
119
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
119
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
}), 'dsh-remote-workspace: /terminal/close');
|
package/lib/routes/tunnels.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
|
|
1
|
+
import { writeJson, readBody, isTrustedSettingsRequest, authorizeRequest } from './helpers.js';
|
|
2
2
|
|
|
3
3
|
export function registerTunnelRoutes(sctx, webServer, { tunnelService, store }) {
|
|
4
4
|
// 18. POST /dsh-remote-workspace/tunnels/start
|
|
@@ -7,7 +7,7 @@ export function registerTunnelRoutes(sctx, webServer, { tunnelService, store })
|
|
|
7
7
|
path: '/dsh-remote-workspace/tunnels/start',
|
|
8
8
|
handler: async (req, res) => {
|
|
9
9
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
10
|
-
if (!
|
|
10
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
11
11
|
try {
|
|
12
12
|
const { profileId, localPort, targetHost, remotePort } = await readBody(req);
|
|
13
13
|
const profile = (profileId ? store.getProfile(profileId) : null) || store.getActiveProfile();
|
|
@@ -20,7 +20,7 @@ export function registerTunnelRoutes(sctx, webServer, { tunnelService, store })
|
|
|
20
20
|
const result = await tunnelService.startLocalTunnel(profile, lPort, targetHost || '127.0.0.1', rPort);
|
|
21
21
|
writeJson(res, 200, { ok: result.success, ...result });
|
|
22
22
|
} catch (err) {
|
|
23
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
23
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}), 'dsh-remote-workspace: /tunnels/start');
|
|
@@ -31,14 +31,14 @@ export function registerTunnelRoutes(sctx, webServer, { tunnelService, store })
|
|
|
31
31
|
path: '/dsh-remote-workspace/tunnels/stop',
|
|
32
32
|
handler: async (req, res) => {
|
|
33
33
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
34
|
-
if (!
|
|
34
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
35
35
|
try {
|
|
36
36
|
const { tunnelId } = await readBody(req);
|
|
37
37
|
if (!tunnelId) return writeJson(res, 400, { ok: false, error: 'tunnelId required' });
|
|
38
38
|
const stopped = tunnelService.stopTunnel(tunnelId);
|
|
39
39
|
writeJson(res, 200, { ok: stopped });
|
|
40
40
|
} catch (err) {
|
|
41
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
41
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
}), 'dsh-remote-workspace: /tunnels/stop');
|
|
@@ -49,11 +49,11 @@ export function registerTunnelRoutes(sctx, webServer, { tunnelService, store })
|
|
|
49
49
|
path: '/dsh-remote-workspace/tunnels/telemetry',
|
|
50
50
|
handler: async (req, res) => {
|
|
51
51
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
52
|
-
if (!
|
|
52
|
+
if (!authorizeRequest(sctx, req, res)) return;
|
|
53
53
|
try {
|
|
54
54
|
writeJson(res, 200, { ok: true, tunnels: tunnelService.listActiveTunnels() });
|
|
55
55
|
} catch (err) {
|
|
56
|
-
writeJson(res, 500, { ok: false, error: err.message });
|
|
56
|
+
writeJson(res, err.statusCode || (err.name === 'SyntaxError' ? 400 : 500), { ok: false, error: err.message });
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
}), 'dsh-remote-workspace: /tunnels/telemetry');
|
package/lib/routes.js
CHANGED
|
@@ -8,7 +8,7 @@ import { registerTunnelRoutes } from './routes/tunnels.js';
|
|
|
8
8
|
import { registerDockerRoutes } from './routes/docker.js';
|
|
9
9
|
import { registerSyncRoutes } from './routes/sync.js';
|
|
10
10
|
|
|
11
|
-
export { isTrustedSettingsRequest } from './routes/helpers.js';
|
|
11
|
+
export { isTrustedSettingsRequest, authorizeRequest, readBody, MAX_BODY_BYTES } from './routes/helpers.js';
|
|
12
12
|
|
|
13
13
|
export function registerApiRoutes(
|
|
14
14
|
ctx,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-remote-workspace",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Enterprise Remote Workspace Plugin for DeepSeek Harness: SSH2 connection pool, SFTP operations, 3-way conflict-aware mirror sync, port forwarding, and native DSH UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|