diff --git a/pkg/gswitch/ssh_server.go b/pkg/gswitch/ssh_server.go index 3367964..b968e9e 100644 --- a/pkg/gswitch/ssh_server.go +++ b/pkg/gswitch/ssh_server.go @@ -6,6 +6,7 @@ import ( "io" "math/rand" "net" + "strconv" "sync/atomic" "go.uber.org/zap" @@ -56,36 +57,93 @@ func (c *connections) handleSSHConnection(ctx context.Context, tcpConn net.Conn, go ssh.DiscardRequests(reqs) for newChannel := range chans { - if newChannel.ChannelType() != "session" { + switch newChannel.ChannelType() { + case "session": + c.handleSessionChannel(ctx, newChannel, logger) + case "direct-tcpip": + c.handleDirectTCPIPChannel(ctx, newChannel, logger) + default: newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") - continue } + } - channel, requests, err := newChannel.Accept() - if err != nil { - logger.Error("could not accept channel", zap.Error(err)) - continue - } + return nil +} + +func (c *connections) handleSessionChannel(ctx context.Context, newChannel ssh.NewChannel, logger *zap.Logger) { + channel, requests, err := newChannel.Accept() + if err != nil { + logger.Error("could not accept channel", zap.Error(err)) + return + } + + go func() { + defer channel.Close() go func() { - defer channel.Close() - - go func() { - for req := range requests { - req.Reply(req.Type == "shell" || req.Type == "pty-req", nil) - } - }() - - logger.Debug("start CLI session") - session := NewCLISession(channel, c.username, c.password, logger, vendors["cisco"]) - err := session.Run(ctx) - if err != nil { - logger.Debug("CLI session ended", zap.Error(err)) + for req := range requests { + req.Reply(req.Type == "shell" || req.Type == "pty-req" || req.Type == "auth-agent-req@openssh.com", nil) } }() + + logger.Debug("start CLI session") + session := NewCLISession(channel, c.username, c.password, logger, vendors["cisco"]) + err := session.Run(ctx) + if err != nil { + logger.Debug("CLI session ended", zap.Error(err)) + } + }() +} + +type directTCPIPChannelData struct { + RAddr string + RPort uint32 + LAddr string + LPort uint32 +} + +func (c *connections) handleDirectTCPIPChannel(ctx context.Context, newChannel ssh.NewChannel, logger *zap.Logger) { + var channelData directTCPIPChannelData + if err := ssh.Unmarshal(newChannel.ExtraData(), &channelData); err != nil { + newChannel.Reject(ssh.ConnectionFailed, fmt.Sprintf("invalid direct-tcpip data: %s", err)) + return + } + remoteAddr := net.JoinHostPort(channelData.RAddr, strconv.Itoa(int(channelData.RPort))) + remoteConn, err := net.Dial("tcp", remoteAddr) + if err != nil { + newChannel.Reject(ssh.ConnectionFailed, err.Error()) + return } - return nil + channel, requests, err := newChannel.Accept() + if err != nil { + _ = remoteConn.Close() + logger.Error("could not accept direct-tcpip channel", zap.Error(err)) + return + } + go ssh.DiscardRequests(requests) + + go func() { + defer channel.Close() + defer remoteConn.Close() + + logger.Debug("start direct-tcpip", zap.String("remote", remoteAddr)) + copyDone := make(chan struct{}, 2) + go func() { + _, _ = io.Copy(channel, remoteConn) + copyDone <- struct{}{} + }() + go func() { + _, _ = io.Copy(remoteConn, channel) + copyDone <- struct{}{} + }() + + select { + case <-ctx.Done(): + case <-copyDone: + } + logger.Debug("direct-tcpip closed", zap.String("remote", remoteAddr)) + }() } // TelnetConnection implements ssh.Channel over a Telnet TCP connection. diff --git a/pkg/server/devuath.go b/pkg/server/devuath.go index 591e2b2..f24425a 100644 --- a/pkg/server/devuath.go +++ b/pkg/server/devuath.go @@ -4,9 +4,12 @@ import ( "fmt" "net/netip" "os" + "strconv" + "strings" "github.com/annetutil/gnetcli/pkg/credentials" pb "github.com/annetutil/gnetcli/pkg/server/proto" + "github.com/kevinburke/ssh_config" "go.uber.org/zap" ) @@ -35,16 +38,15 @@ type proxyConfig struct { func (m authApp) resolveProxyConfig(host string, ip netip.Addr) proxyConfig { cfg := proxyConfig{ip: ip} - // Priority: explicit ProxyJump from app config > SSH config settings + // Priority: explicit ProxyJump from app config > SSH config settings. if len(m.config.ProxyJump) > 0 { cfg.proxyJump = m.config.ProxyJump } else if m.config.SshConfig && m.sshConfig != nil { - // Read all SSH config settings when enabled cfg.proxyJump = m.sshConfig.Get(host, "ProxyJump") cfg.controlPath = m.sshConfig.Get(host, "ControlPath") if realHost := m.sshConfig.Get(host, "Hostname"); len(realHost) > 0 { cfg.connectHost = realHost - // Clear IP to ensure we connect to Hostname from config, not IP received from client + // Clear IP to ensure we connect to Hostname from config, not IP received from client. cfg.ip = netip.Addr{} } } @@ -57,6 +59,17 @@ func (m authApp) GetHostParams(host string, params *pb.HostParams) (hostParams, return hostParams{}, fmt.Errorf("host connection params: %w", err) } + if port == 0 && m.config.SshConfig && m.sshConfig != nil { + configPort := m.sshConfig.Get(host, "Port") + if len(configPort) > 0 { + parsedPort, err := strconv.Atoi(configPort) + if err != nil { + return hostParams{}, fmt.Errorf("invalid ssh config port for %s: %w", host, err) + } + port = parsedPort + } + } + cfg := m.resolveProxyConfig(host, ip) creds, err := m.Get(host) @@ -75,8 +88,7 @@ func (m authApp) GetHostParams(host string, params *pb.HostParams) (hostParams, func (m authApp) Get(host string) (credentials.Credentials, error) { if m.config.SshConfig { sshConfigPassphrase := "" // TODO: pass it - // here we read ssh config each call - cred, err := BuildCredsFromSSHConfig(m.config.Login, m.config.Password.Value(), host, sshConfigPassphrase, m.config.PrivateKey, m.log) + cred, err := m.buildCredsFromSSHConfig(m.config.Login, m.config.Password.Value(), host, sshConfigPassphrase, m.config.PrivateKey) if err != nil { return nil, err } @@ -116,3 +128,113 @@ func NewAuthApp(config authAppConfig, logger *zap.Logger) authApp { sshConfig: realSSHConfigReader{}, } } + +func NewAuthAppWithSSHConfig(config authAppConfig, logger *zap.Logger, sshConfigText string) (authApp, error) { + parsedConfig, err := ssh_config.Decode(strings.NewReader(sshConfigText)) + if err != nil { + return authApp{}, err + } + return authApp{config: config, log: logger, sshConfig: parsedSSHConfigReader{config: parsedConfig}}, nil +} + +func (m authApp) getSSHConfig(host, key string) string { + if m.sshConfig == nil { + return "" + } + return m.sshConfig.Get(host, key) +} + +func (m authApp) getSSHConfigAll(host, key string) []string { + if m.sshConfig == nil { + return nil + } + return m.sshConfig.GetAll(host, key) +} + +func (m authApp) buildCredsFromSSHConfig(login, password, host, sshConfigPassphrase, privateKeyPath string) (credentials.Credentials, error) { + var privateKeys [][]byte + if len(privateKeyPath) > 0 { + key, err := os.ReadFile(privateKeyPath) + if err != nil { + return nil, err + } + privateKeys = [][]byte{key} + } else { + var err error + privateKeys, err = m.getPrivateKeysFromSSHConfig(host) + if err != nil { + return nil, err + } + } + if len(login) == 0 { + configLogin := m.getSSHConfig(host, "User") + if len(configLogin) == 0 { + newLogin := credentials.GetLogin() + m.log.Debug("Use system login", zap.String("configLogin", newLogin)) + login = newLogin + } else { + login = configLogin + m.log.Debug("Use login from config", zap.String("configLogin", configLogin)) + } + } else { + m.log.Debug("Use login from input", zap.String("login", login)) + } + agentSocket, err := m.getAgentSocketFromSSHConfig(host) + if err != nil { + return nil, err + } + + opts := []credentials.CredentialsOption{ + credentials.WithUsername(login), + credentials.WithLogger(m.log), + credentials.WithSSHAgentSocket(agentSocket), + } + if len(password) > 0 { + opts = append(opts, credentials.WithPassword(credentials.Secret(password))) + } + if len(privateKeys) > 0 { + opts = append(opts, credentials.WithPrivateKeys(privateKeys)) + } + if len(sshConfigPassphrase) > 0 { + opts = append(opts, credentials.WithPassphrase(credentials.Secret(sshConfigPassphrase))) + } + + return credentials.NewSimpleCredentials(opts...), nil +} + +func (m authApp) getPrivateKeysFromSSHConfig(host string) ([][]byte, error) { + switch m.sshConfig.(type) { + case nil, realSSHConfigReader: + return credentials.GetPrivateKeysFromConfig(host) + } + + identityFiles := m.getSSHConfigAll(host, "IdentityFile") + privKeys := make([][]byte, 0, len(identityFiles)) + for _, v := range identityFiles { + content, err := os.ReadFile(v) + if err != nil { + return nil, fmt.Errorf("failed to read identity file %s: %w", v, err) + } + privKeys = append(privKeys, content) + } + return privKeys, nil +} + +func (m authApp) getAgentSocketFromSSHConfig(host string) (string, error) { + switch m.sshConfig.(type) { + case nil, realSSHConfigReader: + return credentials.GetAgentSocketFromConfig(host) + } + + ia := m.getSSHConfig(host, "IdentityAgent") + if ia == "none" { + return "", nil + } + if ia == "SSH_AUTH_SOCK" || len(ia) == 0 { + if m.getSSHConfig(host, "ForwardAgent") == "yes" || ia == "SSH_AUTH_SOCK" { + return credentials.GetDefaultAgentSocket(), nil + } + return "", nil + } + return ia, nil +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 2af8327..e0fc904 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -232,8 +232,11 @@ func (m *Server) createStreamerSSH(cfg StreamerConfig, add func(op gtrace.Operat if len(jumpHostParams.controlPath) > 0 { opts = append(opts, ssh.SSHTunnelWithControlFIle(jumpHostParams.controlPath)) } - + if jumpHostParams.port > 0 { + opts = append(opts, ssh.SSHTunnelWithPort(jumpHostParams.port)) + } connHost = cfg.params.host + tun := ssh.NewSSHTunnel(jumpHostParams.host, jumpHostParams.GetCredentials(), opts...) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -586,6 +589,9 @@ func (m *Server) getHostParams(hostname string, cmdParams *pb.HostParams) (hostP if defaultHostParams.controlPath != "" { res.controlPath = defaultHostParams.controlPath } + if defaultHostParams.port != 0 { + res.port = defaultHostParams.port + } if defaultHostParams.host != "" { res.host = defaultHostParams.host res.ip = defaultHostParams.ip diff --git a/pkg/server/server_integration_test.go b/pkg/server/server_integration_test.go index 0847768..10cfb4f 100644 --- a/pkg/server/server_integration_test.go +++ b/pkg/server/server_integration_test.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "testing" grpcmiddleware "github.com/grpc-ecosystem/go-grpc-middleware" @@ -18,8 +19,10 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "gopkg.in/yaml.v3" "github.com/annetutil/gnetcli/pkg/credentials" "github.com/annetutil/gnetcli/pkg/gswitch" @@ -27,11 +30,12 @@ import ( pb "github.com/annetutil/gnetcli/pkg/server/proto" ) -// DevAuth private_key from config is used for SSH to the device (gswitch). +// DevAuth private_key from yaml config and host params from ssh config are used for SSH to the device (gswitch). func TestDevAuthPrivateKeyUsedForSSH(t *testing.T) { if testing.Short() { t.Skip("integration test") } + ln, sshPort := newSSHServerPort(t) pub, priv, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err) @@ -40,50 +44,41 @@ func TestDevAuthPrivateKeyUsedForSSH(t *testing.T) { privPEM, err := ssh.MarshalPrivateKey(priv, "") require.NoError(t, err) - tmp := t.TempDir() - privPath := filepath.Join(tmp, "id_ed25519") + privPath := filepath.Join(t.TempDir(), "id_ed25519") require.NoError(t, os.WriteFile(privPath, pem.EncodeToMemory(privPEM), 0o600)) const user = "gnetuser" - swLogger := zap.NewNop() - if testing.Verbose() { - swLogger = zap.Must(zap.NewDevelopmentConfig().Build()) - } - ctx := t.Context() + serveTestSwitch(t, ctx, ln, func(req gswitch.AuthRequest) error { + if req.Method != gswitch.AuthMethodPublicKey { + return fmt.Errorf("unexpected auth method %q", req.Method) + } + if req.Username != user { + return fmt.Errorf("unexpected user %q", req.Username) + } + if req.PublicKey == nil || !bytes.Equal(req.PublicKey.Marshal(), sshPub.Marshal()) { + return fmt.Errorf("unexpected public key") + } + return nil + }) - go func() { - _ = gswitch.ServeSSH(ctx, ln, gswitch.SSHServerOptions{ - Logger: swLogger, - AuthCallback: func(req gswitch.AuthRequest) error { - if req.Method != gswitch.AuthMethodPublicKey { - return fmt.Errorf("unexpected auth method %q", req.Method) - } - if req.Username != user { - return fmt.Errorf("unexpected user %q", req.Username) - } - if req.PublicKey == nil || !bytes.Equal(req.PublicKey.Marshal(), sshPub.Marshal()) { - return fmt.Errorf("unexpected public key") - } - return nil - }, - ConnectionErrorProb: 0, - }) - }() - - logger := zap.NewNop() - var devAuth server.Config - devAuth.DevAuth.Login = user - devAuth.DevAuth.PrivateKey = privPath - devAuth.DevAuth.UseAgent = false - - client := newGnetcliTestClient(t, devAuth, logger) + cfg := serverConfigFromYAML(t, ` +dev_auth: + ssh_config: true +`) + sshConfig := fmt.Sprintf(` +Host mock-sw + HostName 127.0.0.1 + Port %d + User %s + IdentityFile %s +`, sshPort, user, privPath) + + client := newGnetcliTestClient(t, cfg, sshConfig, zap.NewNop()) res, err := client.Exec(ctx, &pb.CMD{ Host: "mock-sw", Cmd: "show version", HostParams: &pb.HostParams{ - Ip: "127.0.0.1", - Port: sshPort, Device: "cisco", }, }) @@ -91,7 +86,7 @@ func TestDevAuthPrivateKeyUsedForSSH(t *testing.T) { require.Contains(t, string(res.GetOut()), "Cisco IOS Software") } -// DevAuth login and password from config are used for SSH to the device (gswitch). +// DevAuth password from yaml config and host params from ssh config are used for SSH to the device (gswitch). func TestDevAuthLoginPasswordUsedForSSH(t *testing.T) { if testing.Short() { t.Skip("integration test") @@ -103,40 +98,33 @@ func TestDevAuthLoginPasswordUsedForSSH(t *testing.T) { const user = "switchadmin" const pass = "correct-horse-battery-staple" - swLogger := zap.NewNop() - if testing.Verbose() { - swLogger = zap.Must(zap.NewDevelopmentConfig().Build()) - } - - go func() { - _ = gswitch.ServeSSH(ctx, ln, gswitch.SSHServerOptions{ - Logger: swLogger, - AuthCallback: func(req gswitch.AuthRequest) error { - if req.Method != gswitch.AuthMethodPassword { - return fmt.Errorf("unexpected auth method %q", req.Method) - } - if req.Username != user || req.Password != pass { - return fmt.Errorf("bad credentials for %q", req.Username) - } - return nil - }, - ConnectionErrorProb: 0, - }) - }() - - logger := zap.NewNop() - var devAuth server.Config - devAuth.DevAuth.Login = user - devAuth.DevAuth.Password = credentials.Secret(pass) - devAuth.DevAuth.UseAgent = false + serveTestSwitch(t, ctx, ln, func(req gswitch.AuthRequest) error { + if req.Method != gswitch.AuthMethodPassword { + return fmt.Errorf("unexpected auth method %q", req.Method) + } + if req.Username != user || req.Password != pass { + return fmt.Errorf("bad credentials for %q", req.Username) + } + return nil + }) - client := newGnetcliTestClient(t, devAuth, logger) + cfg := serverConfigFromYAML(t, fmt.Sprintf(` +dev_auth: + ssh_config: true + password: %s +`, pass)) + sshConfig := fmt.Sprintf(` +Host mock-sw + HostName 127.0.0.1 + Port %d + User %s +`, sshPort, user) + + client := newGnetcliTestClient(t, cfg, sshConfig, zap.NewNop()) res, err := client.Exec(context.Background(), &pb.CMD{ Host: "mock-sw", Cmd: "show version", HostParams: &pb.HostParams{ - Ip: "127.0.0.1", - Port: sshPort, Device: "cisco", }, }) @@ -153,35 +141,33 @@ func TestDevAuthWrongPasswordSSHRejected(t *testing.T) { ctx := t.Context() const user = "switchadmin" - go func() { - _ = gswitch.ServeSSH(ctx, ln, gswitch.SSHServerOptions{ - Logger: zap.NewNop(), - AuthCallback: func(req gswitch.AuthRequest) error { - if req.Method != gswitch.AuthMethodPassword { - return fmt.Errorf("unexpected auth method %q", req.Method) - } - if req.Username != user || req.Password != "server-real-pass" { - return fmt.Errorf("bad credentials for %q", req.Username) - } - return nil - }, - ConnectionErrorProb: 0, - }) - }() - - logger := zap.NewNop() - var devAuth server.Config - devAuth.DevAuth.Login = user - devAuth.DevAuth.Password = credentials.Secret("wrong-pass") - devAuth.DevAuth.UseAgent = false + serveTestSwitch(t, ctx, ln, func(req gswitch.AuthRequest) error { + if req.Method != gswitch.AuthMethodPassword { + return fmt.Errorf("unexpected auth method %q", req.Method) + } + if req.Username != user || req.Password != "server-real-pass" { + return fmt.Errorf("bad credentials for %q", req.Username) + } + return nil + }) - client := newGnetcliTestClient(t, devAuth, logger) + cfg := serverConfigFromYAML(t, ` +dev_auth: + ssh_config: true + password: wrong-pass +`) + sshConfig := fmt.Sprintf(` +Host mock-sw + HostName 127.0.0.1 + Port %d + User %s +`, sshPort, user) + + client := newGnetcliTestClient(t, cfg, sshConfig, zap.NewNop()) _, err := client.Exec(context.Background(), &pb.CMD{ Host: "mock-sw", Cmd: "show version", HostParams: &pb.HostParams{ - Ip: "127.0.0.1", - Port: sshPort, Device: "cisco", }, }) @@ -189,10 +175,126 @@ func TestDevAuthWrongPasswordSSHRejected(t *testing.T) { require.Contains(t, err.Error(), "unable to authenticate") } -func newGnetcliTestClient(t *testing.T, devAuth server.Config, logger *zap.Logger) pb.GnetcliClient { +func TestDevAuthSSHConfigProxyJumpWithAgent(t *testing.T) { + if testing.Short() { + t.Skip("integration test") + } + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + sshPub, err := ssh.NewPublicKey(pub) + require.NoError(t, err) + agentSocket := startTestAgent(t, priv) + + targetLn, targetPort := newSSHServerPort(t) + proxyLn, proxyPort := newSSHServerPort(t) + ctx := t.Context() + + const targetUser = "target-user" + serveTestSwitch(t, ctx, targetLn, publicKeyAuthCallback(targetUser, sshPub)) + + const proxyUser = "proxy-user" + serveTestSwitch(t, ctx, proxyLn, publicKeyAuthCallback(proxyUser, sshPub)) + + cfg := serverConfigFromYAML(t, ` +dev_auth: + ssh_config: true +`) + sshConfig := fmt.Sprintf(` +Host mock-sw + HostName 127.0.0.1 + Port %d + User %s + IdentityAgent %s + ProxyJump mock-proxy + +Host mock-proxy + HostName 127.0.0.1 + Port %d + User %s + IdentityAgent %s + ForwardAgent yes +`, targetPort, targetUser, agentSocket, proxyPort, proxyUser, agentSocket) + + client := newGnetcliTestClient(t, cfg, sshConfig, zap.NewNop()) + res, err := client.Exec(ctx, &pb.CMD{ + Host: "mock-sw", + Cmd: "show version", + HostParams: &pb.HostParams{ + Device: "cisco", + }, + }) + require.NoError(t, err) + require.Contains(t, string(res.GetOut()), "Cisco IOS Software") +} + +func publicKeyAuthCallback(user string, pubKey ssh.PublicKey) gswitch.AuthCallback { + return func(req gswitch.AuthRequest) error { + if req.Method != gswitch.AuthMethodPublicKey { + return fmt.Errorf("unexpected auth method %q", req.Method) + } + if req.Username != user { + return fmt.Errorf("unexpected user %q", req.Username) + } + if req.PublicKey == nil || !bytes.Equal(req.PublicKey.Marshal(), pubKey.Marshal()) { + return fmt.Errorf("unexpected public key") + } + return nil + } +} + +func serveTestSwitch(t *testing.T, ctx context.Context, ln net.Listener, authCallback gswitch.AuthCallback) { + t.Helper() + + logger := zap.NewNop() + if testing.Verbose() { + logger = zap.Must(zap.NewDevelopmentConfig().Build()) + } + + go func() { + _ = gswitch.ServeSSH(ctx, ln, gswitch.SSHServerOptions{ + Logger: logger, + AuthCallback: authCallback, + ConnectionErrorProb: 0, + }) + }() +} + +func serverConfigFromYAML(t *testing.T, configText string) server.Config { t.Helper() - authApp := server.NewAuthApp(devAuth.DevAuth, logger) + var cfg server.Config + err := yaml.NewDecoder(strings.NewReader(configText)).Decode(&cfg) + require.NoError(t, err) + return cfg +} + +func newGnetcliTestClient(t *testing.T, cfg server.Config, args ...interface{}) pb.GnetcliClient { + t.Helper() + + var sshConfigText string + logger := zap.NewNop() + switch len(args) { + case 1: + var ok bool + logger, ok = args[0].(*zap.Logger) + require.True(t, ok, "expected logger as the third argument") + case 2: + var ok bool + sshConfigText, ok = args[0].(string) + require.True(t, ok, "expected ssh config text as the third argument") + logger, ok = args[1].(*zap.Logger) + require.True(t, ok, "expected logger as the fourth argument") + default: + require.Failf(t, "unexpected arguments", "got %d optional arguments", len(args)) + } + + authApp := server.NewAuthApp(cfg.DevAuth, logger) + if len(sshConfigText) > 0 { + var err error + authApp, err = server.NewAuthAppWithSSHConfig(cfg.DevAuth, logger, sshConfigText) + require.NoError(t, err) + } svc, err := server.New(authApp, "", server.WithLogger(logger)) require.NoError(t, err) @@ -222,6 +324,41 @@ func newGnetcliTestClient(t *testing.T, devAuth server.Config, logger *zap.Logge return pb.NewGnetcliClient(conn) } +func startTestAgent(t *testing.T, keys ...ed25519.PrivateKey) string { + t.Helper() + + socketFile, err := os.CreateTemp(os.TempDir(), "gnetcli-agent-*.sock") + require.NoError(t, err) + socketPath := socketFile.Name() + require.NoError(t, socketFile.Close()) + require.NoError(t, os.Remove(socketPath)) + + ln, err := net.Listen("unix", socketPath) + require.NoError(t, err) + t.Cleanup(func() { + _ = ln.Close() + _ = os.Remove(socketPath) + }) + + keyring := agent.NewKeyring() + for _, key := range keys { + err := keyring.Add(agent.AddedKey{PrivateKey: key}) + require.NoError(t, err) + } + + go func() { + for { + conn, err := ln.Accept() + if err != nil { + return + } + go func() { _ = agent.ServeAgent(keyring, conn) }() + } + }() + + return socketPath +} + func newSSHServerPort(t *testing.T) (net.Listener, int32) { t.Helper() diff --git a/pkg/server/sshconfig.go b/pkg/server/sshconfig.go index c190141..b9ee565 100644 --- a/pkg/server/sshconfig.go +++ b/pkg/server/sshconfig.go @@ -2,19 +2,44 @@ package server import "github.com/kevinburke/ssh_config" -// sshConfigReader is an interface for reading SSH config values +// sshConfigReader is an interface for reading SSH config values. type sshConfigReader interface { Get(host, key string) string + GetAll(host, key string) []string } -// realSSHConfigReader reads from actual SSH config file +// realSSHConfigReader reads from actual SSH config file. type realSSHConfigReader struct{} func (r realSSHConfigReader) Get(host, key string) string { return ssh_config.Get(host, key) } -// mockSSHConfigReader for testing +func (r realSSHConfigReader) GetAll(host, key string) []string { + return ssh_config.GetAll(host, key) +} + +type parsedSSHConfigReader struct { + config *ssh_config.Config +} + +func (r parsedSSHConfigReader) Get(host, key string) string { + value, err := r.config.Get(host, key) + if err != nil { + return "" + } + return value +} + +func (r parsedSSHConfigReader) GetAll(host, key string) []string { + value, err := r.config.GetAll(host, key) + if err != nil { + return nil + } + return value +} + +// mockSSHConfigReader for testing. type mockSSHConfigReader struct { configs map[string]map[string]string } @@ -25,3 +50,11 @@ func (m mockSSHConfigReader) Get(host, key string) string { } return "" } + +func (m mockSSHConfigReader) GetAll(host, key string) []string { + value := m.Get(host, key) + if len(value) == 0 { + return nil + } + return []string{value} +}