Let a session's transport say what it negotiated

ISshConnection and ISftpSession both carry Cipher now — the server-to-client
algorithm off SSH.NET's own ConnectionInfo, captured once because a rekey is
not an event that library raises — and TerminalWorkspace.GetSessionFacts hands
that plus the host key's algorithm back per session, without ever handing over
the connection itself. Nothing reads either yet; the status bar that will is
the next commit.
This commit is contained in:
2026-08-08 20:54:14 +02:00
parent 8915650a0d
commit 8209f15741
10 changed files with 134 additions and 0 deletions
@@ -83,6 +83,9 @@ internal sealed class FakeSftpSession(SshConnectionRequest request) : ISftpSessi
public HostKeyPresentation HostKey { get; } =
new(request.Host, request.Port, "ssh-ed25519", "SHA256:fake");
/// <inheritdoc />
public string Cipher { get; } = "aes256-gcm@openssh.com";
/// <inheritdoc />
public string HomeDirectory => $"/home/{request.Username}";
@@ -139,6 +142,9 @@ internal sealed class FakeSshConnection(SshConnectionRequest request) : ISshConn
public HostKeyPresentation HostKey { get; } =
new(request.Host, request.Port, "ssh-ed25519", "SHA256:fake");
/// <inheritdoc />
public string Cipher { get; } = "aes256-gcm@openssh.com";
/// <inheritdoc />
public Task<ISshShellSession> OpenShellAsync(
TerminalSize size,
@@ -36,6 +36,12 @@ public sealed class KeyAuthenticationTests(SshServerFixture fixture)
connection.IsConnected.ShouldBeTrue();
// The one place this suite checks Cipher against a real handshake rather than a fake's fixed string.
// SSH.NET negotiates whatever the container's sshd offers first from its own preference list, so the
// exact algorithm is not pinned here — only that ConnectionInfo.CurrentServerEncryption came back as
// something rather than the empty string a stalled or pre-handshake read would produce.
connection.Cipher.ShouldNotBeNullOrEmpty();
// Authenticated is not the same as usable: a channel has to open on the connection too.
await using var shell = await connection.OpenShellAsync(TerminalSize.Default, Token);
@@ -136,6 +136,9 @@ internal sealed class FakeConnection(SshConnectionRequest request, long bytesPer
public HostKeyPresentation HostKey { get; } =
new(request.Host, request.Port, "ssh-ed25519", "SHA256:fake");
/// <inheritdoc />
public string Cipher { get; } = "aes256-gcm@openssh.com";
/// <summary>The shell this connection opened, if it opened one.</summary>
internal FakeShellSession? Shell { get; private set; }
@@ -149,6 +149,31 @@ public sealed class TerminalWorkspaceTests
workspace.IsSessionLive(second).ShouldBeTrue("closing one tab must not disturb another");
}
/// <remarks>
/// The shell's connect path reads these back once, right after <see cref="TerminalWorkspace.OpenSessionAsync"/>
/// returns, to fill in the status bar's cipher and host-key facts — see <c>VaultViewModel.ConnectAndAnnounceAsync</c>.
/// Asserted the same way <see cref="LivenessIsAnsweredPerSession"/> asserts liveness: per session, and
/// null rather than thrown for an id this workspace never issued.
/// </remarks>
[Fact]
public async Task SessionFactsAreReadPerSession()
{
var connections = new FakeConnectionFactory();
await using var workspace = CreateWorkspace(connections);
var sessionId = await workspace.OpenSessionAsync(
Request(), TerminalSize.Default, TestContext.Current.CancellationToken);
var facts = workspace.GetSessionFacts(sessionId).ShouldNotBeNull();
var connection = connections.Connections.ShouldHaveSingleItem();
facts.Cipher.ShouldBe(connection.Cipher);
facts.HostKeyAlgorithm.ShouldBe(connection.HostKey.Algorithm);
workspace.GetSessionFacts(9999).ShouldBeNull("this workspace never issued that id");
}
/// <remarks>
/// <para>
/// Inserting a snippet has to be able to say whether it arrived, and the transport cannot: it drops
@@ -23,6 +23,9 @@ internal sealed class FakeSftpSession : ISftpSession
/// <inheritdoc />
public HostKeyPresentation HostKey { get; } = new("host.internal", 22, "ssh-ed25519", "SHA256:fake");
/// <inheritdoc />
public string Cipher { get; } = "aes256-gcm@openssh.com";
/// <inheritdoc />
public string HomeDirectory => "/home/dodo";