1562 lines
45 KiB
TypeScript
1562 lines
45 KiB
TypeScript
|
|
import test from "node:test";
|
||
|
|
import assert from "node:assert/strict";
|
||
|
|
|
||
|
|
import {
|
||
|
|
createTerminalSessionStarters,
|
||
|
|
} from "./createTerminalSessionStarters";
|
||
|
|
|
||
|
|
const noop = () => undefined;
|
||
|
|
const ENCRYPTED_CREDENTIAL_PLACEHOLDER = "enc:v1:djEwdGVzdAAAAAAAAAAAAAAAAA==";
|
||
|
|
|
||
|
|
const armSudoPrompt = (
|
||
|
|
autofill: { armForCommand: (command: string) => void } | null,
|
||
|
|
): string => {
|
||
|
|
autofill?.armForCommand("sudo whoami");
|
||
|
|
return "[sudo] password for alice: ";
|
||
|
|
};
|
||
|
|
|
||
|
|
test("startMosh enables sudo autofill with the host saved password", async () => {
|
||
|
|
let detected = "";
|
||
|
|
let probes = 0;
|
||
|
|
let ready: (() => void) | undefined;
|
||
|
|
let resolveDetected!: () => void;
|
||
|
|
const detectionDone = new Promise<void>((resolve) => { resolveDetected = resolve; });
|
||
|
|
let onData: ((data: string) => void) | null = null;
|
||
|
|
const sent: string[] = [];
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => "mosh-session",
|
||
|
|
onMoshSessionReady: (_id: string, callback: () => void) => { ready = callback; return noop; },
|
||
|
|
getSessionDistroInfo: async () => { probes++; return { success: true, stdout: "ID=ubuntu" }; },
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: (_id: string, cb: (data: string) => void) => {
|
||
|
|
onData = cb;
|
||
|
|
return noop;
|
||
|
|
},
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: (_id: string, data: string) => sent.push(data),
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
const sudoAutofillRef = { current: null };
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Target",
|
||
|
|
hostname: "target.example.test",
|
||
|
|
username: "alice",
|
||
|
|
password: "saved-secret",
|
||
|
|
},
|
||
|
|
onOsDetected: (_id: string, distro: string) => { detected = distro; resolveDetected(); },
|
||
|
|
keys: [],
|
||
|
|
identities: [],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sudoAutofillPassword: "saved-secret",
|
||
|
|
onSudoHint: () => true,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: true },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
sudoAutofillRef,
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: noop,
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
assert.equal(probes, 0, "wait for the Mosh handshake before probing");
|
||
|
|
ready?.();
|
||
|
|
await detectionDone;
|
||
|
|
assert.equal(detected, "ubuntu");
|
||
|
|
assert.equal(probes, 1);
|
||
|
|
onData?.(armSudoPrompt(sudoAutofillRef.current));
|
||
|
|
sudoAutofillRef.current?.confirmFill();
|
||
|
|
|
||
|
|
assert.deepEqual(sent, ["saved-secret\n"]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startSSH accepts jump host local identity file paths with unreadable saved passwords", async () => {
|
||
|
|
let capturedOptions: Record<string, unknown> | null = null;
|
||
|
|
let error = "";
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async (options: Record<string, unknown>) => {
|
||
|
|
capturedOptions = options;
|
||
|
|
return "ssh-session";
|
||
|
|
},
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => "mosh-session",
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Target",
|
||
|
|
hostname: "target.example.test",
|
||
|
|
username: "alice",
|
||
|
|
hostChain: { hostIds: ["jump-1"] },
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
resolvedChainHosts: [{
|
||
|
|
id: "jump-1",
|
||
|
|
label: "Jump",
|
||
|
|
hostname: "jump.example.test",
|
||
|
|
username: "jumper",
|
||
|
|
authMethod: "key",
|
||
|
|
password: ENCRYPTED_CREDENTIAL_PLACEHOLDER,
|
||
|
|
identityFilePaths: ["/Users/alice/.ssh/jump_ed25519"],
|
||
|
|
}],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: (message: string) => { error = message; },
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startSSH(term as never);
|
||
|
|
|
||
|
|
assert.equal(error, "");
|
||
|
|
assert.ok(capturedOptions);
|
||
|
|
const jumpHosts = capturedOptions.jumpHosts as Array<Record<string, unknown>>;
|
||
|
|
assert.equal(jumpHosts[0]?.password, undefined);
|
||
|
|
assert.deepEqual(jumpHosts[0]?.identityFilePaths, ["/Users/alice/.ssh/jump_ed25519"]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startSSH does not use stale local key paths when selected key material is unavailable", async () => {
|
||
|
|
let capturedOptions: Record<string, unknown> | null = null;
|
||
|
|
let needsAuth = false;
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async (options: Record<string, unknown>) => {
|
||
|
|
capturedOptions = options;
|
||
|
|
return "ssh-session";
|
||
|
|
},
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => "mosh-session",
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Target",
|
||
|
|
hostname: "target.example.test",
|
||
|
|
username: "alice",
|
||
|
|
authMethod: "key",
|
||
|
|
identityFileId: "bad-key",
|
||
|
|
identityFilePaths: ["/Users/alice/.ssh/stale_ed25519"],
|
||
|
|
},
|
||
|
|
keys: [{
|
||
|
|
id: "bad-key",
|
||
|
|
label: "Imported key",
|
||
|
|
source: "imported",
|
||
|
|
privateKey: ENCRYPTED_CREDENTIAL_PLACEHOLDER,
|
||
|
|
}],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: noop,
|
||
|
|
setNeedsAuth: (value: boolean) => { needsAuth = value; },
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startSSH(term as never);
|
||
|
|
|
||
|
|
assert.equal(capturedOptions, null);
|
||
|
|
assert.equal(needsAuth, true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startSSH does not use stale jump host local key paths when selected key material is unavailable", async () => {
|
||
|
|
let capturedOptions: Record<string, unknown> | null = null;
|
||
|
|
let error = "";
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async (options: Record<string, unknown>) => {
|
||
|
|
capturedOptions = options;
|
||
|
|
return "ssh-session";
|
||
|
|
},
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => "mosh-session",
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Target",
|
||
|
|
hostname: "target.example.test",
|
||
|
|
username: "alice",
|
||
|
|
hostChain: { hostIds: ["jump-1"] },
|
||
|
|
},
|
||
|
|
keys: [{
|
||
|
|
id: "bad-jump-key",
|
||
|
|
label: "Jump key",
|
||
|
|
source: "imported",
|
||
|
|
privateKey: ENCRYPTED_CREDENTIAL_PLACEHOLDER,
|
||
|
|
}],
|
||
|
|
resolvedChainHosts: [{
|
||
|
|
id: "jump-1",
|
||
|
|
label: "Jump",
|
||
|
|
hostname: "jump.example.test",
|
||
|
|
username: "jumper",
|
||
|
|
authMethod: "key",
|
||
|
|
identityFileId: "bad-jump-key",
|
||
|
|
identityFilePaths: ["/Users/alice/.ssh/stale_jump_ed25519"],
|
||
|
|
}],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: (message: string) => { error = message; },
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startSSH(term as never);
|
||
|
|
|
||
|
|
assert.equal(capturedOptions, null);
|
||
|
|
assert.match(error, /jump host has saved credentials/i);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh does not pass legacy configured mosh client paths to the backend", async () => {
|
||
|
|
let capturedOptions: Record<string, unknown> | null = null;
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async (options: Record<string, unknown>) => {
|
||
|
|
capturedOptions = options;
|
||
|
|
return "mosh-session";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
port: 2200,
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {
|
||
|
|
terminalEmulationType: "xterm-256color",
|
||
|
|
moshClientPath: "/usr/local/bin/mosh-client",
|
||
|
|
},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null as (() => void) | null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: noop,
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
assert.ok(capturedOptions);
|
||
|
|
assert.equal("moshClientPath" in capturedOptions, false);
|
||
|
|
assert.equal(capturedOptions.hostname, "example.test");
|
||
|
|
assert.equal(capturedOptions.port, 2200);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh passes the saved password to the mosh backend", async () => {
|
||
|
|
let capturedOptions: Record<string, unknown> | null = null;
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async (options: Record<string, unknown>) => {
|
||
|
|
capturedOptions = options;
|
||
|
|
return "mosh-session";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
password: "saved-secret",
|
||
|
|
port: 2200,
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null as (() => void) | null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: noop,
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
assert.ok(capturedOptions);
|
||
|
|
assert.equal(capturedOptions.username, "alice");
|
||
|
|
assert.equal(capturedOptions.password, "saved-secret");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh passes configured key material to the mosh backend", async () => {
|
||
|
|
let capturedOptions: Record<string, unknown> | null = null;
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async (options: Record<string, unknown>) => {
|
||
|
|
capturedOptions = options;
|
||
|
|
return "mosh-session";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
password: "wrong-password",
|
||
|
|
authMethod: "key",
|
||
|
|
identityFileId: "key-1",
|
||
|
|
identityFilePaths: ["/should/not/be/used"],
|
||
|
|
port: 2200,
|
||
|
|
},
|
||
|
|
keys: [{
|
||
|
|
id: "key-1",
|
||
|
|
label: "Deploy key",
|
||
|
|
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nkey\n-----END OPENSSH PRIVATE KEY-----",
|
||
|
|
passphrase: "key-passphrase",
|
||
|
|
}],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: noop,
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
assert.ok(capturedOptions);
|
||
|
|
assert.equal(capturedOptions.password, "wrong-password");
|
||
|
|
assert.equal(capturedOptions.privateKey, "-----BEGIN OPENSSH PRIVATE KEY-----\nkey\n-----END OPENSSH PRIVATE KEY-----");
|
||
|
|
assert.equal(capturedOptions.keyId, "key-1");
|
||
|
|
assert.equal(capturedOptions.passphrase, "key-passphrase");
|
||
|
|
assert.equal(capturedOptions.identityFilePaths, undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh asks for credential re-entry when saved key material cannot be decrypted", async () => {
|
||
|
|
let started = false;
|
||
|
|
let needsAuth = false;
|
||
|
|
let retryMessage: string | null = null;
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => {
|
||
|
|
started = true;
|
||
|
|
return "mosh-session";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
authMethod: "key",
|
||
|
|
identityFileId: "key-1",
|
||
|
|
port: 2200,
|
||
|
|
},
|
||
|
|
keys: [{
|
||
|
|
id: "key-1",
|
||
|
|
label: "Deploy key",
|
||
|
|
privateKey: "enc:v1:djEwdGVzdAAAAAAAAAAAAAAAAA==",
|
||
|
|
}],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: noop,
|
||
|
|
setNeedsAuth: (value: boolean) => { needsAuth = value; },
|
||
|
|
setAuthRetryMessage: (message: string | null) => { retryMessage = message; },
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
assert.equal(started, false);
|
||
|
|
assert.equal(needsAuth, true);
|
||
|
|
assert.match(retryMessage || "", /Saved credentials cannot be decrypted/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh does not use stale local key paths when selected key material is unavailable", async () => {
|
||
|
|
let started = false;
|
||
|
|
let needsAuth = false;
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => {
|
||
|
|
started = true;
|
||
|
|
return "mosh-session";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
authMethod: "key",
|
||
|
|
identityFileId: "key-1",
|
||
|
|
identityFilePaths: ["/Users/alice/.ssh/stale_ed25519"],
|
||
|
|
port: 2200,
|
||
|
|
},
|
||
|
|
keys: [{
|
||
|
|
id: "key-1",
|
||
|
|
label: "Deploy key",
|
||
|
|
privateKey: "enc:v1:djEwdGVzdAAAAAAAAAAAAAAAAA==",
|
||
|
|
}],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null as (() => void) | null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: noop,
|
||
|
|
setNeedsAuth: (value: boolean) => { needsAuth = value; },
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
assert.equal(started, false);
|
||
|
|
assert.equal(needsAuth, true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh omits identity file paths when password auth is explicit", async () => {
|
||
|
|
let capturedOptions: Record<string, unknown> | null = null;
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async (options: Record<string, unknown>) => {
|
||
|
|
capturedOptions = options;
|
||
|
|
return "mosh-session";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
authMethod: "password",
|
||
|
|
requiresMfa: true,
|
||
|
|
password: "saved-secret",
|
||
|
|
identityFilePaths: ["/should/not/be/used"],
|
||
|
|
port: 2200,
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: noop,
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
assert.ok(capturedOptions);
|
||
|
|
assert.equal(capturedOptions.password, "saved-secret");
|
||
|
|
assert.equal(capturedOptions.requiresMfa, true);
|
||
|
|
assert.equal(capturedOptions.identityFilePaths, undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh rejects missing saved proxy profiles", async () => {
|
||
|
|
let started = false;
|
||
|
|
let error = "";
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => {
|
||
|
|
started = true;
|
||
|
|
return "mosh-session";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
port: 2200,
|
||
|
|
proxyProfileId: "missing-proxy",
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: (message: string) => { error = message; },
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
assert.equal(started, false);
|
||
|
|
assert.match(error, /Saved proxy/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh rejects missing proxy identities before the unsupported proxy guard", async () => {
|
||
|
|
let started = false;
|
||
|
|
let error = "";
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => {
|
||
|
|
started = true;
|
||
|
|
return "mosh-session";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
port: 2200,
|
||
|
|
proxyConfig: {
|
||
|
|
type: "http",
|
||
|
|
host: "proxy.example.com",
|
||
|
|
port: 3128,
|
||
|
|
identityId: "missing-identity",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
identities: [],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: (message: string) => { error = message; },
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
assert.equal(started, false);
|
||
|
|
assert.match(error, /Proxy identity/);
|
||
|
|
assert.match(error, /missing/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh rejects incomplete proxy identities before the unsupported proxy guard", async () => {
|
||
|
|
let started = false;
|
||
|
|
let error = "";
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => {
|
||
|
|
started = true;
|
||
|
|
return "mosh-session";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
port: 2200,
|
||
|
|
proxyConfig: {
|
||
|
|
type: "http",
|
||
|
|
host: "proxy.example.com",
|
||
|
|
port: 3128,
|
||
|
|
identityId: "identity-1",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
identities: [{
|
||
|
|
id: "identity-1",
|
||
|
|
label: "Proxy login",
|
||
|
|
username: "proxy-user",
|
||
|
|
authMethod: "password",
|
||
|
|
created: 1,
|
||
|
|
}],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: (message: string) => { error = message; },
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
assert.equal(started, false);
|
||
|
|
assert.match(error, /Proxy identity/);
|
||
|
|
assert.match(error, /incomplete/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh does not connect when a proxy identity password is encrypted", async () => {
|
||
|
|
let started = false;
|
||
|
|
let error = "";
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => {
|
||
|
|
started = true;
|
||
|
|
return "mosh-session";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
port: 2200,
|
||
|
|
proxyConfig: {
|
||
|
|
type: "http",
|
||
|
|
host: "proxy.example.com",
|
||
|
|
port: 3128,
|
||
|
|
identityId: "identity-1",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
identities: [{
|
||
|
|
id: "identity-1",
|
||
|
|
label: "Proxy login",
|
||
|
|
username: "proxy-user",
|
||
|
|
authMethod: "password",
|
||
|
|
password: ENCRYPTED_CREDENTIAL_PLACEHOLDER,
|
||
|
|
created: 1,
|
||
|
|
}],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: (message: string) => { error = message; },
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
assert.equal(started, false);
|
||
|
|
assert.match(error, /Mosh does not support proxy/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh rejects configured proxies instead of connecting directly", async () => {
|
||
|
|
let started = false;
|
||
|
|
let error = "";
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => {
|
||
|
|
started = true;
|
||
|
|
return "mosh-session";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
port: 2200,
|
||
|
|
proxyProfileId: "proxy-1",
|
||
|
|
proxyConfig: { type: "http", host: "proxy.example.com", port: 3128 },
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: (message: string) => { error = message; },
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
assert.equal(started, false);
|
||
|
|
assert.match(error, /Mosh does not support proxy/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh rejects jump host chains instead of connecting directly", async () => {
|
||
|
|
let started = false;
|
||
|
|
let error = "";
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => {
|
||
|
|
started = true;
|
||
|
|
return "mosh-session";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
hostChain: { hostIds: ["jump-1"] },
|
||
|
|
port: 2200,
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
resolvedChainHosts: [{ id: "jump-1", hostname: "jump.example.test" }],
|
||
|
|
sessionId: "session-1",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null },
|
||
|
|
disposeExitRef: { current: null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: (message: string) => { error = message; },
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
assert.equal(started, false);
|
||
|
|
assert.match(error, /Mosh does not support jump host chains/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh defers startup commands until mosh-client is ready", async () => {
|
||
|
|
const sent: string[] = [];
|
||
|
|
let readyCb: ((evt: { sessionId: string }) => void) | null = null;
|
||
|
|
let readyDisposed = false;
|
||
|
|
let dataCb: ((data: string, meta?: { moshHandshake?: boolean }) => void) | null = null;
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => "mosh-session",
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: (_id: string, cb: (data: string, meta?: { moshHandshake?: boolean }) => void) => {
|
||
|
|
dataCb = cb;
|
||
|
|
return noop;
|
||
|
|
},
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onMoshSessionReady: (_id: string, cb: (evt: { sessionId: string }) => void) => {
|
||
|
|
readyCb = cb;
|
||
|
|
return () => {
|
||
|
|
readyDisposed = true;
|
||
|
|
};
|
||
|
|
},
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: (_id: string, data: string) => sent.push(data),
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
identities: [],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
startupCommand: "echo from-snippet",
|
||
|
|
terminalSettings: { startupCommandDelayMs: 0 },
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null as string | null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null as (() => void) | null },
|
||
|
|
disposeExitRef: { current: null as (() => void) | null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: (status: string) => {
|
||
|
|
if (status === "connected") ctx.hasConnectedRef.current = true;
|
||
|
|
},
|
||
|
|
setStatus: noop,
|
||
|
|
setError: noop,
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
modes: {},
|
||
|
|
options: {},
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
|
||
|
|
// Ordinary handshake output must not dismiss the connection overlay before
|
||
|
|
// the interactive Mosh client is ready.
|
||
|
|
dataCb?.("ssh login banner\r\n", { moshHandshake: true });
|
||
|
|
assert.equal(ctx.hasConnectedRef.current, false);
|
||
|
|
assert.deepEqual(sent, []);
|
||
|
|
assert.ok(readyCb, "expected onMoshSessionReady subscription");
|
||
|
|
|
||
|
|
// Hibernate detaches exit listeners without closing the session — ready work
|
||
|
|
// must survive that dispose so startup can still run after wake.
|
||
|
|
ctx.disposeExitRef.current?.();
|
||
|
|
assert.equal(readyDisposed, false);
|
||
|
|
|
||
|
|
readyCb?.({ sessionId: "mosh-session" });
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||
|
|
assert.equal(ctx.hasConnectedRef.current, true);
|
||
|
|
assert.ok(sent.some((chunk) => chunk.includes("echo from-snippet")));
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh disposes ready subscription when startMoshSession rejects", async () => {
|
||
|
|
let readyDisposed = false;
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => {
|
||
|
|
throw new Error("mosh-client missing");
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onMoshSessionReady: () => () => {
|
||
|
|
readyDisposed = true;
|
||
|
|
},
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: noop,
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
identities: [],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
startupCommand: "echo startup",
|
||
|
|
terminalSettings: {},
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null as string | null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null as (() => void) | null },
|
||
|
|
disposeExitRef: { current: null as (() => void) | null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: noop,
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
assert.equal(readyDisposed, true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("startMosh still runs startup when ready fires during startMoshSession await", async () => {
|
||
|
|
const sent: string[] = [];
|
||
|
|
let readyCb: ((evt: { sessionId: string }) => void) | null = null;
|
||
|
|
|
||
|
|
const terminalBackend = {
|
||
|
|
backendAvailable: () => true,
|
||
|
|
telnetAvailable: () => true,
|
||
|
|
moshAvailable: () => true,
|
||
|
|
localAvailable: () => true,
|
||
|
|
serialAvailable: () => true,
|
||
|
|
execAvailable: () => true,
|
||
|
|
startSSHSession: async () => "ssh-session",
|
||
|
|
startTelnetSession: async () => "telnet-session",
|
||
|
|
startMoshSession: async () => {
|
||
|
|
// Simulate a fast handshake that emits ready before the await resumes.
|
||
|
|
readyCb?.({ sessionId: "session-1" });
|
||
|
|
return "session-1";
|
||
|
|
},
|
||
|
|
startLocalSession: async () => "local-session",
|
||
|
|
startSerialSession: async () => "serial-session",
|
||
|
|
execCommand: async () => ({}),
|
||
|
|
onSessionData: () => noop,
|
||
|
|
onSessionExit: () => noop,
|
||
|
|
onMoshSessionReady: (_id: string, cb: (evt: { sessionId: string }) => void) => {
|
||
|
|
readyCb = cb;
|
||
|
|
return noop;
|
||
|
|
},
|
||
|
|
onChainProgress: () => noop,
|
||
|
|
writeToSession: (_id: string, data: string) => sent.push(data),
|
||
|
|
resizeSession: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const ctx = {
|
||
|
|
host: {
|
||
|
|
id: "host-1",
|
||
|
|
label: "Example",
|
||
|
|
hostname: "example.test",
|
||
|
|
username: "alice",
|
||
|
|
},
|
||
|
|
keys: [],
|
||
|
|
identities: [],
|
||
|
|
resolvedChainHosts: [],
|
||
|
|
sessionId: "session-1",
|
||
|
|
startupCommand: "echo startup",
|
||
|
|
terminalSettings: { startupCommandDelayMs: 0 },
|
||
|
|
terminalBackend,
|
||
|
|
sessionRef: { current: null as string | null },
|
||
|
|
hasConnectedRef: { current: false },
|
||
|
|
hasRunStartupCommandRef: { current: false },
|
||
|
|
disposeDataRef: { current: null as (() => void) | null },
|
||
|
|
disposeExitRef: { current: null as (() => void) | null },
|
||
|
|
fitAddonRef: { current: null },
|
||
|
|
serializeAddonRef: { current: null },
|
||
|
|
pendingAuthRef: { current: null },
|
||
|
|
updateStatus: noop,
|
||
|
|
setStatus: noop,
|
||
|
|
setError: noop,
|
||
|
|
setNeedsAuth: noop,
|
||
|
|
setAuthRetryMessage: noop,
|
||
|
|
setAuthPassword: noop,
|
||
|
|
setProgressLogs: noop,
|
||
|
|
setProgressValue: noop,
|
||
|
|
setChainProgress: noop,
|
||
|
|
};
|
||
|
|
|
||
|
|
const term = {
|
||
|
|
cols: 120,
|
||
|
|
rows: 32,
|
||
|
|
write: noop,
|
||
|
|
writeln: noop,
|
||
|
|
scrollToBottom: noop,
|
||
|
|
modes: {},
|
||
|
|
options: {},
|
||
|
|
};
|
||
|
|
|
||
|
|
await createTerminalSessionStarters(ctx as never).startMosh(term as never);
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||
|
|
assert.ok(sent.some((chunk) => chunk.includes("echo startup")));
|
||
|
|
});
|