feat: add shell interface, registry, and bash shell emulator
Implement Phase 1.4: replaces the hardcoded banner/timeout stub with a proper shell system. Adds a Shell interface with weighted registry for shell selection, a RecordingChannel wrapper (pass-through for now, prep for Phase 2.3 replay), and a bash-like shell with fake filesystem, terminal line reader, and command handling (pwd, ls, cd, cat, whoami, hostname, id, uname, exit). Sessions now log command/output pairs to the store and record the shell name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -109,6 +111,10 @@ func TestIntegrationSSHConnect(t *testing.T) {
|
||||
{Username: "root", Password: "toor"},
|
||||
},
|
||||
},
|
||||
Shell: config.ShellConfig{
|
||||
Hostname: "ubuntu-server",
|
||||
Banner: "Welcome to Ubuntu 22.04.3 LTS\r\n\r\n",
|
||||
},
|
||||
LogLevel: "debug",
|
||||
}
|
||||
|
||||
@@ -152,7 +158,7 @@ func TestIntegrationSSHConnect(t *testing.T) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Test static credential login.
|
||||
// Test static credential login with shell interaction.
|
||||
t.Run("static_cred", func(t *testing.T) {
|
||||
clientCfg := &ssh.ClientConfig{
|
||||
User: "root",
|
||||
@@ -172,6 +178,62 @@ func TestIntegrationSSHConnect(t *testing.T) {
|
||||
t.Fatalf("new session: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Request PTY and shell.
|
||||
if err := session.RequestPty("xterm", 80, 40, ssh.TerminalModes{}); err != nil {
|
||||
t.Fatalf("request pty: %v", err)
|
||||
}
|
||||
|
||||
stdin, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("stdin pipe: %v", err)
|
||||
}
|
||||
|
||||
var output bytes.Buffer
|
||||
session.Stdout = &output
|
||||
|
||||
if err := session.Shell(); err != nil {
|
||||
t.Fatalf("shell: %v", err)
|
||||
}
|
||||
|
||||
// Wait for the prompt, then send commands.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
stdin.Write([]byte("pwd\r"))
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
stdin.Write([]byte("whoami\r"))
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
stdin.Write([]byte("exit\r"))
|
||||
|
||||
// Wait for session to end.
|
||||
session.Wait()
|
||||
|
||||
out := output.String()
|
||||
if !strings.Contains(out, "Welcome to Ubuntu") {
|
||||
t.Errorf("output should contain banner, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "/root") {
|
||||
t.Errorf("output should contain /root from pwd, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "root") {
|
||||
t.Errorf("output should contain 'root' from whoami, got: %s", out)
|
||||
}
|
||||
|
||||
// Verify session logs were recorded.
|
||||
if len(store.SessionLogs) < 2 {
|
||||
t.Errorf("expected at least 2 session logs, got %d", len(store.SessionLogs))
|
||||
}
|
||||
|
||||
// Verify session was created with shell name.
|
||||
var foundBash bool
|
||||
for _, s := range store.Sessions {
|
||||
if s.ShellName == "bash" {
|
||||
foundBash = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundBash {
|
||||
t.Error("expected a session with shell_name='bash'")
|
||||
}
|
||||
})
|
||||
|
||||
// Test wrong password is rejected.
|
||||
|
||||
Reference in New Issue
Block a user