dsh-remote 0.8.71 → 0.8.73
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/LICENSE +21 -21
- package/README.en.md +389 -389
- package/README.md +462 -462
- package/cordis.patch.yml +9 -9
- package/lib/audit.js +45 -0
- package/lib/binding.js +172 -172
- package/lib/client.js +4022 -3990
- package/lib/credential.js +263 -263
- package/lib/errors.js +141 -118
- package/lib/exec-job.js +235 -235
- package/lib/file-reference.js +457 -457
- package/lib/forwards.js +258 -258
- package/lib/hostkey.js +108 -108
- package/lib/http-transport.js +196 -196
- package/lib/ignore.js +202 -202
- package/lib/index.js +4550 -4548
- package/lib/instructions-dynamic.js +415 -415
- package/lib/openssh.js +879 -879
- package/lib/paths.js +252 -252
- package/lib/pool.js +963 -886
- package/lib/registry.js +296 -293
- package/lib/remote-fs.js +173 -173
- package/lib/remote-sandbox.js +183 -183
- package/lib/remote-shell-child.cjs +344 -305
- package/lib/remote-shell.js +542 -518
- package/lib/routes-fs.js +245 -245
- package/lib/routes-term.js +169 -169
- package/lib/search.js +235 -235
- package/lib/sftp-surface.js +119 -119
- package/lib/sshconfig.js +779 -705
- package/lib/sync.js +275 -275
- package/lib/tasks.js +95 -95
- package/lib/telemetry.js +213 -213
- package/lib/terminal.js +205 -205
- package/lib/update.js +472 -472
- package/lib/web-attach.js +850 -850
- package/package.json +100 -99
package/lib/routes-term.js
CHANGED
|
@@ -1,169 +1,169 @@
|
|
|
1
|
-
// dsh-remote — HTTP routes for the embedded terminal.
|
|
2
|
-
//
|
|
3
|
-
// Four small endpoints around one host-owned PTY session (see lib/terminal.js
|
|
4
|
-
// for why polling instead of a WebSocket):
|
|
5
|
-
//
|
|
6
|
-
// POST /dsh-remote/term/open {cols, rows} → {id}
|
|
7
|
-
// POST /dsh-remote/term/input {id, data} → {ok}
|
|
8
|
-
// GET /dsh-remote/term/output ?id&from=N → {chunks, next, lossy, closed}
|
|
9
|
-
// POST /dsh-remote/term/resize {id, cols, rows} → {ok}
|
|
10
|
-
// POST /dsh-remote/term/close {id} → {ok}
|
|
11
|
-
//
|
|
12
|
-
// Every route resolves the binding from the SESSION first (the same
|
|
13
|
-
// `resolveRequestBinding` the file routes use), so a terminal can only ever be
|
|
14
|
-
// opened on — and attached to — the machine that session is actually bound to.
|
|
15
|
-
// That is the same isolation rule that keeps one session from acting on
|
|
16
|
-
// another's host, applied to an interactive channel where a mistake would be
|
|
17
|
-
// even more visible.
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Build the terminal routes.
|
|
21
|
-
*
|
|
22
|
-
* @param {object} deps
|
|
23
|
-
* @param {Function} deps.sendJson
|
|
24
|
-
* @param {Function} deps.readBody
|
|
25
|
-
* @param {Function} deps.resolveRequestBinding - session-aware binding resolver.
|
|
26
|
-
* @param {object} deps.terminals - the manager from createTerminalManager().
|
|
27
|
-
* @param {Function} deps.machineKey - (binding) → a stable machine identity.
|
|
28
|
-
* @returns {Array<object>} route descriptors for webServer.register().
|
|
29
|
-
*/
|
|
30
|
-
export function createTerminalRoutes({ sendJson, readBody, resolveRequestBinding, terminals, machineKey }) {
|
|
31
|
-
/** Resolve the binding and derive the machine identity, or reply and return null. */
|
|
32
|
-
const bind = async (req, res, body = {}) => {
|
|
33
|
-
let b
|
|
34
|
-
try {
|
|
35
|
-
b = await resolveRequestBinding(req, body)
|
|
36
|
-
} catch (err) {
|
|
37
|
-
sendJson(res, (err && err.httpStatus) || 500, { ok: false, error: String((err && err.message) || err) })
|
|
38
|
-
return null
|
|
39
|
-
}
|
|
40
|
-
if (!b) return null
|
|
41
|
-
return { b, machine: machineKey(b) }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return [
|
|
45
|
-
{
|
|
46
|
-
kind: 'exact',
|
|
47
|
-
path: '/dsh-remote/term/open',
|
|
48
|
-
handler: async (req, res) => {
|
|
49
|
-
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
50
|
-
try {
|
|
51
|
-
const body = JSON.parse((await readBody(req)) || '{}')
|
|
52
|
-
const bound = await bind(req, res, body)
|
|
53
|
-
if (!bound) return
|
|
54
|
-
const r = await terminals.open({
|
|
55
|
-
pool: bound.b.pool,
|
|
56
|
-
machine: bound.machine,
|
|
57
|
-
cols: Number(body.cols) || 80,
|
|
58
|
-
rows: Number(body.rows) || 24,
|
|
59
|
-
})
|
|
60
|
-
if (!r.ok) return sendJson(res, 503, { ok: false, error: r.error })
|
|
61
|
-
return sendJson(res, 200, { ok: true, id: r.id })
|
|
62
|
-
} catch (err) {
|
|
63
|
-
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
kind: 'exact',
|
|
70
|
-
path: '/dsh-remote/term/input',
|
|
71
|
-
handler: async (req, res) => {
|
|
72
|
-
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
73
|
-
try {
|
|
74
|
-
const body = JSON.parse((await readBody(req)) || '{}')
|
|
75
|
-
const bound = await bind(req, res, body)
|
|
76
|
-
if (!bound) return
|
|
77
|
-
const { session, error } = terminals.owned(body.id, bound.machine)
|
|
78
|
-
if (error) return sendJson(res, 404, { ok: false, error })
|
|
79
|
-
const data = typeof body.data === 'string' ? body.data : ''
|
|
80
|
-
if (!data) return sendJson(res, 400, { ok: false, error: 'data is required' })
|
|
81
|
-
if (!session.write(data)) return sendJson(res, 409, { ok: false, error: 'session is closed' })
|
|
82
|
-
return sendJson(res, 200, { ok: true })
|
|
83
|
-
} catch (err) {
|
|
84
|
-
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
kind: 'exact',
|
|
91
|
-
path: '/dsh-remote/term/output',
|
|
92
|
-
handler: async (req, res) => {
|
|
93
|
-
if (req.method !== 'GET' && req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
94
|
-
try {
|
|
95
|
-
const q = new URL(req.url, 'http://localhost').searchParams
|
|
96
|
-
let body = {}
|
|
97
|
-
if (req.method === 'POST') body = JSON.parse((await readBody(req)) || '{}')
|
|
98
|
-
const bound = await bind(req, res, body)
|
|
99
|
-
if (!bound) return
|
|
100
|
-
const id = body.id || q.get('id') || ''
|
|
101
|
-
const from = Number(body.from != null ? body.from : q.get('from')) || 0
|
|
102
|
-
const { session, error } = terminals.owned(id, bound.machine)
|
|
103
|
-
if (error) return sendJson(res, 404, { ok: false, error })
|
|
104
|
-
const out = session.readFrom(from)
|
|
105
|
-
return sendJson(res, 200, { ok: true, ...out })
|
|
106
|
-
} catch (err) {
|
|
107
|
-
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
{
|
|
113
|
-
kind: 'exact',
|
|
114
|
-
path: '/dsh-remote/term/resize',
|
|
115
|
-
handler: async (req, res) => {
|
|
116
|
-
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
117
|
-
try {
|
|
118
|
-
const body = JSON.parse((await readBody(req)) || '{}')
|
|
119
|
-
const bound = await bind(req, res, body)
|
|
120
|
-
if (!bound) return
|
|
121
|
-
const { session, error } = terminals.owned(body.id, bound.machine)
|
|
122
|
-
if (error) return sendJson(res, 404, { ok: false, error })
|
|
123
|
-
session.resize(body.cols, body.rows)
|
|
124
|
-
return sendJson(res, 200, { ok: true, cols: session.cols, rows: session.rows })
|
|
125
|
-
} catch (err) {
|
|
126
|
-
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
{
|
|
132
|
-
kind: 'exact',
|
|
133
|
-
path: '/dsh-remote/term/close',
|
|
134
|
-
handler: async (req, res) => {
|
|
135
|
-
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
136
|
-
try {
|
|
137
|
-
const body = JSON.parse((await readBody(req)) || '{}')
|
|
138
|
-
const bound = await bind(req, res, body)
|
|
139
|
-
if (!bound) return
|
|
140
|
-
const r = terminals.close(body.id, bound.machine)
|
|
141
|
-
if (!r.ok) return sendJson(res, 404, { ok: false, error: r.error })
|
|
142
|
-
return sendJson(res, 200, { ok: true })
|
|
143
|
-
} catch (err) {
|
|
144
|
-
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
{
|
|
150
|
-
kind: 'exact',
|
|
151
|
-
path: '/dsh-remote/term/list',
|
|
152
|
-
handler: async (req, res) => {
|
|
153
|
-
if (req.method !== 'GET') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
154
|
-
try {
|
|
155
|
-
const bound = await bind(req, res, {})
|
|
156
|
-
if (!bound) return
|
|
157
|
-
// Only THIS machine's sessions: a listing that leaks other hosts'
|
|
158
|
-
// session ids would let a caller probe them (ids are guessable UUIDs
|
|
159
|
-
// only in the sense that they are opaque; the machine fence is the
|
|
160
|
-
// real boundary).
|
|
161
|
-
const mine = terminals.list().filter((s) => s.machine === bound.machine)
|
|
162
|
-
return sendJson(res, 200, { ok: true, sessions: mine })
|
|
163
|
-
} catch (err) {
|
|
164
|
-
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
]
|
|
169
|
-
}
|
|
1
|
+
// dsh-remote — HTTP routes for the embedded terminal.
|
|
2
|
+
//
|
|
3
|
+
// Four small endpoints around one host-owned PTY session (see lib/terminal.js
|
|
4
|
+
// for why polling instead of a WebSocket):
|
|
5
|
+
//
|
|
6
|
+
// POST /dsh-remote/term/open {cols, rows} → {id}
|
|
7
|
+
// POST /dsh-remote/term/input {id, data} → {ok}
|
|
8
|
+
// GET /dsh-remote/term/output ?id&from=N → {chunks, next, lossy, closed}
|
|
9
|
+
// POST /dsh-remote/term/resize {id, cols, rows} → {ok}
|
|
10
|
+
// POST /dsh-remote/term/close {id} → {ok}
|
|
11
|
+
//
|
|
12
|
+
// Every route resolves the binding from the SESSION first (the same
|
|
13
|
+
// `resolveRequestBinding` the file routes use), so a terminal can only ever be
|
|
14
|
+
// opened on — and attached to — the machine that session is actually bound to.
|
|
15
|
+
// That is the same isolation rule that keeps one session from acting on
|
|
16
|
+
// another's host, applied to an interactive channel where a mistake would be
|
|
17
|
+
// even more visible.
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the terminal routes.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} deps
|
|
23
|
+
* @param {Function} deps.sendJson
|
|
24
|
+
* @param {Function} deps.readBody
|
|
25
|
+
* @param {Function} deps.resolveRequestBinding - session-aware binding resolver.
|
|
26
|
+
* @param {object} deps.terminals - the manager from createTerminalManager().
|
|
27
|
+
* @param {Function} deps.machineKey - (binding) → a stable machine identity.
|
|
28
|
+
* @returns {Array<object>} route descriptors for webServer.register().
|
|
29
|
+
*/
|
|
30
|
+
export function createTerminalRoutes({ sendJson, readBody, resolveRequestBinding, terminals, machineKey }) {
|
|
31
|
+
/** Resolve the binding and derive the machine identity, or reply and return null. */
|
|
32
|
+
const bind = async (req, res, body = {}) => {
|
|
33
|
+
let b
|
|
34
|
+
try {
|
|
35
|
+
b = await resolveRequestBinding(req, body)
|
|
36
|
+
} catch (err) {
|
|
37
|
+
sendJson(res, (err && err.httpStatus) || 500, { ok: false, error: String((err && err.message) || err) })
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
if (!b) return null
|
|
41
|
+
return { b, machine: machineKey(b) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
kind: 'exact',
|
|
47
|
+
path: '/dsh-remote/term/open',
|
|
48
|
+
handler: async (req, res) => {
|
|
49
|
+
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
50
|
+
try {
|
|
51
|
+
const body = JSON.parse((await readBody(req)) || '{}')
|
|
52
|
+
const bound = await bind(req, res, body)
|
|
53
|
+
if (!bound) return
|
|
54
|
+
const r = await terminals.open({
|
|
55
|
+
pool: bound.b.pool,
|
|
56
|
+
machine: bound.machine,
|
|
57
|
+
cols: Number(body.cols) || 80,
|
|
58
|
+
rows: Number(body.rows) || 24,
|
|
59
|
+
})
|
|
60
|
+
if (!r.ok) return sendJson(res, 503, { ok: false, error: r.error })
|
|
61
|
+
return sendJson(res, 200, { ok: true, id: r.id })
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
kind: 'exact',
|
|
70
|
+
path: '/dsh-remote/term/input',
|
|
71
|
+
handler: async (req, res) => {
|
|
72
|
+
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
73
|
+
try {
|
|
74
|
+
const body = JSON.parse((await readBody(req)) || '{}')
|
|
75
|
+
const bound = await bind(req, res, body)
|
|
76
|
+
if (!bound) return
|
|
77
|
+
const { session, error } = terminals.owned(body.id, bound.machine)
|
|
78
|
+
if (error) return sendJson(res, 404, { ok: false, error })
|
|
79
|
+
const data = typeof body.data === 'string' ? body.data : ''
|
|
80
|
+
if (!data) return sendJson(res, 400, { ok: false, error: 'data is required' })
|
|
81
|
+
if (!session.write(data)) return sendJson(res, 409, { ok: false, error: 'session is closed' })
|
|
82
|
+
return sendJson(res, 200, { ok: true })
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
kind: 'exact',
|
|
91
|
+
path: '/dsh-remote/term/output',
|
|
92
|
+
handler: async (req, res) => {
|
|
93
|
+
if (req.method !== 'GET' && req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
94
|
+
try {
|
|
95
|
+
const q = new URL(req.url, 'http://localhost').searchParams
|
|
96
|
+
let body = {}
|
|
97
|
+
if (req.method === 'POST') body = JSON.parse((await readBody(req)) || '{}')
|
|
98
|
+
const bound = await bind(req, res, body)
|
|
99
|
+
if (!bound) return
|
|
100
|
+
const id = body.id || q.get('id') || ''
|
|
101
|
+
const from = Number(body.from != null ? body.from : q.get('from')) || 0
|
|
102
|
+
const { session, error } = terminals.owned(id, bound.machine)
|
|
103
|
+
if (error) return sendJson(res, 404, { ok: false, error })
|
|
104
|
+
const out = session.readFrom(from)
|
|
105
|
+
return sendJson(res, 200, { ok: true, ...out })
|
|
106
|
+
} catch (err) {
|
|
107
|
+
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
kind: 'exact',
|
|
114
|
+
path: '/dsh-remote/term/resize',
|
|
115
|
+
handler: async (req, res) => {
|
|
116
|
+
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
117
|
+
try {
|
|
118
|
+
const body = JSON.parse((await readBody(req)) || '{}')
|
|
119
|
+
const bound = await bind(req, res, body)
|
|
120
|
+
if (!bound) return
|
|
121
|
+
const { session, error } = terminals.owned(body.id, bound.machine)
|
|
122
|
+
if (error) return sendJson(res, 404, { ok: false, error })
|
|
123
|
+
session.resize(body.cols, body.rows)
|
|
124
|
+
return sendJson(res, 200, { ok: true, cols: session.cols, rows: session.rows })
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
{
|
|
132
|
+
kind: 'exact',
|
|
133
|
+
path: '/dsh-remote/term/close',
|
|
134
|
+
handler: async (req, res) => {
|
|
135
|
+
if (req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
136
|
+
try {
|
|
137
|
+
const body = JSON.parse((await readBody(req)) || '{}')
|
|
138
|
+
const bound = await bind(req, res, body)
|
|
139
|
+
if (!bound) return
|
|
140
|
+
const r = terminals.close(body.id, bound.machine)
|
|
141
|
+
if (!r.ok) return sendJson(res, 404, { ok: false, error: r.error })
|
|
142
|
+
return sendJson(res, 200, { ok: true })
|
|
143
|
+
} catch (err) {
|
|
144
|
+
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
{
|
|
150
|
+
kind: 'exact',
|
|
151
|
+
path: '/dsh-remote/term/list',
|
|
152
|
+
handler: async (req, res) => {
|
|
153
|
+
if (req.method !== 'GET') return sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
154
|
+
try {
|
|
155
|
+
const bound = await bind(req, res, {})
|
|
156
|
+
if (!bound) return
|
|
157
|
+
// Only THIS machine's sessions: a listing that leaks other hosts'
|
|
158
|
+
// session ids would let a caller probe them (ids are guessable UUIDs
|
|
159
|
+
// only in the sense that they are opaque; the machine fence is the
|
|
160
|
+
// real boundary).
|
|
161
|
+
const mine = terminals.list().filter((s) => s.machine === bound.machine)
|
|
162
|
+
return sendJson(res, 200, { ok: true, sessions: mine })
|
|
163
|
+
} catch (err) {
|
|
164
|
+
return sendJson(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
]
|
|
169
|
+
}
|