U-1: Replace ssh.InsecureIgnoreHostKey() with TOFU (Trust On First Use) host
key verification via HostKeyStore. New keys auto-store, matching keys accept
silently, CHANGED keys reject with MITM warning. Added DeleteHostKey() for
legitimate re-key scenarios.
U-2: Wire CWDTracker per SSH session. readLoop() now processes OSC 7 escape
sequences, strips them from terminal output, and emits ssh:cwd:{sessionID}
Wails events on directory changes. Shell integration commands (bash/zsh
PROMPT_COMMAND) injected after connection.
U-3: Session manager now tracks all SSH and RDP sessions via CreateWithID()
which accepts the service-level UUID instead of generating a new one.
ConnectSSH, ConnectSSHWithPassword, ConnectRDP register sessions;
DisconnectSession and RDPDisconnect remove them. ConnectedAt timestamp set.
U-4: WorkspaceService instantiated in New(), clean shutdown flag managed on
startup/exit, workspace state auto-saved on every session open/close.
Frontend-facing proxy methods exposed: SaveWorkspace, LoadWorkspace,
MarkCleanShutdown, WasCleanShutdown, GetSessionCWD.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
149 lines
3.4 KiB
Go
149 lines
3.4 KiB
Go
package ssh
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/pem"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
func TestNewSSHService(t *testing.T) {
|
|
svc := NewSSHService(nil, nil, nil, nil)
|
|
if svc == nil {
|
|
t.Fatal("NewSSHService returned nil")
|
|
}
|
|
if len(svc.ListSessions()) != 0 {
|
|
t.Error("new service should have no sessions")
|
|
}
|
|
}
|
|
|
|
func TestBuildPasswordAuth(t *testing.T) {
|
|
svc := NewSSHService(nil, nil, nil, nil)
|
|
auth := svc.BuildPasswordAuth("mypassword")
|
|
if auth == nil {
|
|
t.Error("BuildPasswordAuth returned nil")
|
|
}
|
|
}
|
|
|
|
func TestBuildKeyAuth(t *testing.T) {
|
|
svc := NewSSHService(nil, nil, nil, nil)
|
|
|
|
// Generate a test Ed25519 key
|
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey error: %v", err)
|
|
}
|
|
pemBlock, err := ssh.MarshalPrivateKey(priv, "")
|
|
if err != nil {
|
|
t.Fatalf("MarshalPrivateKey error: %v", err)
|
|
}
|
|
keyBytes := pem.EncodeToMemory(pemBlock)
|
|
|
|
auth, err := svc.BuildKeyAuth(keyBytes, "")
|
|
if err != nil {
|
|
t.Fatalf("BuildKeyAuth error: %v", err)
|
|
}
|
|
if auth == nil {
|
|
t.Error("BuildKeyAuth returned nil")
|
|
}
|
|
}
|
|
|
|
func TestBuildKeyAuthInvalidKey(t *testing.T) {
|
|
svc := NewSSHService(nil, nil, nil, nil)
|
|
_, err := svc.BuildKeyAuth([]byte("not a key"), "")
|
|
if err == nil {
|
|
t.Error("BuildKeyAuth should fail with invalid key")
|
|
}
|
|
}
|
|
|
|
func TestSessionTracking(t *testing.T) {
|
|
svc := NewSSHService(nil, nil, nil, nil)
|
|
|
|
// Manually add a session to test tracking
|
|
svc.mu.Lock()
|
|
svc.sessions["test-123"] = &SSHSession{
|
|
ID: "test-123",
|
|
Hostname: "192.168.1.4",
|
|
Port: 22,
|
|
Username: "vstockwell",
|
|
Connected: time.Now(),
|
|
}
|
|
svc.mu.Unlock()
|
|
|
|
s, ok := svc.GetSession("test-123")
|
|
if !ok {
|
|
t.Fatal("session not found")
|
|
}
|
|
if s.Hostname != "192.168.1.4" {
|
|
t.Errorf("Hostname = %q, want %q", s.Hostname, "192.168.1.4")
|
|
}
|
|
|
|
sessions := svc.ListSessions()
|
|
if len(sessions) != 1 {
|
|
t.Errorf("ListSessions() = %d, want 1", len(sessions))
|
|
}
|
|
}
|
|
|
|
func TestGetSessionNotFound(t *testing.T) {
|
|
svc := NewSSHService(nil, nil, nil, nil)
|
|
_, ok := svc.GetSession("nonexistent")
|
|
if ok {
|
|
t.Error("GetSession should return false for nonexistent session")
|
|
}
|
|
}
|
|
|
|
func TestWriteNotFound(t *testing.T) {
|
|
svc := NewSSHService(nil, nil, nil, nil)
|
|
err := svc.Write("nonexistent", "data")
|
|
if err == nil {
|
|
t.Error("Write should fail for nonexistent session")
|
|
}
|
|
}
|
|
|
|
func TestResizeNotFound(t *testing.T) {
|
|
svc := NewSSHService(nil, nil, nil, nil)
|
|
err := svc.Resize("nonexistent", 80, 24)
|
|
if err == nil {
|
|
t.Error("Resize should fail for nonexistent session")
|
|
}
|
|
}
|
|
|
|
func TestDisconnectNotFound(t *testing.T) {
|
|
svc := NewSSHService(nil, nil, nil, nil)
|
|
err := svc.Disconnect("nonexistent")
|
|
if err == nil {
|
|
t.Error("Disconnect should fail for nonexistent session")
|
|
}
|
|
}
|
|
|
|
func TestDisconnectRemovesSession(t *testing.T) {
|
|
svc := NewSSHService(nil, nil, nil, nil)
|
|
|
|
// Manually add a session with nil Client/Session/Stdin (no real connection)
|
|
svc.mu.Lock()
|
|
svc.sessions["test-dc"] = &SSHSession{
|
|
ID: "test-dc",
|
|
Hostname: "10.0.0.1",
|
|
Port: 22,
|
|
Username: "admin",
|
|
Connected: time.Now(),
|
|
}
|
|
svc.mu.Unlock()
|
|
|
|
if err := svc.Disconnect("test-dc"); err != nil {
|
|
t.Fatalf("Disconnect error: %v", err)
|
|
}
|
|
|
|
_, ok := svc.GetSession("test-dc")
|
|
if ok {
|
|
t.Error("session should be removed after Disconnect")
|
|
}
|
|
|
|
if len(svc.ListSessions()) != 0 {
|
|
t.Error("ListSessions should be empty after Disconnect")
|
|
}
|
|
}
|