ai-remote 0.4.2 → 0.4.4

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.
Files changed (75) hide show
  1. package/dist/index.js +1 -6
  2. package/dist/{protocols/index.d.ts → protocols.d.ts} +2 -0
  3. package/dist/protocols.js +24 -0
  4. package/package.json +15 -10
  5. package/dist/protocols/index.js +0 -34
  6. package/dist/protocols/rdp/buffer.d.ts +0 -38
  7. package/dist/protocols/rdp/buffer.js +0 -169
  8. package/dist/protocols/rdp/caps.d.ts +0 -49
  9. package/dist/protocols/rdp/caps.js +0 -259
  10. package/dist/protocols/rdp/cert.d.ts +0 -18
  11. package/dist/protocols/rdp/cert.js +0 -134
  12. package/dist/protocols/rdp/client.d.ts +0 -56
  13. package/dist/protocols/rdp/client.js +0 -1069
  14. package/dist/protocols/rdp/cliprdr.d.ts +0 -27
  15. package/dist/protocols/rdp/cliprdr.js +0 -239
  16. package/dist/protocols/rdp/credssp.d.ts +0 -34
  17. package/dist/protocols/rdp/credssp.js +0 -253
  18. package/dist/protocols/rdp/crypto.d.ts +0 -63
  19. package/dist/protocols/rdp/crypto.js +0 -368
  20. package/dist/protocols/rdp/display.d.ts +0 -38
  21. package/dist/protocols/rdp/display.js +0 -588
  22. package/dist/protocols/rdp/gcc.d.ts +0 -23
  23. package/dist/protocols/rdp/gcc.js +0 -131
  24. package/dist/protocols/rdp/keymap.d.ts +0 -9
  25. package/dist/protocols/rdp/keymap.js +0 -131
  26. package/dist/protocols/rdp/mcs.d.ts +0 -40
  27. package/dist/protocols/rdp/mcs.js +0 -222
  28. package/dist/protocols/rdp/ntlm.d.ts +0 -61
  29. package/dist/protocols/rdp/ntlm.js +0 -304
  30. package/dist/protocols/rdp/pdu.d.ts +0 -155
  31. package/dist/protocols/rdp/pdu.js +0 -443
  32. package/dist/protocols/rdp/rail.d.ts +0 -119
  33. package/dist/protocols/rdp/rail.js +0 -382
  34. package/dist/protocols/rdp/rle.d.ts +0 -13
  35. package/dist/protocols/rdp/rle.js +0 -275
  36. package/dist/protocols/rdp/sec.d.ts +0 -59
  37. package/dist/protocols/rdp/sec.js +0 -155
  38. package/dist/protocols/rdp/session.d.ts +0 -62
  39. package/dist/protocols/rdp/session.js +0 -306
  40. package/dist/protocols/rdp/tls.d.ts +0 -18
  41. package/dist/protocols/rdp/tls.js +0 -353
  42. package/dist/protocols/rdp/vchannel.d.ts +0 -28
  43. package/dist/protocols/rdp/vchannel.js +0 -58
  44. package/dist/protocols/rdp/x224.d.ts +0 -40
  45. package/dist/protocols/rdp/x224.js +0 -109
  46. package/dist/protocols/ssh/kex.d.ts +0 -97
  47. package/dist/protocols/ssh/kex.js +0 -176
  48. package/dist/protocols/ssh/messages.d.ts +0 -50
  49. package/dist/protocols/ssh/messages.js +0 -54
  50. package/dist/protocols/ssh/session.d.ts +0 -66
  51. package/dist/protocols/ssh/session.js +0 -608
  52. package/dist/protocols/ssh/terminal.d.ts +0 -46
  53. package/dist/protocols/ssh/terminal.js +0 -190
  54. package/dist/protocols/ssh/transport.d.ts +0 -62
  55. package/dist/protocols/ssh/transport.js +0 -612
  56. package/dist/protocols/ssh/wire.d.ts +0 -52
  57. package/dist/protocols/ssh/wire.js +0 -188
  58. package/dist/protocols/types.d.ts +0 -127
  59. package/dist/protocols/types.js +0 -0
  60. package/dist/protocols/vnc/input.d.ts +0 -5
  61. package/dist/protocols/vnc/input.js +0 -15
  62. package/dist/protocols/vnc/session.d.ts +0 -12
  63. package/dist/protocols/vnc/session.js +0 -258
  64. package/dist/shared/connection.d.ts +0 -81
  65. package/dist/shared/connection.js +0 -113
  66. package/dist/shared/device.d.ts +0 -26
  67. package/dist/shared/device.js +0 -0
  68. package/dist/shared/hosts.d.ts +0 -63
  69. package/dist/shared/hosts.js +0 -132
  70. package/dist/shared/icons.d.ts +0 -46
  71. package/dist/shared/icons.js +0 -41
  72. package/dist/shared/protocol.d.ts +0 -28
  73. package/dist/shared/protocol.js +0 -39
  74. package/dist/shared/signals.d.ts +0 -37
  75. package/dist/shared/signals.js +0 -41
@@ -1,608 +0,0 @@
1
- import { MSG, EXTENDED_DATA_STDERR } from "./messages.js";
2
- import { SshReader, SshWriter, decodeUtf8, encodeUtf8, toBase64 } from "./wire.js";
3
- import { SshTransport } from "./transport.js";
4
- const DEFAULT_PORT = 22;
5
- const SERVICE = "ssh-connection";
6
- const TERMINAL_TYPE = "xterm-256color";
7
- const INITIAL_WINDOW = 2 * 1024 * 1024;
8
- const MAX_PACKET = 32 * 1024;
9
- const WINDOW_REFILL_THRESHOLD = INITIAL_WINDOW / 2;
10
- const PTY_MODES = new Uint8Array([0]);
11
- const COMMAND_START_MS = 2e4;
12
- const COMMAND_IDLE_MS = 5 * 6e4;
13
- const COMMAND_MAX_MS = 60 * 6e4;
14
- const NO_BYTES = new Uint8Array(0);
15
- function singleQuote(value) {
16
- return `'${String(value).replace(/'/g, `'\\''`)}'`;
17
- }
18
- function shellFromPrompt(text) {
19
- const tail = text.slice(-400);
20
- if (/(?:^|\n)PS [A-Za-z]:\\[^\n]*>\s*$/.test(tail)) return "powershell";
21
- if (/(?:^|\n)[^\n]*[A-Za-z]:\\[^\n]*>\s*$/.test(tail)) return "cmd";
22
- return "";
23
- }
24
- const FRAMING_REJECTED = /is not recognized as an internal or external command|command not found|not recognized as the name of a cmdlet|CommandNotFoundException/i;
25
- function frameCommand(family, command, id) {
26
- if (family === "cmd") {
27
- return `echo.&echo CCB${id}&${command}&call echo.CCE${id} %^ERRORLEVEL%\r`;
28
- }
29
- if (family === "powershell") {
30
- const encoded = toBase64(encodeUtf8(command));
31
- return `Write-Output ""; Write-Output ("CCB" + "${id}"); $__ccCode = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}')); $global:LASTEXITCODE = $null; . ([ScriptBlock]::Create($__ccCode)); $__ccOk = $?; $__cc = if ($LASTEXITCODE -ne $null) { $LASTEXITCODE } elseif ($__ccOk) { 0 } else { 1 }; Write-Output ("CCE" + "${id} " + $__cc)\r`;
32
- }
33
- return `printf '\\n%s%s\\n' 'CCB' '${id}'; eval ${singleQuote(command)}; __cc=$?; printf '\\n%s%s %s\\n' 'CCE' '${id}' "$__cc"\r`;
34
- }
35
- function stripTerminalCodes(text) {
36
- return text.replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, "").replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\u001b[@-Z\\-_]/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "");
37
- }
38
- function filterDisplay(session, bytes) {
39
- const filter = session.displayFilter;
40
- if (!filter) return bytes;
41
- filter.pending += decodeUtf8(bytes);
42
- let shown = "";
43
- if (filter.stage === "before") {
44
- const at = filter.pending.indexOf(filter.begin);
45
- if (at === -1) return NO_BYTES;
46
- filter.pending = filter.pending.slice(at + filter.begin.length).replace(/^\r?\n/, "");
47
- filter.stage = "inside";
48
- }
49
- if (filter.stage === "inside") {
50
- const at = filter.pending.indexOf(filter.end);
51
- if (at === -1) {
52
- shown = filter.pending;
53
- filter.pending = "";
54
- return shown ? encodeUtf8(shown) : NO_BYTES;
55
- }
56
- shown = filter.pending.slice(0, at);
57
- filter.pending = filter.pending.slice(at);
58
- filter.stage = "closing";
59
- }
60
- const newline = filter.pending.indexOf("\n");
61
- if (newline !== -1) {
62
- shown += filter.pending.slice(newline + 1);
63
- session.displayFilter = null;
64
- }
65
- return shown ? encodeUtf8(shown) : NO_BYTES;
66
- }
67
- function releaseDisplayFilter(session) {
68
- const filter = session.displayFilter;
69
- session.displayFilter = null;
70
- if (!filter || filter.stage !== "before" || !filter.pending) return;
71
- session.dispatchEvent(new CustomEvent("data", {
72
- detail: { bytes: NO_BYTES, display: encodeUtf8(filter.pending), isStderr: false }
73
- }));
74
- }
75
- class SshSession extends EventTarget {
76
- /**
77
- * @param {string} url gateway WebSocket URL
78
- * @param {object} options
79
- * @param {string} options.username
80
- * @param {string} [options.password]
81
- * @param {'powershell'} [options.preferredShell]
82
- * @param {(info: object) => Promise<boolean>} [options.verifyHost]
83
- * @param {(prompt: object) => Promise<string>} [options.requestInput]
84
- */
85
- constructor(url, options = {}) {
86
- super();
87
- this.options = options;
88
- this.username = options.username || "";
89
- this.password = options.password || "";
90
- this.requestInput = options.requestInput || (async () => "");
91
- this.columns = options.columns || 80;
92
- this.rows = options.rows || 24;
93
- this.channelId = 0;
94
- this.remoteChannelId = null;
95
- this.remoteWindow = 0;
96
- this.remoteMaxPacket = MAX_PACKET;
97
- this.localWindow = INITIAL_WINDOW;
98
- this.pendingWrites = [];
99
- this.shellOpen = false;
100
- this.commandInFlight = false;
101
- this.displayFilter = null;
102
- this.readySignaled = false;
103
- this.powerShellAttempted = false;
104
- this.powerShellTimer = null;
105
- this.shellFamily = "";
106
- this.promptTail = "";
107
- this.exitStatus = null;
108
- this.authMethods = [];
109
- this.closed = false;
110
- this.waiters = /* @__PURE__ */ new Map();
111
- this.transport = new SshTransport(url, {
112
- verifyHost: options.verifyHost || (async () => true),
113
- openTransport: options.openTransport || null,
114
- log: (step, detail) => console.info(`[SSH] ${step}`, detail)
115
- });
116
- this.transport.addEventListener("packet", (event) => {
117
- this.#handlePacket(event.detail.type, event.detail.payload);
118
- });
119
- this.transport.addEventListener("greeting", (event) => {
120
- for (const line of event.detail.lines) this.#emitBanner(`${line}\r
121
- `);
122
- });
123
- this.transport.addEventListener("banner", (event) => this.#emitBanner(event.detail.text));
124
- this.transport.addEventListener("keys", (event) => {
125
- this.fingerprint = event.detail.fingerprint;
126
- this.#status("secured", `Host key ${event.detail.fingerprint} verified.`);
127
- this.dispatchEvent(new CustomEvent("secure", { detail: event.detail }));
128
- if (event.detail.firstTime) void this.#authenticate();
129
- });
130
- this.transport.addEventListener("hostdisconnect", (event) => {
131
- this.lastMessage = event.detail.reason;
132
- });
133
- this.transport.addEventListener("error", (event) => {
134
- this.lastMessage = event.detail.message;
135
- this.dispatchEvent(new CustomEvent("error", { detail: event.detail }));
136
- });
137
- this.transport.addEventListener("close", (event) => {
138
- if (this.closed) return;
139
- this.closed = true;
140
- clearTimeout(this.powerShellTimer);
141
- this.#abandonWaiters(event.detail.message || "The SSH connection closed.");
142
- this.dispatchEvent(new CustomEvent("close", {
143
- detail: {
144
- ...event.detail,
145
- message: event.detail.message || this.lastMessage || "",
146
- exitStatus: this.exitStatus
147
- }
148
- }));
149
- });
150
- }
151
- connect() {
152
- this.#status("connecting", "Opening the SSH transport\u2026");
153
- this.transport.connect();
154
- }
155
- disconnect() {
156
- if (this.closed) return;
157
- clearTimeout(this.powerShellTimer);
158
- if (this.shellOpen && this.remoteChannelId !== null) {
159
- this.transport.send(new SshWriter(8).u8(MSG.CHANNEL_CLOSE).u32(this.remoteChannelId).take());
160
- }
161
- this.transport.disconnect("Closed by the user");
162
- }
163
- /** Send keystrokes, or anything else the terminal produces, to the shell. */
164
- write(data) {
165
- const bytes = typeof data === "string" ? encodeUtf8(data) : data;
166
- if (!bytes.length) return;
167
- this.pendingWrites.push(bytes);
168
- this.#flushWrites();
169
- }
170
- /**
171
- * Run one command in this shell and return what it printed.
172
- *
173
- * Deliberately the *same* shell the user is looking at, not a second channel:
174
- * a command the assistant runs inherits the working directory, the
175
- * environment and the login the user already has, and it appears in their
176
- * terminal as it happens. An agent acting on a machine invisibly is worse
177
- * than an agent that is slower to read.
178
- *
179
- * The output is framed by markers rather than measured, because an
180
- * interactive shell echoes its input and prints a prompt around it. The
181
- * markers are assembled by the shell from two pieces, so the echoed command
182
- * line -- which contains the pieces but not the joined marker -- never looks
183
- * like the real boundary.
184
- *
185
- * A `command` event brackets the run so the terminal can label it, and the
186
- * three deadlines above decide when a command is stuck rather than slow.
187
- */
188
- runCommand(command, {
189
- startMs = COMMAND_START_MS,
190
- idleMs = COMMAND_IDLE_MS,
191
- maxMs = COMMAND_MAX_MS
192
- } = {}) {
193
- const text = String(command || "").trim();
194
- if (!text) return Promise.reject(new Error("No command was given."));
195
- if (!this.shellOpen || this.closed) {
196
- return Promise.reject(new Error("The SSH shell is not open."));
197
- }
198
- if (this.readySignaled === false) {
199
- return Promise.reject(new Error("The SSH shell is still starting."));
200
- }
201
- if (this.commandInFlight) {
202
- return Promise.reject(new Error("Another command is still running in this shell."));
203
- }
204
- const id = Array.from(crypto.getRandomValues(new Uint8Array(6))).map((byte) => byte.toString(16).padStart(2, "0")).join("");
205
- const begin = `CCB${id}`;
206
- const end = `CCE${id}`;
207
- const family = this.shellFamily || "posix";
208
- this.commandInFlight = true;
209
- const startedAt = Date.now();
210
- this.displayFilter = { begin, end, stage: "before", pending: "" };
211
- this.dispatchEvent(new CustomEvent("command", {
212
- detail: { phase: "start", id, command: text }
213
- }));
214
- return new Promise((resolve, reject) => {
215
- let transcript = "";
216
- let started = false;
217
- let startTimer = null;
218
- let idleTimer = null;
219
- let maxTimer = null;
220
- const finish = (settle) => {
221
- clearTimeout(startTimer);
222
- clearTimeout(idleTimer);
223
- clearTimeout(maxTimer);
224
- this.removeEventListener("data", onData);
225
- this.removeEventListener("close", onClose);
226
- this.commandInFlight = false;
227
- releaseDisplayFilter(this);
228
- settle();
229
- };
230
- const done = (result) => finish(() => {
231
- this.dispatchEvent(new CustomEvent("command", {
232
- detail: {
233
- phase: "end",
234
- id,
235
- command: text,
236
- exitStatus: result.exitStatus,
237
- timedOut: result.timedOut,
238
- started: result.started !== false,
239
- reason: result.reason,
240
- durationMs: result.durationMs
241
- }
242
- }));
243
- resolve(result);
244
- });
245
- const fail = (error) => finish(() => {
246
- this.dispatchEvent(new CustomEvent("command", {
247
- detail: {
248
- phase: "end",
249
- id,
250
- command: text,
251
- exitStatus: null,
252
- timedOut: false,
253
- started,
254
- durationMs: Date.now() - startedAt,
255
- error: error.message
256
- }
257
- }));
258
- reject(error);
259
- });
260
- const partialOutput = () => {
261
- const at = transcript.indexOf(begin);
262
- return at === -1 ? "" : stripTerminalCodes(transcript.slice(at + begin.length)).replace(/^\r?\n/, "").replace(/\s+$/, "");
263
- };
264
- const giveUp = (reason) => {
265
- this.write("");
266
- done({
267
- output: partialOutput(),
268
- exitStatus: null,
269
- timedOut: true,
270
- started,
271
- reason,
272
- durationMs: Date.now() - startedAt
273
- });
274
- };
275
- const onData = (event) => {
276
- transcript += decodeUtf8(event.detail.bytes);
277
- if (!started && transcript.indexOf(begin) !== -1) {
278
- started = true;
279
- clearTimeout(startTimer);
280
- startTimer = null;
281
- }
282
- if (started) {
283
- clearTimeout(idleTimer);
284
- idleTimer = setTimeout(() => giveUp("idle"), idleMs);
285
- }
286
- const endAt = transcript.indexOf(`${end} `);
287
- if (endAt === -1) {
288
- if (!started && FRAMING_REJECTED.test(stripTerminalCodes(transcript))) {
289
- const guessed = shellFromPrompt(stripTerminalCodes(transcript)) || "cmd";
290
- const wrongGuess = family !== guessed;
291
- this.shellFamily = guessed;
292
- fail(new Error(
293
- wrongGuess ? `This shell is ${guessed === "powershell" ? "PowerShell" : "Windows cmd"}, not a POSIX shell, and it rejected the wrapper used to capture output. The shell has been noted; run the command again and it will be framed correctly.` : `The shell rejected the wrapper used to capture output: ${stripTerminalCodes(transcript).split("\n").filter(Boolean).pop() || "no detail"}`
294
- ));
295
- }
296
- return;
297
- }
298
- const tail = transcript.slice(endAt);
299
- const status = parseInt(tail.slice(end.length + 1).trim(), 10);
300
- const beginAt = transcript.indexOf(begin);
301
- const body = beginAt === -1 ? transcript.slice(0, endAt) : transcript.slice(beginAt + begin.length, endAt);
302
- done({
303
- output: stripTerminalCodes(body).replace(/^\r?\n/, "").replace(/\s+$/, ""),
304
- exitStatus: Number.isInteger(status) ? status : null,
305
- timedOut: false,
306
- started: true,
307
- durationMs: Date.now() - startedAt
308
- });
309
- };
310
- const onClose = () => fail(new Error("The SSH session closed while the command was running."));
311
- this.addEventListener("data", onData);
312
- this.addEventListener("close", onClose);
313
- startTimer = setTimeout(() => giveUp("start"), startMs);
314
- idleTimer = setTimeout(() => giveUp("idle"), idleMs);
315
- maxTimer = setTimeout(() => giveUp("max"), maxMs);
316
- this.write(frameCommand(family, text, id));
317
- });
318
- }
319
- /** Tell the shell its window changed, so full-screen programs redraw. */
320
- resize(columns, rows) {
321
- const nextColumns = Math.max(1, Math.floor(columns));
322
- const nextRows = Math.max(1, Math.floor(rows));
323
- if (nextColumns === this.columns && nextRows === this.rows) return;
324
- this.columns = nextColumns;
325
- this.rows = nextRows;
326
- if (!this.shellOpen || this.remoteChannelId === null) return;
327
- this.transport.send(new SshWriter(64).u8(MSG.CHANNEL_REQUEST).u32(this.remoteChannelId).string("window-change").boolean(false).u32(nextColumns).u32(nextRows).u32(0).u32(0).take());
328
- }
329
- // --- plumbing ------------------------------------------------------------
330
- #status(phase, message) {
331
- this.dispatchEvent(new CustomEvent("status", { detail: { phase, message } }));
332
- }
333
- #emitBanner(text) {
334
- if (text) this.dispatchEvent(new CustomEvent("banner", { detail: { text } }));
335
- }
336
- /** Wait for one of a few message types: the handshakes read linearly that way. */
337
- #expect(...types) {
338
- return new Promise((resolve, reject) => {
339
- for (const type of types) this.waiters.set(type, { resolve, reject });
340
- });
341
- }
342
- #settle(type, payload) {
343
- const waiter = this.waiters.get(type);
344
- if (!waiter) return false;
345
- this.waiters.clear();
346
- waiter.resolve({ type, payload });
347
- return true;
348
- }
349
- /** A closed transport must not leave a handshake waiting forever. */
350
- #abandonWaiters(message) {
351
- if (this.waiters.size === 0) return;
352
- const pending = [...new Set(this.waiters.values())];
353
- this.waiters.clear();
354
- for (const waiter of pending) waiter.reject(new Error(message));
355
- }
356
- #handlePacket(type, payload) {
357
- if (this.#settle(type, payload)) return;
358
- switch (type) {
359
- case MSG.USERAUTH_BANNER:
360
- this.#emitBanner(new SshReader(payload, 1).string().replaceAll("\n", "\r\n"));
361
- return;
362
- case MSG.CHANNEL_DATA: {
363
- const reader = new SshReader(payload, 1);
364
- reader.u32();
365
- this.#consume(reader.stringBytes());
366
- return;
367
- }
368
- case MSG.CHANNEL_EXTENDED_DATA: {
369
- const reader = new SshReader(payload, 1);
370
- reader.u32();
371
- const dataType = reader.u32();
372
- const bytes = reader.stringBytes();
373
- this.#consume(bytes, dataType === EXTENDED_DATA_STDERR);
374
- return;
375
- }
376
- case MSG.CHANNEL_WINDOW_ADJUST: {
377
- const reader = new SshReader(payload, 1);
378
- reader.u32();
379
- this.remoteWindow += reader.u32();
380
- this.#flushWrites();
381
- return;
382
- }
383
- case MSG.CHANNEL_REQUEST: {
384
- const reader = new SshReader(payload, 1);
385
- reader.u32();
386
- const request = reader.string();
387
- const wantReply = reader.boolean();
388
- if (request === "exit-status") this.exitStatus = reader.u32();
389
- if (request === "exit-signal") this.exitSignal = reader.string();
390
- if (wantReply && this.remoteChannelId !== null) {
391
- this.transport.send(new SshWriter(8).u8(MSG.CHANNEL_SUCCESS).u32(this.remoteChannelId).take());
392
- }
393
- return;
394
- }
395
- case MSG.CHANNEL_EOF:
396
- return;
397
- case MSG.CHANNEL_CLOSE:
398
- this.shellOpen = false;
399
- this.#status("closed", "The remote shell ended.");
400
- this.transport.disconnect("Shell closed");
401
- return;
402
- default:
403
- return;
404
- }
405
- }
406
- /**
407
- * Watch the prompt for which shell this machine answers with, so the first
408
- * command is framed correctly rather than discovering it by failing.
409
- */
410
- #noteShellFamily(bytes) {
411
- this.promptTail = (this.promptTail + stripTerminalCodes(decodeUtf8(bytes))).slice(-400);
412
- const family = shellFromPrompt(this.promptTail);
413
- if (!family) return;
414
- if (family === "cmd" && !this.powerShellAttempted) {
415
- this.#startPowerShell();
416
- return;
417
- }
418
- if (family === "cmd" && this.powerShellAttempted && !this.readySignaled) return;
419
- this.shellFamily = family;
420
- if (family === "powershell" && !this.readySignaled) {
421
- this.#markReady("Connected \xB7 PowerShell");
422
- }
423
- }
424
- /** Replace Windows OpenSSH's usual cmd.exe with an interactive PowerShell. */
425
- #startPowerShell() {
426
- if (this.powerShellAttempted || !this.shellOpen) return;
427
- this.powerShellAttempted = true;
428
- this.readySignaled = false;
429
- this.shellFamily = "";
430
- this.#status("opening", "Starting PowerShell\u2026");
431
- this.write("powershell.exe -NoLogo\r");
432
- this.powerShellTimer = setTimeout(() => {
433
- if (this.readySignaled || this.closed) return;
434
- this.shellFamily = "powershell";
435
- this.#markReady("Connected \xB7 PowerShell");
436
- }, 2e3);
437
- }
438
- #markReady(message = "Connected.") {
439
- clearTimeout(this.powerShellTimer);
440
- this.powerShellTimer = null;
441
- this.readySignaled = true;
442
- this.#status("ready", message);
443
- this.dispatchEvent(new CustomEvent("ready", {
444
- detail: { fingerprint: this.fingerprint, shellFamily: this.shellFamily || "posix" }
445
- }));
446
- }
447
- /** Shell output, with the receive window topped up before it runs dry. */
448
- #consume(bytes, isStderr = false) {
449
- if (!bytes.length) return;
450
- this.#noteShellFamily(bytes);
451
- this.dispatchEvent(new CustomEvent("data", {
452
- detail: { bytes, display: filterDisplay(this, bytes), isStderr }
453
- }));
454
- this.localWindow -= bytes.length;
455
- if (this.localWindow > WINDOW_REFILL_THRESHOLD || this.remoteChannelId === null) return;
456
- const increment = INITIAL_WINDOW - this.localWindow;
457
- this.localWindow = INITIAL_WINDOW;
458
- this.transport.send(new SshWriter(16).u8(MSG.CHANNEL_WINDOW_ADJUST).u32(this.remoteChannelId).u32(increment).take());
459
- }
460
- #flushWrites() {
461
- if (!this.shellOpen || this.remoteChannelId === null) return;
462
- while (this.pendingWrites.length > 0) {
463
- const next = this.pendingWrites[0];
464
- const room = Math.min(this.remoteWindow, this.remoteMaxPacket - 64);
465
- if (room <= 0) return;
466
- const chunk = next.length <= room ? next : next.subarray(0, room);
467
- if (chunk.length === next.length) this.pendingWrites.shift();
468
- else this.pendingWrites[0] = next.subarray(chunk.length);
469
- this.remoteWindow -= chunk.length;
470
- this.transport.send(new SshWriter(chunk.length + 16).u8(MSG.CHANNEL_DATA).u32(this.remoteChannelId).string(chunk).take());
471
- }
472
- }
473
- // --- authentication ------------------------------------------------------
474
- async #authenticate() {
475
- try {
476
- this.#status("authenticating", `Authenticating as ${this.username}\u2026`);
477
- this.transport.send(new SshWriter(32).u8(MSG.SERVICE_REQUEST).string("ssh-userauth").take());
478
- await this.#expect(MSG.SERVICE_ACCEPT);
479
- if (await this.#tryNone()) return void await this.#openShell();
480
- if (this.authMethods.includes("password") && this.password) {
481
- if (await this.#tryPassword(this.password)) return void await this.#openShell();
482
- }
483
- if (this.authMethods.includes("keyboard-interactive")) {
484
- if (await this.#tryKeyboardInteractive()) return void await this.#openShell();
485
- if (this.authMethods.includes("keyboard-interactive")) {
486
- if (await this.#tryKeyboardInteractive({ usePassword: false })) {
487
- return void await this.#openShell();
488
- }
489
- }
490
- }
491
- if (this.authMethods.includes("password")) {
492
- const typed = await this.requestInput({
493
- prompt: `${this.username}@${this.options.hostLabel || "host"}'s password: `,
494
- echo: false
495
- });
496
- if (typed && await this.#tryPassword(typed)) return void await this.#openShell();
497
- }
498
- throw new Error(this.authMethods.length ? `SSH: authentication failed. The host accepts ${this.authMethods.join(", ")}.` : "SSH: the host rejected the connection before any authentication method was offered.");
499
- } catch (error) {
500
- if (!this.closed) this.transport.fail(error);
501
- }
502
- }
503
- async #tryNone() {
504
- this.transport.send(new SshWriter(64).u8(MSG.USERAUTH_REQUEST).string(this.username).string(SERVICE).string("none").take());
505
- return this.#authResult();
506
- }
507
- async #tryPassword(password) {
508
- this.transport.send(new SshWriter(128).u8(MSG.USERAUTH_REQUEST).string(this.username).string(SERVICE).string("password").boolean(false).string(password).take());
509
- return this.#authResult();
510
- }
511
- /**
512
- * keyboard-interactive is how most Linux hosts actually ask for a password,
513
- * and the only way a one-time code can be entered. Prompts are answered from
514
- * the password when one is obviously being asked for, and by the user
515
- * otherwise.
516
- */
517
- async #tryKeyboardInteractive({ usePassword = true } = {}) {
518
- this.transport.send(new SshWriter(96).u8(MSG.USERAUTH_REQUEST).string(this.username).string(SERVICE).string("keyboard-interactive").string("").string("").take());
519
- let usedPassword = !usePassword;
520
- while (true) {
521
- const { type, payload } = await this.#expect(
522
- MSG.USERAUTH_SUCCESS,
523
- MSG.USERAUTH_FAILURE,
524
- MSG.USERAUTH_INFO_REQUEST
525
- );
526
- if (type === MSG.USERAUTH_SUCCESS) return true;
527
- if (type === MSG.USERAUTH_FAILURE) {
528
- this.#readFailure(payload);
529
- return false;
530
- }
531
- const reader = new SshReader(payload, 1);
532
- const name = reader.string();
533
- const instruction = reader.string();
534
- reader.string();
535
- const promptCount = reader.u32();
536
- if (name) this.#emitBanner(`${name.replaceAll("\n", "\r\n")}\r
537
- `);
538
- if (instruction) this.#emitBanner(`${instruction.replaceAll("\n", "\r\n")}\r
539
- `);
540
- const answers = [];
541
- for (let index = 0; index < promptCount; index++) {
542
- const prompt = reader.string();
543
- const echo = reader.boolean();
544
- const looksLikePassword = !echo && /pass(word|phrase)/i.test(prompt);
545
- if (looksLikePassword && this.password && !usedPassword) {
546
- usedPassword = true;
547
- answers.push(this.password);
548
- continue;
549
- }
550
- answers.push(await this.requestInput({ prompt, echo }));
551
- }
552
- const response = new SshWriter(256).u8(MSG.USERAUTH_INFO_RESPONSE).u32(answers.length);
553
- for (const answer of answers) response.string(answer);
554
- this.transport.send(response.take());
555
- }
556
- }
557
- async #authResult() {
558
- const { type, payload } = await this.#expect(MSG.USERAUTH_SUCCESS, MSG.USERAUTH_FAILURE);
559
- if (type === MSG.USERAUTH_SUCCESS) return true;
560
- this.#readFailure(payload);
561
- return false;
562
- }
563
- #readFailure(payload) {
564
- const reader = new SshReader(payload, 1);
565
- this.authMethods = reader.nameList();
566
- const partial = reader.boolean();
567
- if (partial) this.#emitBanner("\r\nOne factor accepted; the host wants another.\r\n");
568
- }
569
- // --- the shell -----------------------------------------------------------
570
- async #openShell() {
571
- this.#status("opening", "Starting the remote shell\u2026");
572
- this.transport.send(new SshWriter(64).u8(MSG.CHANNEL_OPEN).string("session").u32(this.channelId).u32(INITIAL_WINDOW).u32(MAX_PACKET).take());
573
- const opened = await this.#expect(MSG.CHANNEL_OPEN_CONFIRMATION, MSG.CHANNEL_OPEN_FAILURE);
574
- if (opened.type === MSG.CHANNEL_OPEN_FAILURE) {
575
- const reader2 = new SshReader(opened.payload, 1);
576
- reader2.u32();
577
- reader2.u32();
578
- throw new Error(`SSH: the host refused to open a session channel (${reader2.string()}).`);
579
- }
580
- const reader = new SshReader(opened.payload, 1);
581
- reader.u32();
582
- this.remoteChannelId = reader.u32();
583
- this.remoteWindow = reader.u32();
584
- this.remoteMaxPacket = Math.max(1024, Math.min(reader.u32(), MAX_PACKET));
585
- this.transport.send(new SshWriter(128).u8(MSG.CHANNEL_REQUEST).u32(this.remoteChannelId).string("pty-req").boolean(true).string(TERMINAL_TYPE).u32(this.columns).u32(this.rows).u32(0).u32(0).string(PTY_MODES).take());
586
- const pty = await this.#expect(MSG.CHANNEL_SUCCESS, MSG.CHANNEL_FAILURE);
587
- if (pty.type === MSG.CHANNEL_FAILURE) {
588
- throw new Error("SSH: the host refused a pseudo-terminal, so there is no interactive shell to show.");
589
- }
590
- this.transport.send(new SshWriter(32).u8(MSG.CHANNEL_REQUEST).u32(this.remoteChannelId).string("shell").boolean(true).take());
591
- const shell = await this.#expect(MSG.CHANNEL_SUCCESS, MSG.CHANNEL_FAILURE);
592
- if (shell.type === MSG.CHANNEL_FAILURE) {
593
- throw new Error("SSH: the host refused to start a shell for this account.");
594
- }
595
- this.shellOpen = true;
596
- this.#flushWrites();
597
- if (this.options.preferredShell === "powershell") this.#startPowerShell();
598
- else this.#markReady();
599
- }
600
- }
601
- export {
602
- COMMAND_IDLE_MS,
603
- COMMAND_MAX_MS,
604
- COMMAND_START_MS,
605
- DEFAULT_PORT,
606
- SshSession,
607
- frameCommand
608
- };
@@ -1,46 +0,0 @@
1
- export declare class SshTerminal {
2
- constructor(container: any);
3
- get columns(): any;
4
- get rows(): any;
5
- onData(handler: any): void;
6
- onResize(handler: any): void;
7
- write(data: Uint8Array): void;
8
- /** Gateway-side notices, kept visually apart from what the host prints. */
9
- notice(text: string, tone?: string): void;
10
- clear(): void;
11
- focus(): void;
12
- /**
13
- * Size the terminal to the panel, choosing the font so MIN_COLUMNS fit.
14
- *
15
- * The columns matter more than the type size here: the commands people run in
16
- * a side panel -- `ls -l`, `top`, anything in PowerShell that prints a table
17
- * -- are laid out for 80 columns, and a narrower grid wraps every row into an
18
- * unreadable stack.
19
- *
20
- * `proposeDimensions()` reports the grid the current font size yields, and a
21
- * monospace cell's width scales with its font size, so `fontSize * cols /
22
- * MIN_COLUMNS` lands close to the size that gives MIN_COLUMNS. It is measured
23
- * rather than assumed because the cell-width-to-font-size ratio belongs to
24
- * whichever font the platform picked. A second pass settles the rounding.
25
- * Changing the font resizes the element, which the ResizeObserver sees, so
26
- * the guard keeps that from recursing.
27
- */
28
- fit(): void;
29
- /** The grid the host is being told about, for a status line or a log. */
30
- get geometry(): {
31
- columns: any;
32
- rows: any;
33
- fontSize: any;
34
- };
35
- get selection(): any;
36
- /**
37
- * Read one line from the user, for a password or a one-time code the host
38
- * asked for during authentication. The shell is not open yet, so the
39
- * terminal is free to be a prompt.
40
- */
41
- readLine({ prompt, echo }?: {
42
- echo?: boolean;
43
- prompt?: string;
44
- }): Promise<unknown>;
45
- destroy(): void;
46
- }