1425 lines
47 KiB
JavaScript
1425 lines
47 KiB
JavaScript
|
|
const test = require("node:test");
|
||
|
|
const assert = require("node:assert/strict");
|
||
|
|
const { EventEmitter } = require("node:events");
|
||
|
|
const fs = require("node:fs");
|
||
|
|
const os = require("node:os");
|
||
|
|
const path = require("node:path");
|
||
|
|
|
||
|
|
const {
|
||
|
|
createConnectionRef,
|
||
|
|
acquireConnectionRef,
|
||
|
|
releaseConnectionRef,
|
||
|
|
transferConnectionRef,
|
||
|
|
consumePendingShellReconnectRisk,
|
||
|
|
findReusableSession,
|
||
|
|
createTransport,
|
||
|
|
borrowTransport,
|
||
|
|
returnTransport,
|
||
|
|
discardTransport,
|
||
|
|
discardAllTransports,
|
||
|
|
findTransportByEndpoint,
|
||
|
|
resolveTransportForReuse,
|
||
|
|
beginTransportDial,
|
||
|
|
waitForTransportDial,
|
||
|
|
completeTransportDial,
|
||
|
|
failTransportDial,
|
||
|
|
getTransportStats,
|
||
|
|
setDefaultTransportIdleTtlMs,
|
||
|
|
getDefaultTransportIdleTtlMs,
|
||
|
|
buildEndpointKey,
|
||
|
|
buildConnectionReuseEndpoint,
|
||
|
|
normalizeEndpoint,
|
||
|
|
endpointAllowsReuse,
|
||
|
|
fingerprintAuth,
|
||
|
|
resetSshTransportRegistryForTests,
|
||
|
|
markEndpointNoIdlePark,
|
||
|
|
endpointAllowsIdlePark,
|
||
|
|
DEFAULT_SSH_TRANSPORT_IDLE_TTL_MS,
|
||
|
|
DEFAULT_MAX_IDLE_SSH_TRANSPORTS,
|
||
|
|
LEASE_KINDS,
|
||
|
|
} = require("./sshConnectionPool.cjs");
|
||
|
|
|
||
|
|
function makeConn() {
|
||
|
|
return {
|
||
|
|
ended: 0,
|
||
|
|
_sock: { destroyed: false },
|
||
|
|
end() { this.ended += 1; },
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeChainConn() {
|
||
|
|
return {
|
||
|
|
ended: 0,
|
||
|
|
end() { this.ended += 1; },
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeLifecycleConn({ emitCloseFromEnd = false } = {}) {
|
||
|
|
const conn = new EventEmitter();
|
||
|
|
conn.ended = 0;
|
||
|
|
conn._sock = { destroyed: false };
|
||
|
|
conn.end = () => {
|
||
|
|
conn.ended += 1;
|
||
|
|
if (emitCloseFromEnd) conn.emit("close");
|
||
|
|
};
|
||
|
|
return conn;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Product default: TTL 0 parks until quit (never auto-reclaim). */
|
||
|
|
function useParkForever() {
|
||
|
|
resetSshTransportRegistryForTests({ defaultIdleTtlMs: 0 });
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Short positive TTL with fake timers so tests can fire idle reclaim.
|
||
|
|
* Returns the timer list.
|
||
|
|
*/
|
||
|
|
function useShortTtlTimers(ttlMs = 1) {
|
||
|
|
const timers = [];
|
||
|
|
resetSshTransportRegistryForTests({
|
||
|
|
defaultIdleTtlMs: ttlMs,
|
||
|
|
setTimeout: (fn, ms) => {
|
||
|
|
const handle = { fn, ms, cleared: false };
|
||
|
|
timers.push(handle);
|
||
|
|
return handle;
|
||
|
|
},
|
||
|
|
clearTimeout: (handle) => {
|
||
|
|
if (handle) handle.cleared = true;
|
||
|
|
},
|
||
|
|
});
|
||
|
|
return timers;
|
||
|
|
}
|
||
|
|
|
||
|
|
function fireIdleTimers(timers) {
|
||
|
|
for (const t of [...timers]) {
|
||
|
|
if (!t.cleared && typeof t.fn === "function") t.fn();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
test.beforeEach(() => {
|
||
|
|
useParkForever();
|
||
|
|
});
|
||
|
|
|
||
|
|
test.afterEach(() => {
|
||
|
|
resetSshTransportRegistryForTests({ defaultIdleTtlMs: 0 });
|
||
|
|
});
|
||
|
|
|
||
|
|
test("concurrent compatible opens share one in-flight physical dial", async () => {
|
||
|
|
const endpoint = {
|
||
|
|
hostId: "host-1",
|
||
|
|
hostname: "same.example",
|
||
|
|
username: "alice",
|
||
|
|
authType: "password",
|
||
|
|
password: "secret",
|
||
|
|
};
|
||
|
|
let physicalDials = 0;
|
||
|
|
let releaseDial;
|
||
|
|
const dialGate = new Promise((resolve) => { releaseDial = resolve; });
|
||
|
|
|
||
|
|
const open = async () => {
|
||
|
|
const coordination = beginTransportDial(endpoint, { kind: "shell" });
|
||
|
|
if (coordination.role === "reuse") return coordination.transport;
|
||
|
|
if (coordination.role === "join") return waitForTransportDial(coordination);
|
||
|
|
|
||
|
|
physicalDials += 1;
|
||
|
|
await dialGate;
|
||
|
|
const transport = createTransport({ conn: makeConn(), endpoint });
|
||
|
|
completeTransportDial(coordination, transport);
|
||
|
|
return transport;
|
||
|
|
};
|
||
|
|
|
||
|
|
const first = open();
|
||
|
|
const second = open();
|
||
|
|
assert.equal(physicalDials, 1, "only the leader may create the physical SSH connection");
|
||
|
|
releaseDial();
|
||
|
|
const [left, right] = await Promise.all([first, second]);
|
||
|
|
assert.equal(left, right);
|
||
|
|
|
||
|
|
const sequential = beginTransportDial(endpoint, { kind: "shell" });
|
||
|
|
assert.equal(sequential.role, "reuse");
|
||
|
|
assert.equal(sequential.transport, left);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("in-flight dials isolate endpoint, profile, credentials, and ForwardAgent policy", () => {
|
||
|
|
const base = {
|
||
|
|
hostId: "host-1",
|
||
|
|
hostname: "same.example",
|
||
|
|
username: "alice",
|
||
|
|
authType: "password",
|
||
|
|
password: "secret",
|
||
|
|
agentForwarding: false,
|
||
|
|
};
|
||
|
|
const leader = beginTransportDial(base, { kind: "shell" });
|
||
|
|
assert.equal(leader.role, "leader");
|
||
|
|
assert.equal(beginTransportDial({ ...base }, { kind: "shell" }).role, "join");
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
beginTransportDial({ ...base, hostname: "other.example" }, { kind: "shell" }).role,
|
||
|
|
"leader",
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
beginTransportDial({ ...base, hostId: "host-2" }, { kind: "shell" }).role,
|
||
|
|
"leader",
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
beginTransportDial({ ...base, password: "rotated" }, { kind: "shell" }).role,
|
||
|
|
"leader",
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
beginTransportDial({ ...base, agentForwarding: true }, { kind: "shell" }).role,
|
||
|
|
"leader",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("automatic key/password dials coalesce only when every possible credential matches", () => {
|
||
|
|
const base = {
|
||
|
|
hostId: "host-auto",
|
||
|
|
hostname: "auto.example",
|
||
|
|
username: "alice",
|
||
|
|
authType: "auto",
|
||
|
|
privateKey: "configured-private-key",
|
||
|
|
password: "fallback-password-v1",
|
||
|
|
};
|
||
|
|
const leader = beginTransportDial(base, { kind: "shell" });
|
||
|
|
assert.equal(leader.role, "leader");
|
||
|
|
assert.equal(
|
||
|
|
beginTransportDial({ ...base }, { kind: "channel" }).role,
|
||
|
|
"join",
|
||
|
|
"identical automatic opens should still share one pending physical dial",
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
beginTransportDial({ ...base, password: "fallback-password-v2" }, { kind: "channel" }).role,
|
||
|
|
"leader",
|
||
|
|
"a rotated fallback password must not join a dial whose final auth method is not known yet",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("channel opens can join a ForwardAgent leader but not the reverse", () => {
|
||
|
|
const base = {
|
||
|
|
hostId: "host-1",
|
||
|
|
hostname: "same.example",
|
||
|
|
username: "alice",
|
||
|
|
};
|
||
|
|
const forwardAgentLeader = beginTransportDial(
|
||
|
|
{ ...base, agentForwarding: true },
|
||
|
|
{ kind: "shell" },
|
||
|
|
);
|
||
|
|
assert.equal(forwardAgentLeader.role, "leader");
|
||
|
|
assert.equal(
|
||
|
|
beginTransportDial({ ...base, agentForwarding: false }, { kind: "channel" }).role,
|
||
|
|
"join",
|
||
|
|
);
|
||
|
|
|
||
|
|
resetSshTransportRegistryForTests({ defaultIdleTtlMs: 0 });
|
||
|
|
const noForwardAgentLeader = beginTransportDial(
|
||
|
|
{ ...base, agentForwarding: false },
|
||
|
|
{ kind: "channel" },
|
||
|
|
);
|
||
|
|
assert.equal(noForwardAgentLeader.role, "leader");
|
||
|
|
assert.equal(
|
||
|
|
beginTransportDial({ ...base, agentForwarding: true }, { kind: "shell" }).role,
|
||
|
|
"leader",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("a channel waiter can use a leader transport that negotiated ForwardAgent off", async () => {
|
||
|
|
const requestedByLeader = {
|
||
|
|
hostId: "host-agent-downgrade",
|
||
|
|
hostname: "agent-downgrade.example",
|
||
|
|
username: "alice",
|
||
|
|
agentForwarding: true,
|
||
|
|
};
|
||
|
|
const leader = beginTransportDial(requestedByLeader, { kind: "shell" });
|
||
|
|
const channelWaiter = beginTransportDial({
|
||
|
|
...requestedByLeader,
|
||
|
|
agentForwarding: false,
|
||
|
|
}, { kind: "channel" });
|
||
|
|
assert.equal(channelWaiter.role, "join");
|
||
|
|
|
||
|
|
const transport = createTransport({
|
||
|
|
conn: makeConn(),
|
||
|
|
endpoint: { ...requestedByLeader, agentForwarding: false },
|
||
|
|
});
|
||
|
|
assert.equal(completeTransportDial(leader, transport), true);
|
||
|
|
assert.equal(await waitForTransportDial(channelWaiter), transport);
|
||
|
|
await assert.rejects(waitForTransportDial(leader), /incompatible/i);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("cancelled in-flight waiter does not cancel the shared physical dial", async () => {
|
||
|
|
const endpoint = { hostname: "same.example", username: "alice" };
|
||
|
|
const leader = beginTransportDial(endpoint, { kind: "shell" });
|
||
|
|
const joined = beginTransportDial(endpoint, { kind: "shell" });
|
||
|
|
const controller = new AbortController();
|
||
|
|
const waiting = waitForTransportDial(joined, { signal: controller.signal });
|
||
|
|
controller.abort(new Error("caller cancelled"));
|
||
|
|
await assert.rejects(waiting, /caller cancelled/);
|
||
|
|
|
||
|
|
const transport = createTransport({ conn: makeConn(), endpoint });
|
||
|
|
assert.equal(completeTransportDial(leader, transport), true);
|
||
|
|
assert.equal(beginTransportDial(endpoint, { kind: "shell" }).transport, transport);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("failed in-flight dial releases the slot so a later open can retry", async () => {
|
||
|
|
const endpoint = { hostname: "retry.example", username: "alice" };
|
||
|
|
const leader = beginTransportDial(endpoint, { kind: "shell" });
|
||
|
|
const joined = beginTransportDial(endpoint, { kind: "shell" });
|
||
|
|
const waiting = waitForTransportDial(joined);
|
||
|
|
assert.equal(failTransportDial(leader, new Error("authentication failed")), true);
|
||
|
|
await assert.rejects(waiting, /authentication failed/);
|
||
|
|
|
||
|
|
const retry = beginTransportDial(endpoint, { kind: "shell" });
|
||
|
|
assert.equal(retry.role, "leader");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("TERM-SSHD last lease ends the transport instead of idle-parking", () => {
|
||
|
|
const conn = makeConn();
|
||
|
|
conn._remoteVer = "TERM-SSHD";
|
||
|
|
const owner = { remoteSshVersion: "TERM-SSHD" };
|
||
|
|
const connRef = createConnectionRef(owner, conn, []);
|
||
|
|
assert.equal(connRef.allowIdlePark, false);
|
||
|
|
assert.equal(endpointAllowsIdlePark(connRef.endpoint, "TERM-SSHD"), false);
|
||
|
|
|
||
|
|
const ended = releaseConnectionRef(owner);
|
||
|
|
assert.equal(ended, true);
|
||
|
|
assert.equal(conn.ended, 1);
|
||
|
|
assert.equal(connRef.state, "dead");
|
||
|
|
assert.equal(connRef.endedReason, "no-idle-park");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("TERM-SSHD last shell with an SFTP lease refuses later shell reuse", () => {
|
||
|
|
const conn = makeConn();
|
||
|
|
conn._remoteVer = "TERM-SSHD";
|
||
|
|
const endpoint = { hostname: "blj.yd.com.cn", username: "test", port: 22 };
|
||
|
|
const owner = { stream: {}, remoteSshVersion: "TERM-SSHD", _reuseEndpoint: endpoint };
|
||
|
|
const sftp = { id: "sftp", __sshLeaseKind: "sftp" };
|
||
|
|
const transport = createConnectionRef(owner, conn, []);
|
||
|
|
acquireConnectionRef(sftp, transport);
|
||
|
|
assert.equal(releaseConnectionRef(owner), false);
|
||
|
|
assert.equal(transport.state, "live");
|
||
|
|
assert.equal(transport.allowShellReuse, false);
|
||
|
|
assert.equal(findTransportByEndpoint(transport.endpoint, { kind: "shell" }), null);
|
||
|
|
assert.equal(findTransportByEndpoint(transport.endpoint, { kind: "channel" }), transport);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("an endpoint denylisted after a dead parked shell does not idle-park later transports", () => {
|
||
|
|
const endpoint = { hostname: "bastion.example", username: "alice", port: 22 };
|
||
|
|
markEndpointNoIdlePark(endpoint);
|
||
|
|
assert.equal(endpointAllowsIdlePark(endpoint, "OpenSSH_9.0"), false);
|
||
|
|
|
||
|
|
const conn = makeConn();
|
||
|
|
conn._remoteVer = "OpenSSH_9.0";
|
||
|
|
const transport = createTransport({ conn, endpoint });
|
||
|
|
assert.equal(transport.allowIdlePark, false);
|
||
|
|
|
||
|
|
const holder = {};
|
||
|
|
borrowTransport(transport, { kind: "shell", holder });
|
||
|
|
assert.equal(releaseConnectionRef(holder), true);
|
||
|
|
assert.equal(transport.state, "dead");
|
||
|
|
assert.equal(conn.ended, 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("releaseConnectionRef parks on last channel when TTL is 0 (until quit)", () => {
|
||
|
|
const conn = makeConn();
|
||
|
|
const chain = [makeChainConn(), makeChainConn()];
|
||
|
|
const owner = {};
|
||
|
|
const reused = {};
|
||
|
|
|
||
|
|
const connRef = createConnectionRef(owner, conn, chain);
|
||
|
|
assert.equal(connRef.count, 1);
|
||
|
|
|
||
|
|
acquireConnectionRef(reused, connRef);
|
||
|
|
assert.equal(connRef.count, 2);
|
||
|
|
assert.equal(reused.connRef, connRef);
|
||
|
|
|
||
|
|
let ended = releaseConnectionRef(reused);
|
||
|
|
assert.equal(ended, false);
|
||
|
|
assert.equal(conn.ended, 0);
|
||
|
|
assert.equal(reused.connRef, null);
|
||
|
|
|
||
|
|
// Last lease parks (TTL 0 = never auto-end).
|
||
|
|
ended = releaseConnectionRef(owner);
|
||
|
|
assert.equal(ended, false);
|
||
|
|
assert.equal(conn.ended, 0);
|
||
|
|
assert.equal(connRef.state, "idle");
|
||
|
|
assert.equal(owner.connRef, null);
|
||
|
|
|
||
|
|
assert.equal(discardTransport(connRef), true);
|
||
|
|
assert.equal(conn.ended, 1);
|
||
|
|
assert.equal(chain[0].ended, 1);
|
||
|
|
assert.equal(chain[1].ended, 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("releaseConnectionRef keeps siblings alive when the owner closes first", () => {
|
||
|
|
const timers = useShortTtlTimers(1);
|
||
|
|
const conn = makeConn();
|
||
|
|
const owner = {};
|
||
|
|
const reused = {};
|
||
|
|
const connRef = createConnectionRef(owner, conn, []);
|
||
|
|
acquireConnectionRef(reused, connRef);
|
||
|
|
|
||
|
|
assert.equal(releaseConnectionRef(owner), false);
|
||
|
|
assert.equal(conn.ended, 0, "connection must survive for the remaining copy");
|
||
|
|
|
||
|
|
// Last holder parks; fire TTL to end.
|
||
|
|
assert.equal(releaseConnectionRef(reused), false);
|
||
|
|
assert.equal(connRef.state, "idle");
|
||
|
|
fireIdleTimers(timers);
|
||
|
|
assert.equal(conn.ended, 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("releaseConnectionRef is idempotent per session", () => {
|
||
|
|
const conn = makeConn();
|
||
|
|
const owner = {};
|
||
|
|
const connRef = createConnectionRef(owner, conn, []);
|
||
|
|
acquireConnectionRef({}, connRef); // bump count to 2 so a double release can't reach 0 by itself
|
||
|
|
|
||
|
|
assert.equal(releaseConnectionRef(owner), false);
|
||
|
|
assert.equal(releaseConnectionRef(owner), false);
|
||
|
|
assert.equal(connRef.count, 1);
|
||
|
|
assert.equal(conn.ended, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("releaseConnectionRef on a session without a descriptor is a safe no-op", () => {
|
||
|
|
assert.equal(releaseConnectionRef({}), false);
|
||
|
|
assert.equal(releaseConnectionRef(null), false);
|
||
|
|
assert.equal(releaseConnectionRef(undefined), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("last shell close marks reconnect risk while an SFTP lease keeps the transport live", () => {
|
||
|
|
const owner = { id: "shell", stream: {} };
|
||
|
|
const sftp = { id: "sftp", __sshLeaseKind: "sftp" };
|
||
|
|
const transport = createConnectionRef(owner, makeConn(), []);
|
||
|
|
acquireConnectionRef(sftp, transport);
|
||
|
|
|
||
|
|
assert.equal(releaseConnectionRef(owner), false);
|
||
|
|
assert.deepEqual(consumePendingShellReconnectRisk(transport), {
|
||
|
|
oldShellPids: [],
|
||
|
|
hasUnknownOldShell: true,
|
||
|
|
});
|
||
|
|
assert.equal(consumePendingShellReconnectRisk(transport), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("single-channel connection parks on release when TTL is 0", () => {
|
||
|
|
const conn = makeConn();
|
||
|
|
const chain = [makeChainConn()];
|
||
|
|
const owner = {};
|
||
|
|
const transport = createConnectionRef(owner, conn, chain);
|
||
|
|
|
||
|
|
assert.equal(releaseConnectionRef(owner), false);
|
||
|
|
assert.equal(transport.state, "idle");
|
||
|
|
assert.equal(conn.ended, 0);
|
||
|
|
assert.equal(discardTransport(transport), true);
|
||
|
|
assert.equal(conn.ended, 1);
|
||
|
|
assert.equal(chain[0].ended, 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("findReusableSession returns a live interactive SSH shell session", () => {
|
||
|
|
const sessions = new Map();
|
||
|
|
const source = {
|
||
|
|
conn: { _sock: { destroyed: false } },
|
||
|
|
stream: {},
|
||
|
|
connRef: { count: 1, state: "live" },
|
||
|
|
};
|
||
|
|
sessions.set("src", source);
|
||
|
|
|
||
|
|
assert.equal(findReusableSession(sessions, "src"), source);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("findReusableSession rejects sessions missing a usable connection", () => {
|
||
|
|
const sessions = new Map();
|
||
|
|
|
||
|
|
sessions.set("no-stream", { conn: {}, connRef: { count: 1, state: "live" } });
|
||
|
|
assert.equal(findReusableSession(sessions, "no-stream"), null);
|
||
|
|
|
||
|
|
sessions.set("no-ref", { conn: {}, stream: {} });
|
||
|
|
assert.equal(findReusableSession(sessions, "no-ref"), null);
|
||
|
|
|
||
|
|
sessions.set("no-conn", { stream: {}, connRef: { count: 1, state: "live" } });
|
||
|
|
assert.equal(findReusableSession(sessions, "no-conn"), null);
|
||
|
|
|
||
|
|
sessions.set("dead", {
|
||
|
|
conn: { _sock: { destroyed: true } },
|
||
|
|
stream: {},
|
||
|
|
connRef: { count: 1, state: "live" },
|
||
|
|
});
|
||
|
|
assert.equal(findReusableSession(sessions, "dead"), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("findReusableSession handles missing inputs gracefully", () => {
|
||
|
|
assert.equal(findReusableSession(null, "x"), null);
|
||
|
|
assert.equal(findReusableSession(new Map(), ""), null);
|
||
|
|
assert.equal(findReusableSession(new Map(), "absent"), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("findReusableSession enforces an exact target endpoint match", () => {
|
||
|
|
const sessions = new Map();
|
||
|
|
const source = {
|
||
|
|
conn: { _sock: { destroyed: false } },
|
||
|
|
stream: {},
|
||
|
|
connRef: { count: 1, state: "live" },
|
||
|
|
_reuseEndpoint: { hostname: "10.0.0.1", port: 22, username: "alice" },
|
||
|
|
};
|
||
|
|
sessions.set("src", source);
|
||
|
|
|
||
|
|
assert.equal(
|
||
|
|
findReusableSession(sessions, "src", { hostname: "10.0.0.1", port: 22, username: "alice" }),
|
||
|
|
source,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
findReusableSession(sessions, "src", { hostname: "10.0.0.1", username: "alice" }),
|
||
|
|
source,
|
||
|
|
);
|
||
|
|
assert.equal(findReusableSession(sessions, "src", { hostname: "10.0.0.2", port: 22, username: "alice" }), null);
|
||
|
|
assert.equal(findReusableSession(sessions, "src", { hostname: "10.0.0.1", port: 2222, username: "alice" }), null);
|
||
|
|
assert.equal(findReusableSession(sessions, "src", { hostname: "10.0.0.1", port: 22, username: "bob" }), null);
|
||
|
|
|
||
|
|
sessions.set("root-src", {
|
||
|
|
conn: { _sock: { destroyed: false } },
|
||
|
|
stream: {},
|
||
|
|
connRef: { count: 1, state: "live" },
|
||
|
|
_reuseEndpoint: { hostname: "10.0.0.9", port: 22, username: "root" },
|
||
|
|
});
|
||
|
|
assert.ok(findReusableSession(sessions, "root-src", { hostname: "10.0.0.9" }));
|
||
|
|
});
|
||
|
|
|
||
|
|
test("findReusableSession refuses reuse when the source has no recorded endpoint", () => {
|
||
|
|
const sessions = new Map();
|
||
|
|
sessions.set("src", {
|
||
|
|
conn: { _sock: { destroyed: false } },
|
||
|
|
stream: {},
|
||
|
|
connRef: { count: 1, state: "live" },
|
||
|
|
});
|
||
|
|
assert.equal(findReusableSession(sessions, "src", { hostname: "10.0.0.1" }), null);
|
||
|
|
assert.ok(findReusableSession(sessions, "src"));
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Transport registry + idle park
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
test("default idle TTL constant is 5 minutes", () => {
|
||
|
|
assert.equal(DEFAULT_SSH_TRANSPORT_IDLE_TTL_MS, 5 * 60_000);
|
||
|
|
assert.equal(DEFAULT_MAX_IDLE_SSH_TRANSPORTS, 128);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("TTL-zero idle pool enforces a hard global limit without ending active transports", () => {
|
||
|
|
resetSshTransportRegistryForTests({
|
||
|
|
defaultIdleTtlMs: 0,
|
||
|
|
maxIdleTransports: 3,
|
||
|
|
});
|
||
|
|
|
||
|
|
const activeConn = makeConn();
|
||
|
|
const active = createTransport({
|
||
|
|
conn: activeConn,
|
||
|
|
endpoint: { hostname: "active.example", username: "root" },
|
||
|
|
});
|
||
|
|
borrowTransport(active, { holder: {} });
|
||
|
|
|
||
|
|
const parked = [];
|
||
|
|
for (let index = 0; index < 5; index += 1) {
|
||
|
|
const conn = makeConn();
|
||
|
|
const holder = {};
|
||
|
|
const transport = createTransport({
|
||
|
|
conn,
|
||
|
|
endpoint: { hostname: `idle-${index}.example`, username: "root" },
|
||
|
|
});
|
||
|
|
borrowTransport(transport, { holder });
|
||
|
|
returnTransport(holder);
|
||
|
|
parked.push({ conn, transport });
|
||
|
|
}
|
||
|
|
|
||
|
|
assert.deepEqual(getTransportStats(), {
|
||
|
|
transports: 4,
|
||
|
|
pendingDials: 0,
|
||
|
|
live: 1,
|
||
|
|
idle: 3,
|
||
|
|
leases: 1,
|
||
|
|
defaultIdleTtlMs: 0,
|
||
|
|
});
|
||
|
|
assert.equal(active.state, "live");
|
||
|
|
assert.equal(activeConn.ended, 0, "a leased transport must never be evicted");
|
||
|
|
assert.deepEqual(parked.map(({ conn }) => conn.ended), [1, 1, 0, 0, 0]);
|
||
|
|
assert.deepEqual(parked.map(({ transport }) => transport.state), [
|
||
|
|
"dead",
|
||
|
|
"dead",
|
||
|
|
"idle",
|
||
|
|
"idle",
|
||
|
|
"idle",
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("idle endpoint hits refresh LRU order before the next eviction", () => {
|
||
|
|
resetSshTransportRegistryForTests({
|
||
|
|
defaultIdleTtlMs: 0,
|
||
|
|
maxIdleTransports: 2,
|
||
|
|
});
|
||
|
|
|
||
|
|
const park = (hostname) => {
|
||
|
|
const endpoint = { hostname, username: "root" };
|
||
|
|
const conn = makeConn();
|
||
|
|
const holder = {};
|
||
|
|
const transport = createTransport({ conn, endpoint });
|
||
|
|
borrowTransport(transport, { holder });
|
||
|
|
returnTransport(holder);
|
||
|
|
return { conn, endpoint, transport };
|
||
|
|
};
|
||
|
|
|
||
|
|
const first = park("first.example");
|
||
|
|
const second = park("second.example");
|
||
|
|
assert.equal(findTransportByEndpoint(first.endpoint), first.transport);
|
||
|
|
const third = park("third.example");
|
||
|
|
|
||
|
|
assert.equal(first.conn.ended, 0);
|
||
|
|
assert.equal(second.conn.ended, 1, "the untouched oldest idle transport is evicted");
|
||
|
|
assert.equal(third.conn.ended, 0);
|
||
|
|
assert.equal(findTransportByEndpoint(first.endpoint), first.transport);
|
||
|
|
assert.equal(findTransportByEndpoint(second.endpoint), null);
|
||
|
|
assert.equal(findTransportByEndpoint(third.endpoint), third.transport);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("borrow and return make a previously idle transport the newest LRU entry", () => {
|
||
|
|
resetSshTransportRegistryForTests({
|
||
|
|
defaultIdleTtlMs: 0,
|
||
|
|
maxIdleTransports: 2,
|
||
|
|
});
|
||
|
|
|
||
|
|
const park = (hostname) => {
|
||
|
|
const conn = makeConn();
|
||
|
|
const holder = {};
|
||
|
|
const transport = createTransport({
|
||
|
|
conn,
|
||
|
|
endpoint: { hostname, username: "root" },
|
||
|
|
});
|
||
|
|
borrowTransport(transport, { holder });
|
||
|
|
returnTransport(holder);
|
||
|
|
return { conn, transport };
|
||
|
|
};
|
||
|
|
|
||
|
|
const first = park("first.example");
|
||
|
|
const second = park("second.example");
|
||
|
|
const borrowed = {};
|
||
|
|
borrowTransport(first.transport, { holder: borrowed });
|
||
|
|
const third = park("third.example");
|
||
|
|
|
||
|
|
assert.equal(second.conn.ended, 0, "capacity ignores the active borrowed transport");
|
||
|
|
returnTransport(borrowed);
|
||
|
|
|
||
|
|
assert.equal(first.conn.ended, 0);
|
||
|
|
assert.equal(second.conn.ended, 1, "returning the borrowed transport evicts the oldest idle one");
|
||
|
|
assert.equal(third.conn.ended, 0);
|
||
|
|
assert.equal(first.transport.state, "idle");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildEndpointKey normalizes port and username defaults", () => {
|
||
|
|
assert.equal(
|
||
|
|
buildEndpointKey({ hostname: "a.example", port: 22, username: "root" }),
|
||
|
|
buildEndpointKey({ hostname: "a.example" }),
|
||
|
|
);
|
||
|
|
assert.notEqual(
|
||
|
|
buildEndpointKey({ hostname: "a.example", username: "alice" }),
|
||
|
|
buildEndpointKey({ hostname: "a.example", username: "bob" }),
|
||
|
|
);
|
||
|
|
assert.notEqual(
|
||
|
|
buildEndpointKey({ hostname: "a.example", jumpFingerprint: "bastion" }),
|
||
|
|
buildEndpointKey({ hostname: "a.example", jumpFingerprint: "other" }),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildEndpointKey distinguishes jump host chains", () => {
|
||
|
|
assert.notEqual(
|
||
|
|
buildEndpointKey({
|
||
|
|
hostname: "target",
|
||
|
|
jumpHosts: [{ hostname: "bastion-a", port: 22, username: "j" }],
|
||
|
|
}),
|
||
|
|
buildEndpointKey({
|
||
|
|
hostname: "target",
|
||
|
|
jumpHosts: [{ hostname: "bastion-b", port: 22, username: "j" }],
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("buildEndpointKey scopes vault hostId so different profiles never share", () => {
|
||
|
|
assert.notEqual(
|
||
|
|
buildEndpointKey({ hostId: "host-a", hostname: "same.example", username: "root" }),
|
||
|
|
buildEndpointKey({ hostId: "host-b", hostname: "same.example", username: "root" }),
|
||
|
|
);
|
||
|
|
// Missing hostId still indexes under a distinct profile slot ("-").
|
||
|
|
assert.notEqual(
|
||
|
|
buildEndpointKey({ hostname: "same.example", username: "root" }),
|
||
|
|
buildEndpointKey({ hostId: "host-a", hostname: "same.example", username: "root" }),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("fingerprintAuth changes when credential material rotates under same keyId", () => {
|
||
|
|
const keyBase = {
|
||
|
|
hostname: "h.example",
|
||
|
|
username: "alice",
|
||
|
|
authType: "key",
|
||
|
|
keyId: "key-1",
|
||
|
|
};
|
||
|
|
assert.notEqual(
|
||
|
|
fingerprintAuth({ ...keyBase, privateKey: "-----BEGIN OLD-----" }),
|
||
|
|
fingerprintAuth({ ...keyBase, privateKey: "-----BEGIN NEW-----" }),
|
||
|
|
);
|
||
|
|
assert.notEqual(
|
||
|
|
fingerprintAuth({ authType: "password", password: "secret-a" }),
|
||
|
|
fingerprintAuth({ authType: "password", password: "secret-b" }),
|
||
|
|
);
|
||
|
|
assert.notEqual(
|
||
|
|
fingerprintAuth({ authType: "certificate", certificate: "old-cert", privateKey: "k" }),
|
||
|
|
fingerprintAuth({ authType: "certificate", certificate: "new-cert", privateKey: "k" }),
|
||
|
|
);
|
||
|
|
// Same material is stable.
|
||
|
|
assert.equal(
|
||
|
|
fingerprintAuth({ ...keyBase, privateKey: "same" }),
|
||
|
|
fingerprintAuth({ ...keyBase, privateKey: "same" }),
|
||
|
|
);
|
||
|
|
// Automatic auth fingerprints every credential that may be selected. This
|
||
|
|
// remains stable for an unchanged key/password policy and invalidates when
|
||
|
|
// either the key or its fallback password rotates.
|
||
|
|
const autoWithPub = {
|
||
|
|
authType: "auto",
|
||
|
|
keyId: "k1",
|
||
|
|
publicKey: "ssh-ed25519 AAAA",
|
||
|
|
privateKey: "-----BEGIN-----",
|
||
|
|
password: "pw",
|
||
|
|
};
|
||
|
|
assert.equal(
|
||
|
|
fingerprintAuth(autoWithPub),
|
||
|
|
fingerprintAuth({ ...autoWithPub }),
|
||
|
|
);
|
||
|
|
assert.notEqual(
|
||
|
|
fingerprintAuth(autoWithPub),
|
||
|
|
fingerprintAuth({ ...autoWithPub, password: "rotated-password" }),
|
||
|
|
);
|
||
|
|
// password-only auto: password rotation invalidates.
|
||
|
|
assert.notEqual(
|
||
|
|
fingerprintAuth({ authType: "auto", password: "old" }),
|
||
|
|
fingerprintAuth({ authType: "auto", password: "new" }),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("forwarding socket changes invalidate shell reuse without blocking channel reuse", () => {
|
||
|
|
const base = {
|
||
|
|
hostname: "h.example",
|
||
|
|
username: "alice",
|
||
|
|
authType: "password",
|
||
|
|
password: "secret",
|
||
|
|
agentForwarding: true,
|
||
|
|
};
|
||
|
|
const apple = { ...base, forwardingAgentSocket: "/tmp/apple-agent.sock" };
|
||
|
|
const bitwarden = {
|
||
|
|
...base,
|
||
|
|
forwardingAgentSocket: "/Users/alice/.bitwarden-ssh-agent.sock",
|
||
|
|
};
|
||
|
|
assert.equal(
|
||
|
|
fingerprintAuth(apple),
|
||
|
|
fingerprintAuth(bitwarden),
|
||
|
|
);
|
||
|
|
assert.equal(buildEndpointKey(apple), buildEndpointKey(bitwarden));
|
||
|
|
assert.equal(endpointAllowsReuse(bitwarden, apple, "shell"), false);
|
||
|
|
assert.equal(endpointAllowsReuse(
|
||
|
|
{ ...base, agentForwarding: false },
|
||
|
|
apple,
|
||
|
|
"channel",
|
||
|
|
), true);
|
||
|
|
|
||
|
|
resetSshTransportRegistryForTests({ defaultIdleTtlMs: 0 });
|
||
|
|
const transport = createTransport({ conn: makeConn(), endpoint: apple });
|
||
|
|
assert.equal(
|
||
|
|
findTransportByEndpoint({ ...base, agentForwarding: false }),
|
||
|
|
transport,
|
||
|
|
);
|
||
|
|
assert.equal(findTransportByEndpoint(bitwarden, { kind: "shell" }), null);
|
||
|
|
resetSshTransportRegistryForTests({ defaultIdleTtlMs: 0 });
|
||
|
|
});
|
||
|
|
|
||
|
|
test("automatic key success reuses an unchanged endpoint without exposing credentials", () => {
|
||
|
|
const endpoint = buildConnectionReuseEndpoint({
|
||
|
|
hostId: "auto-key-profile",
|
||
|
|
hostname: "auto-key.example",
|
||
|
|
username: "alice",
|
||
|
|
authMethod: "auto",
|
||
|
|
privateKey: "private-key-secret",
|
||
|
|
publicKey: "ssh-ed25519 public-key-material",
|
||
|
|
password: "unused-fallback-secret",
|
||
|
|
});
|
||
|
|
const endpointKey = buildEndpointKey(endpoint);
|
||
|
|
const transport = createTransport({ conn: makeConn(), endpoint });
|
||
|
|
|
||
|
|
assert.equal(findTransportByEndpoint({ ...endpoint }), transport);
|
||
|
|
assert.equal(beginTransportDial({ ...endpoint }, { kind: "shell" }).role, "reuse");
|
||
|
|
for (const secret of ["private-key-secret", "public-key-material", "unused-fallback-secret"]) {
|
||
|
|
assert.equal(endpointKey.includes(secret), false);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("TTL-zero automatic password fallback is not reused after password rotation", () => {
|
||
|
|
const firstEndpoint = buildConnectionReuseEndpoint({
|
||
|
|
hostId: "auto-fallback-profile",
|
||
|
|
hostname: "auto-fallback.example",
|
||
|
|
username: "alice",
|
||
|
|
authMethod: "auto",
|
||
|
|
privateKey: "rejected-private-key",
|
||
|
|
password: "accepted-password-v1",
|
||
|
|
});
|
||
|
|
const rotatedEndpoint = buildConnectionReuseEndpoint({
|
||
|
|
hostId: "auto-fallback-profile",
|
||
|
|
hostname: "auto-fallback.example",
|
||
|
|
username: "alice",
|
||
|
|
authMethod: "auto",
|
||
|
|
privateKey: "rejected-private-key",
|
||
|
|
password: "accepted-password-v2",
|
||
|
|
});
|
||
|
|
const conn = makeConn();
|
||
|
|
const holder = {};
|
||
|
|
const transport = createTransport({ conn, endpoint: firstEndpoint });
|
||
|
|
borrowTransport(transport, { kind: "shell", holder });
|
||
|
|
returnTransport(holder);
|
||
|
|
|
||
|
|
assert.equal(transport.state, "idle");
|
||
|
|
assert.equal(conn.ended, 0, "TTL zero intentionally keeps the old transport parked");
|
||
|
|
assert.equal(findTransportByEndpoint(firstEndpoint), transport);
|
||
|
|
assert.equal(findTransportByEndpoint(rotatedEndpoint), null);
|
||
|
|
assert.equal(beginTransportDial(rotatedEndpoint, { kind: "shell" }).role, "leader");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("endpoint key securely covers jump, proxy, agent, and SSH security policy", () => {
|
||
|
|
const base = buildConnectionReuseEndpoint({
|
||
|
|
hostId: "target-id",
|
||
|
|
hostname: "target.example",
|
||
|
|
username: "alice",
|
||
|
|
authMethod: "key",
|
||
|
|
privateKey: "target-private-key",
|
||
|
|
passphrase: "target-passphrase",
|
||
|
|
useSshAgent: true,
|
||
|
|
identityAgent: "/run/user/1000/agent-a.sock",
|
||
|
|
identitiesOnly: true,
|
||
|
|
agentPublicKeys: ["ssh-ed25519 TARGET-A"],
|
||
|
|
verifyHostKeys: true,
|
||
|
|
legacyAlgorithms: false,
|
||
|
|
skipEcdsaHostKey: false,
|
||
|
|
algorithmOverrides: { kex: ["curve25519-sha256"] },
|
||
|
|
proxy: {
|
||
|
|
type: "socks5",
|
||
|
|
host: "proxy.example",
|
||
|
|
port: 1080,
|
||
|
|
identityId: "proxy-identity-a",
|
||
|
|
username: "proxy-user",
|
||
|
|
password: "proxy-secret-a",
|
||
|
|
},
|
||
|
|
jumpHosts: [{
|
||
|
|
hostId: "jump-id",
|
||
|
|
hostname: "jump.example",
|
||
|
|
username: "jump-user",
|
||
|
|
authMethod: "password",
|
||
|
|
password: "jump-secret-a",
|
||
|
|
requiresMfa: true,
|
||
|
|
verifyHostKeys: true,
|
||
|
|
legacyAlgorithms: false,
|
||
|
|
skipEcdsaHostKey: false,
|
||
|
|
algorithmOverrides: { serverHostKey: ["ssh-ed25519"] },
|
||
|
|
proxy: {
|
||
|
|
type: "http",
|
||
|
|
host: "jump-proxy.example",
|
||
|
|
port: 8080,
|
||
|
|
username: "jump-proxy-user",
|
||
|
|
password: "jump-proxy-secret-a",
|
||
|
|
},
|
||
|
|
}],
|
||
|
|
});
|
||
|
|
const baseKey = buildEndpointKey(base);
|
||
|
|
const variants = [
|
||
|
|
{ ...base, identityAgent: "/run/user/1000/agent-b.sock" },
|
||
|
|
{ ...base, identitiesOnly: false },
|
||
|
|
{ ...base, agentPublicKeys: ["ssh-ed25519 TARGET-B"] },
|
||
|
|
{ ...base, verifyHostKeys: false },
|
||
|
|
{ ...base, algorithmOverrides: { kex: ["diffie-hellman-group14-sha256"] } },
|
||
|
|
{ ...base, proxy: { ...base.proxy, password: "proxy-secret-b" } },
|
||
|
|
{ ...base, proxy: { ...base.proxy, identityId: "proxy-identity-b" } },
|
||
|
|
{ ...base, jumpHosts: [{ ...base.jumpHosts[0], password: "jump-secret-b" }] },
|
||
|
|
{ ...base, jumpHosts: [{ ...base.jumpHosts[0], verifyHostKeys: false }] },
|
||
|
|
{ ...base, jumpHosts: [{
|
||
|
|
...base.jumpHosts[0],
|
||
|
|
proxy: { ...base.jumpHosts[0].proxy, password: "jump-proxy-secret-b" },
|
||
|
|
}] },
|
||
|
|
];
|
||
|
|
|
||
|
|
for (const variant of variants) {
|
||
|
|
assert.notEqual(buildEndpointKey(variant), baseKey);
|
||
|
|
}
|
||
|
|
for (const secret of [
|
||
|
|
"target-private-key",
|
||
|
|
"target-passphrase",
|
||
|
|
"proxy-secret-a",
|
||
|
|
"jump-secret-a",
|
||
|
|
"jump-proxy-secret-a",
|
||
|
|
]) {
|
||
|
|
assert.equal(baseKey.includes(secret), false, `endpoint key must not expose ${secret}`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("endpoint key changes when known-host trust content changes", () => {
|
||
|
|
const base = {
|
||
|
|
hostId: "known-host-profile",
|
||
|
|
hostname: "known-host.example",
|
||
|
|
port: 22,
|
||
|
|
username: "alice",
|
||
|
|
authMethod: "password",
|
||
|
|
password: "secret",
|
||
|
|
verifyHostKeys: true,
|
||
|
|
};
|
||
|
|
const first = buildEndpointKey(buildConnectionReuseEndpoint({
|
||
|
|
...base,
|
||
|
|
knownHosts: [{ hostname: "known-host.example", port: 22, publicKey: "ssh-ed25519 AAAA_FIRST" }],
|
||
|
|
}));
|
||
|
|
const rotated = buildEndpointKey(buildConnectionReuseEndpoint({
|
||
|
|
...base,
|
||
|
|
knownHosts: [{ hostname: "known-host.example", port: 22, publicKey: "ssh-ed25519 AAAA_ROTATED" }],
|
||
|
|
}));
|
||
|
|
|
||
|
|
assert.notEqual(rotated, first);
|
||
|
|
|
||
|
|
const jumpFirst = buildEndpointKey(buildConnectionReuseEndpoint({
|
||
|
|
...base,
|
||
|
|
jumpHosts: [{ hostname: "bastion.example", port: 2222 }],
|
||
|
|
knownHosts: [{
|
||
|
|
hostname: "[bastion.example]:2222",
|
||
|
|
port: 2222,
|
||
|
|
publicKey: "ssh-ed25519 AAAA_JUMP_FIRST",
|
||
|
|
}],
|
||
|
|
}));
|
||
|
|
const jumpRotatedEndpoint = buildConnectionReuseEndpoint({
|
||
|
|
...base,
|
||
|
|
jumpHosts: [{ hostname: "bastion.example", port: 2222 }],
|
||
|
|
knownHosts: [{
|
||
|
|
hostname: "[bastion.example]:2222",
|
||
|
|
port: 2222,
|
||
|
|
publicKey: "ssh-ed25519 AAAA_JUMP_ROTATED",
|
||
|
|
}],
|
||
|
|
});
|
||
|
|
assert.notEqual(buildEndpointKey(jumpRotatedEndpoint), jumpFirst);
|
||
|
|
assert.equal(
|
||
|
|
JSON.stringify(normalizeEndpoint(jumpRotatedEndpoint)).includes("AAAA_JUMP_ROTATED"),
|
||
|
|
false,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("endpoint key changes when an identity file changes in place", (t) => {
|
||
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-pool-key-"));
|
||
|
|
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||
|
|
const identityPath = path.join(dir, "id_ed25519");
|
||
|
|
fs.writeFileSync(identityPath, "first-private-key");
|
||
|
|
const endpoint = buildConnectionReuseEndpoint({
|
||
|
|
hostname: "identity-file.example",
|
||
|
|
username: "alice",
|
||
|
|
authMethod: "key",
|
||
|
|
identityFilePaths: [identityPath],
|
||
|
|
});
|
||
|
|
const first = buildEndpointKey(endpoint);
|
||
|
|
|
||
|
|
fs.writeFileSync(identityPath, "rotated-private-key");
|
||
|
|
const second = buildEndpointKey(endpoint);
|
||
|
|
assert.notEqual(second, first);
|
||
|
|
assert.equal(first.includes("first-private-key"), false);
|
||
|
|
assert.equal(second.includes("rotated-private-key"), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("auto-auth endpoint changes when the inline private key rotates", () => {
|
||
|
|
const base = {
|
||
|
|
hostId: "inline-key-profile",
|
||
|
|
hostname: "inline-key.example",
|
||
|
|
username: "alice",
|
||
|
|
authMethod: "auto",
|
||
|
|
publicKey: "stale-public-key-metadata",
|
||
|
|
};
|
||
|
|
const first = buildEndpointKey(buildConnectionReuseEndpoint({
|
||
|
|
...base,
|
||
|
|
privateKey: "first-private-key",
|
||
|
|
}));
|
||
|
|
const rotated = buildEndpointKey(buildConnectionReuseEndpoint({
|
||
|
|
...base,
|
||
|
|
privateKey: "rotated-private-key",
|
||
|
|
}));
|
||
|
|
|
||
|
|
assert.notEqual(rotated, first);
|
||
|
|
assert.equal(first.includes("first-private-key"), false);
|
||
|
|
assert.equal(rotated.includes("rotated-private-key"), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("Terminal and SFTP aliases build the same reuse endpoint semantics", () => {
|
||
|
|
const options = {
|
||
|
|
hostId: "shared-profile",
|
||
|
|
hostname: "shared.example",
|
||
|
|
username: "alice",
|
||
|
|
authMethod: "certificate",
|
||
|
|
keyId: "key-1",
|
||
|
|
privateKey: "private",
|
||
|
|
publicKey: "public",
|
||
|
|
certificate: "certificate",
|
||
|
|
passphrase: "passphrase",
|
||
|
|
identityFilePaths: ["/keys/id"],
|
||
|
|
useSshAgent: true,
|
||
|
|
identityAgent: "SSH_AUTH_SOCK",
|
||
|
|
identitiesOnly: true,
|
||
|
|
addKeysToAgent: "confirm",
|
||
|
|
useKeychain: true,
|
||
|
|
agentPublicKeys: ["ssh-ed25519 SELECTED"],
|
||
|
|
verifyHostKeys: true,
|
||
|
|
legacyAlgorithms: true,
|
||
|
|
skipEcdsaHostKey: true,
|
||
|
|
algorithmOverrides: { hmac: ["hmac-sha2-256"] },
|
||
|
|
jumpHosts: [{ hostname: "jump", password: "jump-password" }],
|
||
|
|
proxy: { type: "http", host: "proxy", port: 8080, password: "proxy-password" },
|
||
|
|
};
|
||
|
|
assert.equal(
|
||
|
|
buildEndpointKey(buildConnectionReuseEndpoint(options)),
|
||
|
|
buildEndpointKey(buildConnectionReuseEndpoint({
|
||
|
|
...options,
|
||
|
|
authType: options.authMethod,
|
||
|
|
authMethod: undefined,
|
||
|
|
})),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("connection reuse identity changes with the effective keepalive policy", () => {
|
||
|
|
const base = { hostname: "keepalive.example", username: "root", port: 22 };
|
||
|
|
const enabled = buildConnectionReuseEndpoint({
|
||
|
|
...base,
|
||
|
|
keepaliveInterval: 30,
|
||
|
|
keepaliveCountMax: 10,
|
||
|
|
});
|
||
|
|
const disabled = buildConnectionReuseEndpoint({
|
||
|
|
...base,
|
||
|
|
keepaliveInterval: 0,
|
||
|
|
keepaliveCountMax: 0,
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.notEqual(buildEndpointKey(enabled), buildEndpointKey(disabled));
|
||
|
|
assert.equal(endpointAllowsReuse(enabled, disabled, "shell"), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("connection endpoint can record negotiated agent forwarding instead of the request", () => {
|
||
|
|
const endpoint = buildConnectionReuseEndpoint(
|
||
|
|
{ hostname: "agent.example", username: "root", agentForwarding: true },
|
||
|
|
{ agentForwarding: false },
|
||
|
|
);
|
||
|
|
assert.equal(normalizeEndpoint(endpoint).agentForwarding, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("channel reuse can borrow a normal SSH transport for sudo SFTP", () => {
|
||
|
|
const base = { hostname: "sudo.example", username: "root", port: 22 };
|
||
|
|
const normalShell = buildConnectionReuseEndpoint(base, { sftpSudo: false });
|
||
|
|
const sudoSftp = buildConnectionReuseEndpoint(base, { sftpSudo: true });
|
||
|
|
|
||
|
|
assert.notEqual(buildEndpointKey(normalShell), buildEndpointKey(sudoSftp));
|
||
|
|
assert.equal(endpointAllowsReuse(sudoSftp, normalShell, "channel"), true);
|
||
|
|
assert.equal(endpointAllowsReuse(sudoSftp, normalShell, "shell"), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("endpointAllowsReuse: shell exact vs channel asymmetric for agentForwarding", () => {
|
||
|
|
const base = { hostname: "a.example", username: "root", port: 22 };
|
||
|
|
// Channel (default): request needs ForwardAgent, existing never enabled it -> reject.
|
||
|
|
assert.equal(
|
||
|
|
endpointAllowsReuse({ ...base, agentForwarding: true }, { ...base, agentForwarding: false }),
|
||
|
|
false,
|
||
|
|
);
|
||
|
|
// Channel: request does not need ForwardAgent; existing with it is fine (SFTP/PF).
|
||
|
|
assert.equal(
|
||
|
|
endpointAllowsReuse({ ...base, agentForwarding: false }, { ...base, agentForwarding: true }),
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
// Shell: disabling ForwardAgent must not reattach to a warm agent-forward conn.
|
||
|
|
assert.equal(
|
||
|
|
endpointAllowsReuse(
|
||
|
|
{ ...base, agentForwarding: false },
|
||
|
|
{ ...base, agentForwarding: true },
|
||
|
|
"shell",
|
||
|
|
),
|
||
|
|
false,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
endpointAllowsReuse(
|
||
|
|
{ ...base, agentForwarding: true },
|
||
|
|
{ ...base, agentForwarding: true },
|
||
|
|
"shell",
|
||
|
|
),
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
// agentForwarding does not change the shared endpoint key (park index stays stable).
|
||
|
|
assert.equal(
|
||
|
|
buildEndpointKey({ ...base, agentForwarding: true }),
|
||
|
|
buildEndpointKey({ ...base, agentForwarding: false }),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("findTransportByEndpoint shell kind refuses mismatched agentForwarding", () => {
|
||
|
|
resetSshTransportRegistryForTests({ defaultIdleTtlMs: 0 });
|
||
|
|
const holder = { id: "s1" };
|
||
|
|
const withFwd = createTransport({
|
||
|
|
conn: makeConn(),
|
||
|
|
endpoint: { hostname: "fwd.example", username: "root", agentForwarding: true },
|
||
|
|
});
|
||
|
|
borrowTransport(withFwd, { kind: "shell", holder, leaseId: "shell:s1" });
|
||
|
|
returnTransport(holder); // park idle forever
|
||
|
|
|
||
|
|
// Shell open after user disables ForwardAgent must not reuse.
|
||
|
|
assert.equal(
|
||
|
|
findTransportByEndpoint(
|
||
|
|
{ hostname: "fwd.example", username: "root", agentForwarding: false },
|
||
|
|
{ kind: "shell" },
|
||
|
|
),
|
||
|
|
null,
|
||
|
|
);
|
||
|
|
// SFTP/PF channel reuse may still borrow the ForwardAgent transport.
|
||
|
|
assert.ok(
|
||
|
|
findTransportByEndpoint({ hostname: "fwd.example", username: "root", agentForwarding: false }),
|
||
|
|
);
|
||
|
|
assert.ok(
|
||
|
|
findTransportByEndpoint(
|
||
|
|
{ hostname: "fwd.example", username: "root", agentForwarding: true },
|
||
|
|
{ kind: "shell" },
|
||
|
|
),
|
||
|
|
);
|
||
|
|
discardTransport(withFwd);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("last return parks with positive TTL then ends when timer fires", () => {
|
||
|
|
const timers = useShortTtlTimers(60_000);
|
||
|
|
|
||
|
|
const conn = makeConn();
|
||
|
|
const holder = { id: "shell-1" };
|
||
|
|
const transport = createTransport({
|
||
|
|
conn,
|
||
|
|
endpoint: { hostname: "10.0.0.1", username: "alice" },
|
||
|
|
});
|
||
|
|
borrowTransport(transport, { kind: LEASE_KINDS.shell, holder });
|
||
|
|
|
||
|
|
const result = returnTransport(holder);
|
||
|
|
assert.equal(result.released, true);
|
||
|
|
assert.equal(result.ended, false);
|
||
|
|
assert.equal(result.idle, true);
|
||
|
|
assert.equal(conn.ended, 0);
|
||
|
|
assert.equal(transport.state, "idle");
|
||
|
|
assert.equal(timers.length, 1);
|
||
|
|
assert.equal(timers[0].ms, 60_000);
|
||
|
|
|
||
|
|
timers[0].fn();
|
||
|
|
assert.equal(conn.ended, 1);
|
||
|
|
assert.equal(transport.state, "dead");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("idle reclaim timer does not keep the app process alive", () => {
|
||
|
|
let unrefCalls = 0;
|
||
|
|
resetSshTransportRegistryForTests({
|
||
|
|
defaultIdleTtlMs: 60_000,
|
||
|
|
setTimeout: () => ({
|
||
|
|
unref() { unrefCalls += 1; },
|
||
|
|
}),
|
||
|
|
clearTimeout: () => {},
|
||
|
|
});
|
||
|
|
const transport = createTransport({ conn: makeConn() });
|
||
|
|
const holder = {};
|
||
|
|
borrowTransport(transport, { holder });
|
||
|
|
|
||
|
|
returnTransport(holder);
|
||
|
|
|
||
|
|
assert.equal(unrefCalls, 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("TTL 0 parks forever without scheduling a timer", () => {
|
||
|
|
const timers = [];
|
||
|
|
resetSshTransportRegistryForTests({
|
||
|
|
defaultIdleTtlMs: 0,
|
||
|
|
setTimeout: (fn, ms) => {
|
||
|
|
const handle = { fn, ms, cleared: false };
|
||
|
|
timers.push(handle);
|
||
|
|
return handle;
|
||
|
|
},
|
||
|
|
clearTimeout: (handle) => {
|
||
|
|
if (handle) handle.cleared = true;
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const conn = makeConn();
|
||
|
|
const transport = createTransport({ conn, endpoint: { hostname: "forever.example" } });
|
||
|
|
const holder = {};
|
||
|
|
borrowTransport(transport, { holder });
|
||
|
|
const result = returnTransport(holder);
|
||
|
|
assert.equal(result.idle, true);
|
||
|
|
assert.equal(result.ended, false);
|
||
|
|
assert.equal(timers.length, 0, "never-reclaim must not schedule idle end");
|
||
|
|
assert.equal(conn.ended, 0);
|
||
|
|
assert.equal(transport.state, "idle");
|
||
|
|
assert.ok(findTransportByEndpoint({ hostname: "forever.example" }));
|
||
|
|
});
|
||
|
|
|
||
|
|
test("borrow while idle cancels park and reuses the same conn", () => {
|
||
|
|
const timers = useShortTtlTimers(60_000);
|
||
|
|
|
||
|
|
const conn = makeConn();
|
||
|
|
const endpoint = { hostname: "10.0.0.2", port: 22, username: "root" };
|
||
|
|
const transport = createTransport({ conn, endpoint });
|
||
|
|
const first = {};
|
||
|
|
borrowTransport(transport, { kind: LEASE_KINDS.shell, holder: first });
|
||
|
|
returnTransport(first);
|
||
|
|
assert.equal(transport.state, "idle");
|
||
|
|
assert.equal(timers[0].cleared, false);
|
||
|
|
|
||
|
|
const found = findTransportByEndpoint(endpoint);
|
||
|
|
assert.equal(found, transport);
|
||
|
|
|
||
|
|
const second = {};
|
||
|
|
borrowTransport(found, { kind: LEASE_KINDS.sftp, holder: second, leaseId: "sftp:panel-1" });
|
||
|
|
assert.equal(transport.state, "live");
|
||
|
|
assert.equal(timers[0].cleared, true);
|
||
|
|
assert.equal(conn.ended, 0);
|
||
|
|
assert.equal(transport.count, 1);
|
||
|
|
assert.equal(second.connRef, transport);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("findTransportByEndpoint prefers live transport over idle", () => {
|
||
|
|
resetSshTransportRegistryForTests({ defaultIdleTtlMs: 60_000 });
|
||
|
|
const endpoint = { hostname: "shared.example", username: "u" };
|
||
|
|
|
||
|
|
const idleConn = makeConn();
|
||
|
|
const idleTransport = createTransport({ conn: idleConn, endpoint });
|
||
|
|
const idleHolder = {};
|
||
|
|
borrowTransport(idleTransport, { holder: idleHolder });
|
||
|
|
returnTransport(idleHolder);
|
||
|
|
assert.equal(idleTransport.state, "idle");
|
||
|
|
|
||
|
|
const liveConn = makeConn();
|
||
|
|
const liveTransport = createTransport({ conn: liveConn, endpoint });
|
||
|
|
borrowTransport(liveTransport, { holder: {} });
|
||
|
|
|
||
|
|
assert.equal(findTransportByEndpoint(endpoint), liveTransport);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("sftp and shell leases share one transport until both return", () => {
|
||
|
|
const timers = useShortTtlTimers(1);
|
||
|
|
const conn = makeConn();
|
||
|
|
const session = { id: "term-1", _reuseEndpoint: { hostname: "h", port: 22, username: "root" } };
|
||
|
|
const transport = createConnectionRef(session, conn, []);
|
||
|
|
const sftpHolder = { id: "sftp-1", __sshLeaseKind: LEASE_KINDS.sftp };
|
||
|
|
acquireConnectionRef(sftpHolder, transport);
|
||
|
|
assert.equal(transport.count, 2);
|
||
|
|
|
||
|
|
assert.equal(releaseConnectionRef(session), false);
|
||
|
|
assert.equal(conn.ended, 0);
|
||
|
|
assert.equal(releaseConnectionRef(sftpHolder), false);
|
||
|
|
assert.equal(transport.state, "idle");
|
||
|
|
fireIdleTimers(timers);
|
||
|
|
assert.equal(conn.ended, 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("resolveTransportForReuse finds idle transport by endpoint without a session", () => {
|
||
|
|
const timers = useShortTtlTimers(30_000);
|
||
|
|
|
||
|
|
const conn = makeConn();
|
||
|
|
const endpoint = { hostname: "parked.example", username: "ops" };
|
||
|
|
const transport = createTransport({ conn, endpoint });
|
||
|
|
const holder = {};
|
||
|
|
borrowTransport(transport, { holder });
|
||
|
|
returnTransport(holder);
|
||
|
|
|
||
|
|
const resolved = resolveTransportForReuse({ endpoint });
|
||
|
|
assert.equal(resolved, transport);
|
||
|
|
assert.equal(resolved.state, "idle");
|
||
|
|
assert.ok(timers.length >= 1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("findTransportByEndpoint ends transports whose socket is destroyed", () => {
|
||
|
|
resetSshTransportRegistryForTests({ defaultIdleTtlMs: 0 });
|
||
|
|
const endpoint = { hostId: "h1", hostname: "dead.example", username: "root" };
|
||
|
|
const conn = makeConn();
|
||
|
|
const transport = createTransport({ conn, endpoint });
|
||
|
|
const holder = {};
|
||
|
|
borrowTransport(transport, { holder });
|
||
|
|
returnTransport(holder);
|
||
|
|
assert.equal(transport.state, "idle");
|
||
|
|
conn._sock.destroyed = true;
|
||
|
|
assert.equal(findTransportByEndpoint(endpoint), null);
|
||
|
|
assert.equal(conn.ended, 1);
|
||
|
|
assert.equal(getTransportStats().transports, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("discardTransport force-ends and unregisters", () => {
|
||
|
|
resetSshTransportRegistryForTests({ defaultIdleTtlMs: 60_000 });
|
||
|
|
const conn = makeConn();
|
||
|
|
const endpoint = { hostname: "x.example" };
|
||
|
|
const transport = createTransport({ conn, endpoint });
|
||
|
|
borrowTransport(transport, { holder: {} });
|
||
|
|
|
||
|
|
assert.equal(discardTransport(transport), true);
|
||
|
|
assert.equal(conn.ended, 1);
|
||
|
|
assert.equal(findTransportByEndpoint(endpoint), null);
|
||
|
|
assert.equal(getTransportStats().transports, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("discardAllTransports clears the registry", () => {
|
||
|
|
resetSshTransportRegistryForTests({ defaultIdleTtlMs: 60_000 });
|
||
|
|
const a = createTransport({ conn: makeConn(), endpoint: { hostname: "a" } });
|
||
|
|
const b = createTransport({ conn: makeConn(), endpoint: { hostname: "b" } });
|
||
|
|
borrowTransport(a, { holder: {} });
|
||
|
|
borrowTransport(b, { holder: {} });
|
||
|
|
assert.equal(discardAllTransports(), 2);
|
||
|
|
assert.equal(getTransportStats().transports, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("active transport error unregisters leases and closes its jump chain once", () => {
|
||
|
|
useParkForever();
|
||
|
|
const conn = makeLifecycleConn({ emitCloseFromEnd: true });
|
||
|
|
const chain = makeChainConn();
|
||
|
|
const holder = {};
|
||
|
|
const transport = createTransport({
|
||
|
|
conn,
|
||
|
|
chainConnections: [chain],
|
||
|
|
endpoint: { hostname: "active-error.example", username: "alice" },
|
||
|
|
});
|
||
|
|
borrowTransport(transport, { holder, kind: "sftp" });
|
||
|
|
|
||
|
|
conn.emit("error", new Error("remote transport failed"));
|
||
|
|
|
||
|
|
assert.equal(getTransportStats().transports, 0);
|
||
|
|
assert.equal(getTransportStats().leases, 0);
|
||
|
|
assert.equal(holder.connRef, null);
|
||
|
|
assert.equal(holder._sshTransportLeaseId, null);
|
||
|
|
assert.equal(conn.ended, 1);
|
||
|
|
assert.equal(chain.ended, 1);
|
||
|
|
assert.equal(transport.state, "dead");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("remote close unregisters a TTL-zero idle transport and its jump chain once", () => {
|
||
|
|
useParkForever();
|
||
|
|
const conn = makeLifecycleConn();
|
||
|
|
const chain = makeChainConn();
|
||
|
|
const holder = {};
|
||
|
|
const transport = createTransport({
|
||
|
|
conn,
|
||
|
|
chainConnections: [chain],
|
||
|
|
endpoint: { hostname: "idle-close.example", username: "alice" },
|
||
|
|
});
|
||
|
|
borrowTransport(transport, { holder, kind: "shell" });
|
||
|
|
returnTransport(holder);
|
||
|
|
assert.equal(getTransportStats().idle, 1);
|
||
|
|
|
||
|
|
conn._sock.destroyed = true;
|
||
|
|
conn.emit("close");
|
||
|
|
conn.emit("close");
|
||
|
|
|
||
|
|
assert.equal(getTransportStats().transports, 0);
|
||
|
|
assert.equal(getTransportStats().idle, 0);
|
||
|
|
assert.equal(conn.ended, 0, "a remote close must not call end on the closed socket again");
|
||
|
|
assert.equal(chain.ended, 1);
|
||
|
|
assert.equal(transport.state, "dead");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("setDefaultTransportIdleTtlMs updates default and reschedules idle transports", () => {
|
||
|
|
const timers = [];
|
||
|
|
let now = 10_000;
|
||
|
|
resetSshTransportRegistryForTests({
|
||
|
|
defaultIdleTtlMs: 60_000,
|
||
|
|
now: () => now,
|
||
|
|
setTimeout: (fn, ms) => {
|
||
|
|
const handle = { fn, ms, cleared: false };
|
||
|
|
timers.push(handle);
|
||
|
|
return handle;
|
||
|
|
},
|
||
|
|
clearTimeout: (handle) => {
|
||
|
|
if (handle) handle.cleared = true;
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const conn = makeConn();
|
||
|
|
const transport = createTransport({ conn, endpoint: { hostname: "resched.example" } });
|
||
|
|
const holder = {};
|
||
|
|
borrowTransport(transport, { holder });
|
||
|
|
returnTransport(holder);
|
||
|
|
assert.equal(transport.state, "idle");
|
||
|
|
assert.equal(timers[0].ms, 60_000);
|
||
|
|
|
||
|
|
now += 250;
|
||
|
|
setDefaultTransportIdleTtlMs(5_000);
|
||
|
|
assert.equal(getDefaultTransportIdleTtlMs(), 5_000);
|
||
|
|
assert.equal(timers[0].cleared, true, "old idle timer must be cancelled");
|
||
|
|
const last = timers[timers.length - 1];
|
||
|
|
assert.equal(last.ms, 4_750, "reschedule must preserve elapsed idle time");
|
||
|
|
assert.equal(transport.idleTtlMs, 5_000);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("createConnectionRef indexes endpoint from session._reuseEndpoint including jumpHosts and hostId", () => {
|
||
|
|
resetSshTransportRegistryForTests({ defaultIdleTtlMs: 60_000 });
|
||
|
|
const conn = makeConn();
|
||
|
|
const session = {
|
||
|
|
id: "s1",
|
||
|
|
_reuseEndpoint: {
|
||
|
|
hostId: "vault-host-1",
|
||
|
|
hostname: "indexed.example",
|
||
|
|
port: 2222,
|
||
|
|
username: "deploy",
|
||
|
|
jumpHosts: [{ hostname: "bastion", port: 22, username: "jump" }],
|
||
|
|
},
|
||
|
|
};
|
||
|
|
createConnectionRef(session, conn, []);
|
||
|
|
const found = findTransportByEndpoint({
|
||
|
|
hostId: "vault-host-1",
|
||
|
|
hostname: "indexed.example",
|
||
|
|
port: 2222,
|
||
|
|
username: "deploy",
|
||
|
|
jumpHosts: [{ hostname: "bastion", port: 22, username: "jump" }],
|
||
|
|
});
|
||
|
|
assert.ok(found);
|
||
|
|
assert.equal(found.conn, conn);
|
||
|
|
assert.equal(
|
||
|
|
findTransportByEndpoint({
|
||
|
|
hostId: "vault-host-1",
|
||
|
|
hostname: "indexed.example",
|
||
|
|
port: 2222,
|
||
|
|
username: "deploy",
|
||
|
|
}),
|
||
|
|
null,
|
||
|
|
"missing jump chain must not match",
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
findTransportByEndpoint({
|
||
|
|
hostId: "other-host",
|
||
|
|
hostname: "indexed.example",
|
||
|
|
port: 2222,
|
||
|
|
username: "deploy",
|
||
|
|
jumpHosts: [{ hostname: "bastion", port: 22, username: "jump" }],
|
||
|
|
}),
|
||
|
|
null,
|
||
|
|
"different vault hostId must not match",
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("borrowTransport rejects bare non-registry connRef objects", () => {
|
||
|
|
const bare = { count: 1, conn: makeConn(), chainConnections: [] };
|
||
|
|
assert.throws(
|
||
|
|
() => acquireConnectionRef({ id: "x" }, bare),
|
||
|
|
/not a registry transport/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("transferConnectionRef rebinds a lease without changing count", () => {
|
||
|
|
const timers = useShortTtlTimers(1);
|
||
|
|
const conn = makeConn();
|
||
|
|
const transport = createTransport({ conn, endpoint: { hostname: "t.example" } });
|
||
|
|
const temp = {};
|
||
|
|
const session = { id: "shell-copy" };
|
||
|
|
borrowTransport(transport, { kind: LEASE_KINDS.shell, holder: temp });
|
||
|
|
assert.equal(transport.count, 1);
|
||
|
|
|
||
|
|
assert.equal(transferConnectionRef(temp, session), true);
|
||
|
|
assert.equal(transport.count, 1);
|
||
|
|
assert.equal(temp.connRef, null);
|
||
|
|
assert.equal(session.connRef, transport);
|
||
|
|
assert.ok(session._sshTransportLeaseId);
|
||
|
|
|
||
|
|
assert.equal(releaseConnectionRef(session), false);
|
||
|
|
assert.equal(transport.state, "idle");
|
||
|
|
fireIdleTimers(timers);
|
||
|
|
assert.equal(conn.ended, 1);
|
||
|
|
});
|