Files
NetMesh/electron/bridges/sshBridge/moshStatsConnection.test.cjs

943 lines
34 KiB
JavaScript
Raw Permalink Normal View History

const test = require("node:test");
const assert = require("node:assert/strict");
const { EventEmitter } = require("node:events");
const { createMoshStatsConnectionApi } = require("./moshStatsConnection.cjs");
// The connection is created inside an async flow (after credentials are
// resolved, which may touch the filesystem), so the fake SSH client appears a
// few microtasks/immediates after ensureMoshStatsConnection() is called.
function tick() {
return new Promise((resolve) => setImmediate(resolve));
}
// Minimal fake ssh2 Client. Records connect() opts and lets the test drive
// the lifecycle via emitReady / emitError.
class FakeSSHClient extends EventEmitter {
constructor() {
super();
FakeSSHClient.instances.push(this);
this.connectOpts = null;
this.ended = false;
}
connect(opts) {
this.connectOpts = opts;
return this;
}
end() {
this.ended = true;
}
emitReady() {
this.emit("ready");
}
emitError(err) {
this.emit("error", err);
}
}
FakeSSHClient.instances = [];
function makeApi(overrides = {}) {
FakeSSHClient.instances = [];
const sessions = overrides.sessions || new Map();
const logs = [];
const api = createMoshStatsConnectionApi({
get sessions() {
return sessions;
},
SSHClient: overrides.SSHClient || FakeSSHClient,
sshUtils: overrides.sshUtils || {
// Default: treat any non-empty string as a parseable key.
parseKey: (key) => (key && key.length > 0 ? { ok: true } : new Error("bad key")),
},
NetcattyAgent: overrides.NetcattyAgent || class {},
buildAlgorithms: overrides.buildAlgorithms || (() => ({ algos: true })),
getSshAgentSocket: overrides.getSshAgentSocket || (() => null),
prepareSystemSshAgentForAuth: overrides.prepareSystemSshAgentForAuth,
readFileNoFollow: overrides.readFileNoFollow || (async () => null),
expandIdentityFilePath: overrides.expandIdentityFilePath || ((p) => p),
isAutoFillablePasswordChallenge:
overrides.isAutoFillablePasswordChallenge || (() => false),
hostKeyVerifier: overrides.hostKeyVerifier || {
// Default: classify everything as trusted so password tests connect.
describeHostKey: () => ({ keyType: "ssh-ed25519", fingerprint: "fp" }),
classifyHostKey: () => ({ status: "trusted" }),
},
// Default: the system known_hosts vouches for nothing, so trust comes
// solely from the Netcatty classifier above. Individual tests override this
// to exercise the system-known_hosts fallback.
isHostKeyTrustedBySystem:
"isHostKeyTrustedBySystem" in overrides
? overrides.isHostKeyTrustedBySystem
: () => false,
log: (...args) => logs.push(args),
});
return { api, sessions, logs };
}
test("reuses an already-established companion (moshStatsConn) without reconnecting", async () => {
const existing = { exec() {} };
const { api } = makeApi();
const session = { moshStatsConn: existing, moshStatsAuth: { hostname: "h", password: "p" } };
const result = await api.ensureMoshStatsConnection(session, "sid");
assert.equal(result, existing);
assert.equal(FakeSSHClient.instances.length, 0);
});
test("gives up (no connection) when there is no usable non-interactive auth", async () => {
const { api } = makeApi();
// Only an interactively-typed password would have worked; nothing stored.
const session = { moshStatsAuth: { hostname: "h", username: "u" } };
const result = await api.ensureMoshStatsConnection(session, "sid");
assert.equal(result, null);
assert.equal(session.moshStatsConnFailed, true);
assert.equal(FakeSSHClient.instances.length, 0);
});
test("missing moshStatsAuth is transient (handshake not yet swapped), not a permanent failure", async () => {
const { api, sessions } = makeApi();
// Session is connected (renderer polls) but the handshake hasn't swapped to
// mosh-client yet, so moshStatsAuth is not assigned.
const session = {};
sessions.set("sid", session);
const result = await api.ensureMoshStatsConnection(session, "sid");
assert.equal(result, null);
// Must NOT be permanently disabled — a later poll (after the swap sets
// moshStatsAuth) has to be able to establish the companion.
assert.notEqual(session.moshStatsConnFailed, true);
assert.equal(FakeSSHClient.instances.length, 0);
// Once auth becomes available, a subsequent poll connects.
session.moshStatsAuth = { hostname: "h", username: "u", password: "p" };
api.ensureMoshStatsConnection(session, "sid");
await tick();
assert.equal(FakeSSHClient.instances.length, 1);
});
test("connects with a stored password and adopts the connection on ready", async () => {
const { api, sessions } = makeApi();
const session = { moshStatsAuth: { hostname: "example.com", port: 2222, username: "alice", password: "secret" } };
sessions.set("sid", session);
const pending = api.ensureMoshStatsConnection(session, "sid");
await tick();
// One connection attempt with the password and host wired in.
assert.equal(FakeSSHClient.instances.length, 1);
const client = FakeSSHClient.instances[0];
assert.equal(client.connectOpts.host, "example.com");
assert.equal(client.connectOpts.port, 2222);
assert.equal(client.connectOpts.username, "alice");
assert.equal(client.connectOpts.password, "secret");
client.emitReady();
const result = await pending;
assert.equal(result, client);
// Stored ONLY on moshStatsConn, never on session.conn (keeps the companion
// invisible to getSessionPwd / SFTP / MCP exec).
assert.equal(session.moshStatsConn, client);
assert.equal(session.conn, undefined);
});
test("ET stats companion uses ET-specific session fields", async () => {
const { api, sessions } = makeApi();
const session = { etStatsAuth: { hostname: "example.com", port: 2222, username: "alice", password: "secret" } };
sessions.set("sid", session);
const pending = api.ensureEtStatsConnection(session, "sid");
await tick();
assert.equal(FakeSSHClient.instances.length, 1);
const client = FakeSSHClient.instances[0];
assert.equal(client.connectOpts.host, "example.com");
assert.equal(client.connectOpts.port, 2222);
assert.equal(client.connectOpts.username, "alice");
assert.equal(client.connectOpts.password, "secret");
client.emitReady();
const result = await pending;
assert.equal(result, client);
assert.equal(session.etStatsConn, client);
assert.equal(session.moshStatsConn, undefined);
assert.equal(session.conn, undefined);
});
test("ET stats companion caches permanent failures on ET-specific fields", async () => {
const { api, sessions } = makeApi();
const session = { etStatsAuth: { hostname: "h", username: "u", password: "p" } };
sessions.set("sid", session);
const p1 = api.ensureEtStatsConnection(session, "sid");
await tick();
const authErr = new Error("All configured authentication methods failed");
authErr.level = "client-authentication";
FakeSSHClient.instances[0].emitError(authErr);
assert.equal(await p1, null);
assert.equal(session.etStatsConnFailed, true);
const r2 = await api.ensureEtStatsConnection(session, "sid");
assert.equal(r2, null);
assert.equal(FakeSSHClient.instances.length, 1);
});
test("uses a parseable private key and passphrase, not password fallback only", async () => {
const { api, sessions } = makeApi();
const session = {
moshStatsAuth: {
hostname: "h",
username: "u",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nx\n-----END OPENSSH PRIVATE KEY-----",
passphrase: "pw",
},
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
assert.equal(client.connectOpts.privateKey, session.moshStatsAuth.privateKey);
assert.equal(client.connectOpts.passphrase, "pw");
});
test("skips an unparseable (e.g. encrypted, wrong passphrase) private key", async () => {
const { api, sessions } = makeApi({
sshUtils: { parseKey: () => new Error("encrypted") },
});
const session = {
moshStatsAuth: { hostname: "h", username: "u", privateKey: "enc", password: "fallback" },
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
// Falls back to the stored password instead of offering the bad key.
assert.equal(client.connectOpts.privateKey, undefined);
assert.equal(client.connectOpts.password, "fallback");
});
test("reads identity files non-interactively when no inline key is present", async () => {
const reads = [];
const { api, sessions } = makeApi({
readFileNoFollow: async (p) => {
reads.push(p);
return "FILEKEY";
},
sshUtils: { parseKey: (k) => (k === "FILEKEY" ? { ok: true } : new Error("no")) },
});
const session = {
moshStatsAuth: { hostname: "h", username: "u", identityFilePaths: ["~/.ssh/id_ed25519"] },
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
await tick(); // allow chained async identity read + client creation
assert.deepEqual(reads, ["~/.ssh/id_ed25519"]);
const client = FakeSSHClient.instances[0];
assert.equal(client.connectOpts.privateKey, "FILEKEY");
});
test("tries every discovered identity file for automatic stats auth", async () => {
const { api, sessions } = makeApi({
readFileNoFollow: async (p) => p.endsWith("id_first") ? "FIRSTKEY" : "SECONDKEY",
getSshAgentSocket: () => "/tmp/agent.sock",
});
const session = {
moshStatsAuth: {
hostname: "h",
username: "u",
identityFilePaths: ["~/.ssh/id_first", "~/.ssh/id_second"],
},
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
await tick();
const handler = FakeSSHClient.instances[0].connectOpts.authHandler;
const nextMethod = () => {
let method;
handler(null, false, (value) => { method = value; });
return method;
};
assert.equal(nextMethod(), "none");
assert.equal(nextMethod(), "agent");
assert.deepEqual(nextMethod(), {
type: "publickey",
username: "u",
key: "FIRSTKEY",
passphrase: undefined,
});
assert.deepEqual(nextMethod(), {
type: "publickey",
username: "u",
key: "SECONDKEY",
passphrase: undefined,
});
});
test("automatic stats auth keeps local-key fallback with an explicit agent", async () => {
const { api, sessions } = makeApi({
readFileNoFollow: async (p) => p.endsWith("id_first") ? "FIRSTKEY" : "SECONDKEY",
prepareSystemSshAgentForAuth: async () => "/tmp/selected-agent.sock",
});
const session = {
moshStatsAuth: {
hostname: "h",
username: "u",
authMethod: "auto",
useSshAgent: true,
identityFilePaths: ["~/.ssh/id_first", "~/.ssh/id_second"],
},
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
await tick();
const connectOpts = FakeSSHClient.instances[0].connectOpts;
assert.equal(connectOpts.agent, "/tmp/selected-agent.sock");
const offered = drainAuthHandler(connectOpts.authHandler);
assert.equal(offered[0], "none");
assert.equal(offered[1], "agent");
assert.deepEqual(offered.slice(2), [
{ type: "publickey", username: "u", key: "FIRSTKEY", passphrase: undefined },
{ type: "publickey", username: "u", key: "SECONDKEY", passphrase: undefined },
]);
});
test("falls back to ssh-agent when a socket is available and no inline creds", async () => {
// The system ssh used by the Mosh handshake authenticates via the local
// agent by default, so the companion should too — regardless of the
// agentForwarding (remote forwarding) setting.
for (const agentForwarding of [true, false, undefined]) {
const { api, sessions } = makeApi({
getSshAgentSocket: () => "/tmp/agent.sock",
});
const session = { moshStatsAuth: { hostname: "h", username: "u", agentForwarding } };
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
assert.equal(client.connectOpts.agent, "/tmp/agent.sock");
}
});
test("does not attempt a connection when no agent socket and no inline creds", async () => {
const { api } = makeApi({ getSshAgentSocket: () => null });
const session = { moshStatsAuth: { hostname: "h", username: "u" } };
const result = await api.ensureMoshStatsConnection(session, "sid");
assert.equal(result, null);
assert.equal(FakeSSHClient.instances.length, 0);
});
test("enables keyboard-interactive and auto-fills the saved password for a single prompt", async () => {
// Use the real auto-fill predicate so this exercises the actual handler.
const { isAutoFillablePasswordChallenge } = require("../sshAuthHelper.cjs");
const { api, sessions } = makeApi({ isAutoFillablePasswordChallenge });
const session = { moshStatsAuth: { hostname: "h", username: "u", password: "secret" } };
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
assert.equal(client.connectOpts.tryKeyboard, true);
let answered = null;
client.emit(
"keyboard-interactive",
"",
"",
"",
[{ prompt: "Password:", echo: false }],
(responses) => { answered = responses; },
);
assert.deepEqual(answered, ["secret"]);
});
test("keyboard-interactive finishes empty on a 2FA / OTP challenge (no hang, no prompt)", async () => {
const { isAutoFillablePasswordChallenge } = require("../sshAuthHelper.cjs");
const { api, sessions } = makeApi({ isAutoFillablePasswordChallenge });
const session = { moshStatsAuth: { hostname: "h", username: "u", password: "secret" } };
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
let answered = null;
client.emit(
"keyboard-interactive",
"",
"",
"",
[{ prompt: "Verification code:", echo: false }],
(responses) => { answered = responses; },
);
assert.deepEqual(answered, []);
});
test("keyboard-interactive only auto-fills once, then finishes empty to avoid a loop", async () => {
const { isAutoFillablePasswordChallenge } = require("../sshAuthHelper.cjs");
const { api, sessions } = makeApi({ isAutoFillablePasswordChallenge });
const session = { moshStatsAuth: { hostname: "h", username: "u", password: "secret" } };
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
const prompts = [{ prompt: "Password:", echo: false }];
let first = null;
let second = null;
client.emit("keyboard-interactive", "", "", "", prompts, (r) => { first = r; });
client.emit("keyboard-interactive", "", "", "", prompts, (r) => { second = r; });
assert.deepEqual(first, ["secret"]);
assert.deepEqual(second, []); // retry not re-filled with the same wrong password
});
test("does not enable keyboard-interactive when there is no password", async () => {
const { api, sessions } = makeApi();
const session = {
moshStatsAuth: {
hostname: "h",
username: "u",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nx\n-----END OPENSSH PRIVATE KEY-----",
},
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
assert.notEqual(client.connectOpts.tryKeyboard, true);
});
// Drive an ssh2 function-form authHandler to completion, collecting the
// method names it offers. Each call is answered by invoking the callback,
// then we ask for the next method until it yields false (exhausted).
function drainAuthHandler(authHandler) {
const offered = [];
let done = false;
let guard = 0;
while (!done && guard++ < 50) {
let answered = false;
authHandler([], false, (method) => {
answered = true;
if (method === false) {
done = true;
} else {
offered.push(method);
}
});
if (!answered) break;
}
return offered;
}
test("verifier is attached for every auth method; gated authHandler only with a password", async () => {
// Password present -> verifier + gated authHandler attached.
{
const { api, sessions } = makeApi();
const session = { moshStatsAuth: { hostname: "h", username: "u", password: "p" } };
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
assert.equal(typeof FakeSSHClient.instances[0].connectOpts.hostVerifier, "function");
assert.equal(typeof FakeSSHClient.instances[0].connectOpts.authHandler, "function");
}
// Key only -> verifier still attached (the host must be vetted); no authHandler.
{
const { api, sessions } = makeApi();
const session = {
moshStatsAuth: {
hostname: "h",
username: "u",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nx\n-----END OPENSSH PRIVATE KEY-----",
},
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
assert.equal(typeof FakeSSHClient.instances[0].connectOpts.hostVerifier, "function");
assert.equal(FakeSSHClient.instances[0].connectOpts.authHandler, undefined);
}
// Agent only -> verifier still attached; no authHandler.
{
const { api, sessions } = makeApi({ getSshAgentSocket: () => "/tmp/agent.sock" });
const session = { moshStatsAuth: { hostname: "h", username: "u", agentForwarding: true } };
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
assert.equal(typeof FakeSSHClient.instances[0].connectOpts.hostVerifier, "function");
assert.equal(FakeSSHClient.instances[0].connectOpts.authHandler, undefined);
}
});
test("the verifier rejects the transport for an untrusted host (key auth included)", async () => {
const hostKeyVerifier = require("../hostKeyVerifier.cjs");
const { api, sessions } = makeApi({ hostKeyVerifier });
// Untrusted host: empty known-hosts, key auth and NO password — must still be
// refused, even though key auth would leak no reusable secret.
const session = {
moshStatsAuth: {
hostname: "unknown.example.com",
port: 22,
username: "u",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nx\n-----END OPENSSH PRIVATE KEY-----",
knownHosts: [],
},
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const verify = FakeSSHClient.instances[0].connectOpts.hostVerifier;
let accepted = null;
verify(require("node:crypto").randomBytes(32), (ok) => { accepted = ok; });
// Refuses an unvetted host outright — no auth attempted, no stats command run.
assert.equal(accepted, false);
});
test("the gated authHandler offers password ONLY when the host key is trusted", async () => {
const hostKeyVerifier = require("../hostKeyVerifier.cjs");
const rawKey = require("node:crypto").randomBytes(32);
const { keyType, fingerprint } = hostKeyVerifier.describeHostKey(rawKey);
const knownHosts = [
{ id: "k1", hostname: "trusted.example.com", port: 22, keyType, fingerprint, publicKey: "" },
];
// Trusted host: after the verifier runs, password + keyboard-interactive
// are offered.
{
const { api, sessions } = makeApi({ hostKeyVerifier });
const session = {
moshStatsAuth: { hostname: "trusted.example.com", port: 22, username: "u", password: "p", knownHosts },
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const { hostVerifier, authHandler } = FakeSSHClient.instances[0].connectOpts;
hostVerifier(rawKey, () => {}); // verifier runs during transport, sets trust
const offered = drainAuthHandler(authHandler);
assert.ok(offered.includes("password"));
assert.ok(offered.includes("keyboard-interactive"));
}
// Untrusted host: password methods are withheld even though a password is
// saved, so the secret is never sent.
{
const { api, sessions } = makeApi({ hostKeyVerifier });
const session = {
moshStatsAuth: { hostname: "other.example.com", port: 22, username: "u", password: "p", knownHosts },
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const { hostVerifier, authHandler } = FakeSSHClient.instances[0].connectOpts;
hostVerifier(rawKey, () => {});
const offered = drainAuthHandler(authHandler);
assert.ok(!offered.includes("password"));
assert.ok(!offered.includes("keyboard-interactive"));
}
});
test("an explicit private key suppresses the ssh-agent fallback", async () => {
const { api, sessions } = makeApi({ getSshAgentSocket: () => "/tmp/agent.sock" });
const session = {
moshStatsAuth: {
hostname: "h",
username: "u",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nx\n-----END OPENSSH PRIVATE KEY-----",
},
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
assert.ok(client.connectOpts.privateKey);
assert.equal(client.connectOpts.agent, undefined);
});
test("a saved password does NOT suppress agent auth (agent offered alongside password)", async () => {
// A public-key host that authenticates via the agent may still carry a
// stored password; the companion must offer both so agent auth (tried
// first by ssh2) can succeed instead of failing on password only.
const { api, sessions } = makeApi({ getSshAgentSocket: () => "/tmp/agent.sock" });
const session = { moshStatsAuth: { hostname: "h", username: "u", password: "pw" } };
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
assert.equal(client.connectOpts.agent, "/tmp/agent.sock");
assert.equal(client.connectOpts.password, "pw");
});
test("concurrent calls share a single in-flight connection attempt", async () => {
const { api, sessions } = makeApi();
const session = { moshStatsAuth: { hostname: "h", username: "u", password: "p" } };
sessions.set("sid", session);
const p1 = api.ensureMoshStatsConnection(session, "sid");
const p2 = api.ensureMoshStatsConnection(session, "sid");
await tick();
assert.equal(FakeSSHClient.instances.length, 1);
FakeSSHClient.instances[0].emitReady();
const [r1, r2] = await Promise.all([p1, p2]);
assert.equal(r1, FakeSSHClient.instances[0]);
assert.equal(r2, FakeSSHClient.instances[0]);
});
test("auth rejection is permanent: no reconnect on the next poll", async () => {
const { api, sessions } = makeApi();
const session = { moshStatsAuth: { hostname: "h", username: "u", password: "p" } };
sessions.set("sid", session);
const p1 = api.ensureMoshStatsConnection(session, "sid");
await tick();
const authErr = new Error("All configured authentication methods failed");
authErr.level = "client-authentication";
FakeSSHClient.instances[0].emitError(authErr);
assert.equal(await p1, null);
assert.equal(session.moshStatsConnFailed, true);
// Second poll must not open a new connection.
const r2 = await api.ensureMoshStatsConnection(session, "sid");
assert.equal(r2, null);
assert.equal(FakeSSHClient.instances.length, 1);
});
test("a transient error allows a reconnect on the next poll", async () => {
const { api, sessions } = makeApi();
const session = { moshStatsAuth: { hostname: "h", username: "u", password: "p" } };
sessions.set("sid", session);
const p1 = api.ensureMoshStatsConnection(session, "sid");
await tick();
const netErr = new Error("connect ETIMEDOUT");
netErr.level = "client-socket";
FakeSSHClient.instances[0].emitError(netErr);
assert.equal(await p1, null);
assert.notEqual(session.moshStatsConnFailed, true);
// Next poll is allowed to try again.
api.ensureMoshStatsConnection(session, "sid");
await tick();
assert.equal(FakeSSHClient.instances.length, 2);
});
test("a socket that closes mid-handshake settles the attempt instead of hanging", async () => {
const { api, sessions } = makeApi();
const session = { moshStatsAuth: { hostname: "h", username: "u", password: "p" } };
sessions.set("sid", session);
const pending = api.ensureMoshStatsConnection(session, "sid");
await tick();
// Socket drops during the handshake with no prior "ready" or "error".
FakeSSHClient.instances[0].emit("close");
const result = await pending;
assert.equal(result, null);
// Transient — must not permanently disable stats, and the promise must clear.
assert.notEqual(session.moshStatsConnFailed, true);
assert.equal(session.moshStatsConnPromise, null);
// The next poll is allowed to retry.
api.ensureMoshStatsConnection(session, "sid");
await tick();
assert.equal(FakeSSHClient.instances.length, 2);
});
test("a connection that becomes ready after the session closed is discarded", async () => {
const { api, sessions } = makeApi();
const session = { moshStatsAuth: { hostname: "h", username: "u", password: "p" } };
sessions.set("sid", session);
const pending = api.ensureMoshStatsConnection(session, "sid");
await tick();
// Session goes away before the handshake completes.
session.closed = true;
sessions.delete("sid");
FakeSSHClient.instances[0].emitReady();
const result = await pending;
assert.equal(result, null);
assert.equal(FakeSSHClient.instances[0].ended, true);
assert.equal(session.conn, undefined);
assert.equal(session.moshStatsConn, undefined);
});
test("honors host algorithm settings via buildAlgorithms", async () => {
const calls = [];
const { api, sessions } = makeApi({
buildAlgorithms: (legacy, opts) => {
calls.push({ legacy, opts });
return { built: true };
},
});
const overrides = { cipher: ["aes128-cbc"] };
const session = {
moshStatsAuth: {
hostname: "h",
username: "u",
password: "p",
legacyAlgorithms: true,
skipEcdsaHostKey: true,
algorithmOverrides: overrides,
},
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
assert.equal(calls.length, 1);
assert.equal(calls[0].legacy, true);
assert.equal(calls[0].opts.skipEcdsaHostKey, true);
assert.equal(calls[0].opts.algorithmOverrides, overrides);
assert.deepEqual(FakeSSHClient.instances[0].connectOpts.algorithms, { built: true });
});
test("installs a host-key verifier even for key-only auth (no password)", async () => {
const { api, sessions } = makeApi();
const session = {
moshStatsAuth: {
hostname: "h",
username: "u",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nx\n-----END OPENSSH PRIVATE KEY-----",
},
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
// Regression (#1198 review): a background companion must verify the host key
// for EVERY auth method, not only when a password is present.
assert.equal(typeof client.connectOpts.hostVerifier, "function");
});
test("rejects an untrusted host key for key auth and treats it as permanent", async () => {
const { api, sessions } = makeApi({
hostKeyVerifier: {
describeHostKey: () => ({ keyType: "ssh-ed25519", fingerprint: "live-fp" }),
classifyHostKey: () => ({ status: "unknown" }),
},
});
const session = {
moshStatsAuth: {
hostname: "h",
username: "u",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nx\n-----END OPENSSH PRIVATE KEY-----",
},
};
sessions.set("sid", session);
const pending = api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
// The verifier refuses the transport for an unvetted host, even though the
// companion would have authenticated with a key (no reusable secret leaked).
let verdict;
client.connectOpts.hostVerifier(Buffer.from("rawkey"), (ok) => { verdict = ok; });
assert.equal(verdict, false);
// ssh2 then aborts the handshake; an untrusted host must be a permanent
// failure so we don't reconnect (and re-reject) on every stats poll.
client.emitError(Object.assign(new Error("handshake failed"), { level: "protocol" }));
const result = await pending;
assert.equal(result, null);
assert.equal(session.moshStatsConnFailed, true);
});
test("accepts a trusted host key and adopts the connection", async () => {
const { api, sessions } = makeApi({
hostKeyVerifier: {
describeHostKey: () => ({ keyType: "ssh-ed25519", fingerprint: "fp" }),
classifyHostKey: () => ({ status: "trusted" }),
},
});
const session = {
moshStatsAuth: { hostname: "h", username: "u", password: "secret" },
};
sessions.set("sid", session);
const pending = api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
let verdict;
client.connectOpts.hostVerifier(Buffer.from("rawkey"), (ok) => { verdict = ok; });
assert.equal(verdict, true);
client.emitReady();
assert.equal(await pending, client);
});
test("trusts a host vouched for ONLY by the system known_hosts (Netcatty snapshot empty)", async () => {
// Netcatty's in-app vault has no record (classify -> unknown), but the user's
// system OpenSSH known_hosts already trusts the exact live key — which is
// what the Mosh handshake's system ssh actually used. The companion must
// accept it so Mosh stats appear.
const seen = [];
const { api, sessions } = makeApi({
hostKeyVerifier: {
describeHostKey: () => ({ keyType: "ssh-ed25519", fingerprint: "live-fp" }),
classifyHostKey: () => ({ status: "unknown" }),
},
isHostKeyTrustedBySystem: (args) => {
seen.push(args);
return args.hostname === "sys.example.com" && args.fingerprint === "live-fp";
},
});
const session = {
moshStatsAuth: {
hostname: "sys.example.com",
port: 22,
username: "u",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nx\n-----END OPENSSH PRIVATE KEY-----",
knownHosts: [],
},
};
sessions.set("sid", session);
const pending = api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
let verdict;
client.connectOpts.hostVerifier(Buffer.from("rawkey"), (ok) => { verdict = ok; });
assert.equal(verdict, true);
// The system check was consulted with the live key's fingerprint.
assert.equal(seen.length, 1);
assert.equal(seen[0].fingerprint, "live-fp");
client.emitReady();
assert.equal(await pending, client);
});
test("rejects (permanently) when NEITHER Netcatty nor the system known_hosts trust the key", async () => {
const { api, sessions } = makeApi({
hostKeyVerifier: {
describeHostKey: () => ({ keyType: "ssh-ed25519", fingerprint: "live-fp" }),
classifyHostKey: () => ({ status: "unknown" }),
},
isHostKeyTrustedBySystem: () => false,
});
const session = {
moshStatsAuth: {
hostname: "untrusted.example.com",
port: 22,
username: "u",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nx\n-----END OPENSSH PRIVATE KEY-----",
knownHosts: [],
},
};
sessions.set("sid", session);
const pending = api.ensureMoshStatsConnection(session, "sid");
await tick();
const client = FakeSSHClient.instances[0];
let verdict;
client.connectOpts.hostVerifier(Buffer.from("rawkey"), (ok) => { verdict = ok; });
assert.equal(verdict, false);
// ssh2 aborts; an untrusted host is a permanent failure (no re-poll loop).
client.emitError(Object.assign(new Error("handshake failed"), { level: "protocol" }));
assert.equal(await pending, null);
assert.equal(session.moshStatsConnFailed, true);
});
test("the system fallback is NOT consulted when Netcatty already trusts the key", async () => {
// Netcatty says trusted -> accept without even touching the system files.
let consulted = false;
const { api, sessions } = makeApi({
hostKeyVerifier: {
describeHostKey: () => ({ keyType: "ssh-ed25519", fingerprint: "fp" }),
classifyHostKey: () => ({ status: "trusted" }),
},
isHostKeyTrustedBySystem: () => {
consulted = true;
return false;
},
});
const session = { moshStatsAuth: { hostname: "h", username: "u", password: "p", knownHosts: [] } };
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
let verdict;
FakeSSHClient.instances[0].connectOpts.hostVerifier(Buffer.from("rawkey"), (ok) => { verdict = ok; });
assert.equal(verdict, true);
assert.equal(consulted, false);
});
test("a Netcatty 'changed' key is NOT rescued by a non-matching system check (key rotation stays rejected)", async () => {
// Netcatty flags a key rotation (changed). The system check is consulted with
// the LIVE fingerprint; since the system does not record this exact new key,
// it returns false and the connection is refused — the mismatch is never
// silently accepted.
const { api, sessions } = makeApi({
hostKeyVerifier: {
describeHostKey: () => ({ keyType: "ssh-ed25519", fingerprint: "rotated-fp" }),
classifyHostKey: () => ({ status: "changed", expectedFingerprint: "old-fp" }),
},
isHostKeyTrustedBySystem: () => false,
});
const session = { moshStatsAuth: { hostname: "h", username: "u", password: "p", knownHosts: [] } };
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
let verdict;
FakeSSHClient.instances[0].connectOpts.hostVerifier(Buffer.from("rawkey"), (ok) => { verdict = ok; });
assert.equal(verdict, false);
});
test("works when isHostKeyTrustedBySystem is not wired in (optional dependency)", async () => {
// Backward-compat: an api built without the system-known_hosts dependency
// must not throw; it simply falls back to the Netcatty-only decision.
const { api, sessions } = makeApi({
hostKeyVerifier: {
describeHostKey: () => ({ keyType: "ssh-ed25519", fingerprint: "fp" }),
classifyHostKey: () => ({ status: "unknown" }),
},
isHostKeyTrustedBySystem: undefined,
});
const session = {
moshStatsAuth: {
hostname: "h",
username: "u",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nx\n-----END OPENSSH PRIVATE KEY-----",
knownHosts: [],
},
};
sessions.set("sid", session);
api.ensureMoshStatsConnection(session, "sid");
await tick();
let verdict;
FakeSSHClient.instances[0].connectOpts.hostVerifier(Buffer.from("rawkey"), (ok) => { verdict = ok; });
assert.equal(verdict, false);
});