153 lines
3.9 KiB
Go
153 lines
3.9 KiB
Go
package sshclient
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/andydixon/nexus/internal/models"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
func TestConnectRequiresHostKeyCallback(t *testing.T) {
|
|
profile := models.NewProfile(models.ProtocolSSH)
|
|
client := NewClient(&profile)
|
|
|
|
err := client.Connect(nil, nil)
|
|
if err == nil {
|
|
t.Fatal("expected Connect to reject missing host key callback")
|
|
}
|
|
}
|
|
|
|
func TestJumpHostClientConfigUsesFinalHostKeyCallback(t *testing.T) {
|
|
called := false
|
|
hostKeyCallback := func(hostname string, _ net.Addr, _ ssh.PublicKey) error {
|
|
called = true
|
|
if hostname != "jump.example:2222" {
|
|
t.Fatalf("hostname = %q, want jump.example:2222", hostname)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
finalConfig := &ssh.ClientConfig{
|
|
HostKeyCallback: hostKeyCallback,
|
|
Timeout: 12 * time.Second,
|
|
}
|
|
jump := models.JumpHost{
|
|
Host: "jump.example",
|
|
Port: 2222,
|
|
Username: "alice",
|
|
}
|
|
|
|
got := jumpHostClientConfig(jump, nil, finalConfig)
|
|
if got.User != jump.Username {
|
|
t.Fatalf("User = %q, want %q", got.User, jump.Username)
|
|
}
|
|
if got.Timeout != finalConfig.Timeout {
|
|
t.Fatalf("Timeout = %s, want %s", got.Timeout, finalConfig.Timeout)
|
|
}
|
|
if got.HostKeyCallback == nil {
|
|
t.Fatal("HostKeyCallback is nil")
|
|
}
|
|
|
|
if err := got.HostKeyCallback("jump.example:2222", nil, nil); err != nil {
|
|
t.Fatalf("HostKeyCallback() error = %v", err)
|
|
}
|
|
if !called {
|
|
t.Fatal("final host key callback was not used")
|
|
}
|
|
}
|
|
|
|
func TestPingReturnsAfterSSHTransportCloses(t *testing.T) {
|
|
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey() error = %v", err)
|
|
}
|
|
signer, err := ssh.NewSignerFromKey(privateKey)
|
|
if err != nil {
|
|
t.Fatalf("NewSignerFromKey() error = %v", err)
|
|
}
|
|
|
|
serverConfig := &ssh.ServerConfig{NoClientAuth: true}
|
|
serverConfig.AddHostKey(signer)
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("Listen() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = listener.Close() })
|
|
|
|
serverReady := make(chan *ssh.ServerConn, 1)
|
|
serverErrors := make(chan error, 1)
|
|
go func() {
|
|
conn, acceptErr := listener.Accept()
|
|
if acceptErr != nil {
|
|
serverErrors <- acceptErr
|
|
return
|
|
}
|
|
serverConn, _, _, handshakeErr := ssh.NewServerConn(conn, serverConfig)
|
|
if handshakeErr != nil {
|
|
_ = conn.Close()
|
|
serverErrors <- handshakeErr
|
|
return
|
|
}
|
|
serverReady <- serverConn
|
|
}()
|
|
|
|
conn, err := net.DialTimeout("tcp", listener.Addr().String(), 2*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("DialTimeout() error = %v", err)
|
|
}
|
|
clientConn, channels, requests, err := ssh.NewClientConn(conn, listener.Addr().String(), &ssh.ClientConfig{
|
|
User: "test",
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // Test server uses an ephemeral key.
|
|
})
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
t.Fatalf("NewClientConn() error = %v", err)
|
|
}
|
|
rawClient := ssh.NewClient(clientConn, channels, requests)
|
|
|
|
var serverConn *ssh.ServerConn
|
|
select {
|
|
case serverConn = <-serverReady:
|
|
case serverErr := <-serverErrors:
|
|
t.Fatalf("SSH server handshake error = %v", serverErr)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timed out waiting for SSH server handshake")
|
|
}
|
|
|
|
client := NewClient(&models.Profile{})
|
|
client.mu.Lock()
|
|
client.sshClient = rawClient
|
|
client.connected = true
|
|
client.mu.Unlock()
|
|
t.Cleanup(func() { _ = client.Close() })
|
|
|
|
if err := serverConn.Close(); err != nil {
|
|
t.Fatalf("server Close() error = %v", err)
|
|
}
|
|
waitDone := make(chan error, 1)
|
|
go func() { waitDone <- rawClient.Wait() }()
|
|
select {
|
|
case <-waitDone:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timed out waiting for SSH transport to close")
|
|
}
|
|
|
|
pingDone := make(chan error, 1)
|
|
go func() {
|
|
_, pingErr := client.Ping()
|
|
pingDone <- pingErr
|
|
}()
|
|
select {
|
|
case pingErr := <-pingDone:
|
|
if pingErr == nil {
|
|
t.Fatal("Ping() succeeded after the SSH transport closed")
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Ping() did not return after the SSH transport closed")
|
|
}
|
|
}
|