From 2ca2a34768e0d0067d89160026c9c619eced660b Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Thu, 17 Oct 2024 21:15:23 -0300 Subject: [PATCH 01/31] wip golang ssh --- go.mod | 5 ++ go.sum | 14 ++++ src/client/main.go | 166 ++++++++++++++++++++++++++++++++++++++++++-- src/context/main.go | 4 ++ 4 files changed, 183 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 1eadcc8..afd3aa2 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,13 @@ go 1.23.1 require ( github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/dusted-go/logging v1.3.0 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/neovim/go-client v1.2.2-0.20240514170004-863141a115a5 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/skeema/knownhosts v1.3.0 // indirect github.com/urfave/cli/v2 v2.27.4 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect ) diff --git a/go.sum b/go.sum index 806e908..c5f1332 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,27 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lV github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/dusted-go/logging v1.3.0 h1:SL/EH1Rp27oJQIte+LjWvWACSnYDTqNx5gZULin0XRY= github.com/dusted-go/logging v1.3.0/go.mod h1:s58+s64zE5fxSWWZfp+b8ZV0CHyKHjamITGyuY1wzGg= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/neovim/go-client v1.2.1 h1:kl3PgYgbnBfvaIoGYi3ojyXH0ouY6dJY/rYUCssZKqI= github.com/neovim/go-client v1.2.1/go.mod h1:EeqCP3z1vJd70JTaH/KXz9RMZ/nIgEFveX83hYnh/7c= github.com/neovim/go-client v1.2.2-0.20240514170004-863141a115a5 h1:bDKPFxHFy0ApEmtUFFQzbxMGgywlKrpyNJ2opMX4hjc= github.com/neovim/go-client v1.2.2-0.20240514170004-863141a115a5/go.mod h1:UBsOERb5epbeQT0nyPTZkmUPTffRYBcHvrXXidr1NQQ= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= diff --git a/src/client/main.go b/src/client/main.go index c4390b5..6f8dcb1 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -4,17 +4,24 @@ import ( "fmt" "log/slog" "math/rand/v2" + "net/url" "os" "os/exec" "os/signal" + "os/user" "path" + "path/filepath" "runtime" "strings" "time" "github.com/dusted-go/logging/prettylog" + "github.com/kevinburke/ssh_config" "github.com/neovim/go-client/nvim" + "github.com/skeema/knownhosts" "github.com/urfave/cli/v2" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/terminal" "nvrh/src/context" "nvrh/src/nvim_helpers" @@ -80,6 +87,17 @@ var CliClientOpenCommand = cli.Command{ Action: func(c *cli.Context) error { // Prepare the context. sessionId := fmt.Sprintf("%d", time.Now().Unix()) + + endpoint, err := parseServerString(c.Args().Get(0)) + if err != nil { + return err + } + + sshClient, err := getSshClientForServer(endpoint) + if err != nil { + return err + } + nvrhContext := context.NvrhContext{ SessionId: sessionId, Server: c.Args().Get(0), @@ -97,6 +115,8 @@ var CliClientOpenCommand = cli.Command{ SshPath: c.String("ssh-path"), Debug: c.Bool("debug"), + + SshClient: sshClient, } // Prepare the logger. @@ -124,6 +144,30 @@ var CliClientOpenCommand = cli.Command{ return fmt.Errorf(" is required") } + // client, err := ssh.Dial("tcp", "10.0.1.99:22", sshConfig) + // if err != nil { + // slog.Error("Failed to dial", "err", err) + // return err + // } + defer nvrhContext.SshClient.Close() + + // Each ClientConn can support multiple interactive sessions, + // represented by a Session. + session, err := nvrhContext.SshClient.NewSession() + if err != nil { + slog.Error("Failed to create session", "err", err) + return err + } + session.Stdout = os.Stdout + defer session.Close() + + // Once a Session is created, you can a single command on + // the remote side using the Run method. + if err := session.Run("uname -a"); err != nil { + slog.Error("Failed to run", "err", err) + return err + } + var nv *nvim.Nvim doneChan := make(chan error) @@ -209,7 +253,7 @@ var CliClientOpenCommand = cli.Command{ } }() - err := <-doneChan + err = <-doneChan slog.Info("Closing nvrh") closeNvimSocket(nv) @@ -303,11 +347,6 @@ os.execute('chmod +x ' .. browser_script_path) return true `, nil, nvrhContext.BrowserScriptPath, nvrhContext.RemoteSocketOrPort(), nv.ChannelID()) - - - - - if err := batch.Execute(); err != nil { return err } @@ -359,3 +398,118 @@ func closeNvimSocket(nv *nvim.Nvim) { } nv.Close() } + +func getSshClientForServer(endpoint *Endpoint) (*ssh.Client, error) { + kh, err := knownhosts.NewDB(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")) + if err != nil { + return nil, err + } + + slog.Debug("Connecting to server", "endpoint", endpoint) + + authMethods := []ssh.AuthMethod{} + + // identityFile := ssh_config.Get(endpoint.Host, "IdentityFile") + // slog.Debug("Identity file", "file", identityFile) + // if identityFile != "" { + // key, err := os.ReadFile(identityFile) + // if err != nil { + // slog.Error("unable to read private key", "err", err) + // return nil, err + // } + + // // Create the Signer for this private key. + // signer, err := ssh.ParsePrivateKey(key) + // if err != nil { + // slog.Error("unable to parse private key", "err", err) + // } + + // authMethods = append(authMethods, ssh.PublicKeys(signer)) + // } + + if len(authMethods) == 0 { + slog.Debug("No identity file found, using password auth") + fmt.Printf("Password for %s: ", endpoint) + password, err := terminal.ReadPassword(0) + if err != nil { + slog.Error("Error reading password", "err", err) + return nil, err + } + + authMethods = append(authMethods, ssh.Password(string(password))) + } + + config := &ssh.ClientConfig{ + User: endpoint.User, + Auth: authMethods, + HostKeyCallback: kh.HostKeyCallback(), + HostKeyAlgorithms: kh.HostKeyAlgorithms(fmt.Sprintf("%s:%s", endpoint.Host, endpoint.Port)), + } + + client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", endpoint.Host, endpoint.Port), config) + if err != nil { + slog.Error("Failed to dial", "err", err) + return nil, err + } + + return client, nil +} + +type Endpoint struct { + User string + Host string + Port string +} + +func (e *Endpoint) String() string { + return fmt.Sprintf("%s@%s:%s", e.User, e.Host, e.Port) +} + +func parseServerString(server string) (*Endpoint, error) { + currentUser, err := user.Current() + + if err != nil { + slog.Error("Error getting current user", "err", err) + return nil, err + } + + fallbackUsername := currentUser.Username + fallbackPort := "22" + + parsed, err := url.Parse(fmt.Sprintf("ssh://%s", server)) + if err != nil { + return nil, err + } + + givenHostname := parsed.Hostname() + givenUsername := parsed.User.Username() + givenPort := parsed.Port() + + finalUsername := givenUsername + if finalUsername == "" { + finalUsername = ssh_config.Get(givenHostname, "User") + } + if finalUsername == "" { + finalUsername = fallbackUsername + } + + finalPort := givenPort + if finalPort == "" { + finalPort = ssh_config.Get(givenHostname, "Port") + } + if finalPort == "" { + finalPort = fallbackPort + } + + finalHostname := givenHostname + configHostname := ssh_config.Get(givenHostname, "HostName") + if configHostname != "" { + finalHostname = configHostname + } + + return &Endpoint{ + User: finalUsername, + Host: finalHostname, + Port: finalPort, + }, nil +} diff --git a/src/context/main.go b/src/context/main.go index 5168b64..eb6c285 100644 --- a/src/context/main.go +++ b/src/context/main.go @@ -3,6 +3,8 @@ package context import ( "fmt" "os/exec" + + "golang.org/x/crypto/ssh" ) type NvrhContext struct { @@ -24,6 +26,8 @@ type NvrhContext struct { SshPath string Debug bool + + SshClient *ssh.Client } func (nc NvrhContext) LocalSocketOrPort() string { From 29d0b253b3a05da211419c199818595de437d87d Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Thu, 17 Oct 2024 21:17:16 -0300 Subject: [PATCH 02/31] cleanup --- src/client/main.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index 6f8dcb1..52eb0eb 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -144,11 +144,6 @@ var CliClientOpenCommand = cli.Command{ return fmt.Errorf(" is required") } - // client, err := ssh.Dial("tcp", "10.0.1.99:22", sshConfig) - // if err != nil { - // slog.Error("Failed to dial", "err", err) - // return err - // } defer nvrhContext.SshClient.Close() // Each ClientConn can support multiple interactive sessions, From ba29e85f027fb8ad5199a891d9a70c23f3e1b625 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Sun, 20 Oct 2024 02:35:15 -0300 Subject: [PATCH 03/31] omg --- src/client/main.go | 140 ++++++++++++++++++++++++---------------- src/ssh_helpers/main.go | 139 +++++++++++++++++++++++++++++++-------- 2 files changed, 196 insertions(+), 83 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index 52eb0eb..4c6586b 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "math/rand/v2" + "net" "net/url" "os" "os/exec" @@ -21,6 +22,7 @@ import ( "github.com/skeema/knownhosts" "github.com/urfave/cli/v2" "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" "golang.org/x/crypto/ssh/terminal" "nvrh/src/context" @@ -146,23 +148,6 @@ var CliClientOpenCommand = cli.Command{ defer nvrhContext.SshClient.Close() - // Each ClientConn can support multiple interactive sessions, - // represented by a Session. - session, err := nvrhContext.SshClient.NewSession() - if err != nil { - slog.Error("Failed to create session", "err", err) - return err - } - session.Stdout = os.Stdout - defer session.Close() - - // Once a Session is created, you can a single command on - // the remote side using the Run method. - if err := session.Run("uname -a"); err != nil { - slog.Error("Failed to run", "err", err) - return err - } - var nv *nvim.Nvim doneChan := make(chan error) @@ -172,28 +157,17 @@ var CliClientOpenCommand = cli.Command{ // Prepare remote instance. go func() { - remoteCmd := ssh_helpers.BuildRemoteNvimCmd(&nvrhContext) - if nvrhContext.Debug { - remoteCmd.Stdout = os.Stdout - remoteCmd.Stderr = os.Stderr - // remoteCmd.Stdin = os.Stdin - } - nvrhContext.CommandsToKill = append(nvrhContext.CommandsToKill, remoteCmd) - - // We don't want the ssh process ending too early, if it does we can't - // clean up the remote nvim instance. - // exec_helpers.PrepareForForking(remoteCmd) - - if err := remoteCmd.Start(); err != nil { - slog.Error("Error starting ssh", "err", err) + go ssh_helpers.TunnelSshSocket(&nvrhContext, ssh_helpers.SshTunnelInfo{ + Mode: "unix", + LocalSocket: nvrhContext.LocalSocketPath, + RemoteSocket: nvrhContext.RemoteSocketPath, + }) + + nvimCommandString := ssh_helpers.BuildRemoteCommandString(&nvrhContext) + nvimCommandString = fmt.Sprintf("$SHELL -i -c '%s'", nvimCommandString) + slog.Info("Starting remote nvim", "nvimCommandString", nvimCommandString) + if err := ssh_helpers.RunCommand(&nvrhContext, nvimCommandString); err != nil { doneChan <- err - return - } - - if err := remoteCmd.Wait(); err != nil { - slog.Error("Error running ssh", "err", err) - } else { - slog.Info("Remote nvim exited") } }() @@ -405,34 +379,90 @@ func getSshClientForServer(endpoint *Endpoint) (*ssh.Client, error) { authMethods := []ssh.AuthMethod{} // identityFile := ssh_config.Get(endpoint.Host, "IdentityFile") - // slog.Debug("Identity file", "file", identityFile) // if identityFile != "" { - // key, err := os.ReadFile(identityFile) - // if err != nil { - // slog.Error("unable to read private key", "err", err) - // return nil, err + // if _, err := os.Stat(identityFile); os.IsExist(err) { + // slog.Debug("Using identity file", "identityFile", identityFile) + + // key, err := os.ReadFile(identityFile) + // if err != nil { + // slog.Error("unable to read private key", "err", err) + // return nil, err + // } + + // // Create the Signer for this private key. + // signer, err := ssh.ParsePrivateKey(key) + // if err != nil { + // slog.Error("unable to parse private key", "err", err) + // } + + // authMethods = append(authMethods, ssh.PublicKeys(signer)) // } + // } - // // Create the Signer for this private key. - // signer, err := ssh.ParsePrivateKey(key) - // if err != nil { - // slog.Error("unable to parse private key", "err", err) - // } + authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { + identityFile := ssh_config.Get(endpoint.Host, "IdentityFile") - // authMethods = append(authMethods, ssh.PublicKeys(signer)) - // } + if identityFile == "" { + return nil, nil + } + + if _, err := os.Stat(identityFile); os.IsNotExist(err) { + slog.Error("Identity file does not exist", "identityFile", identityFile) + return nil, nil + } - if len(authMethods) == 0 { - slog.Debug("No identity file found, using password auth") + slog.Debug("Using identity file", "identityFile", identityFile) + + key, err := os.ReadFile(identityFile) + if err != nil { + slog.Error("unable to read private key", "err", err) + return nil, err + } + + // Create the Signer for this private key. + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + slog.Error("unable to parse private key", "err", err) + } + + return []ssh.Signer{signer}, nil + })) + + sshAuthSock := ssh_config.Get(endpoint.Host, "IdentityAgent") + if sshAuthSock == "" { + sshAuthSock = os.Getenv("SSH_AUTH_SOCK") + } + + if sshAuthSock != "" { + userHomeDir, err := os.UserHomeDir() + if err != nil { + slog.Error("Error getting user home dir", "err", err) + } else { + sshAuthSock = strings.ReplaceAll(sshAuthSock, "\"", "") + sshAuthSock = strings.ReplaceAll(sshAuthSock, "$HOME", userHomeDir) + sshAuthSock = strings.ReplaceAll(sshAuthSock, "~", userHomeDir) + + slog.Debug("Using ssh agent", "socket", sshAuthSock) + conn, err := net.Dial("unix", sshAuthSock) + if err != nil { + slog.Error("Failed to open SSH auth socket", "err", err) + } else { + agentClient := agent.NewClient(conn) + authMethods = append(authMethods, ssh.PublicKeysCallback(agentClient.Signers)) + } + } + } + + authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) { fmt.Printf("Password for %s: ", endpoint) password, err := terminal.ReadPassword(0) if err != nil { slog.Error("Error reading password", "err", err) - return nil, err + return "", err } - authMethods = append(authMethods, ssh.Password(string(password))) - } + return string(password), nil + })) config := &ssh.ClientConfig{ User: endpoint.User, diff --git a/src/ssh_helpers/main.go b/src/ssh_helpers/main.go index 7b91686..1a10fa0 100644 --- a/src/ssh_helpers/main.go +++ b/src/ssh_helpers/main.go @@ -2,16 +2,20 @@ package ssh_helpers import ( "fmt" + "io" "log/slog" + "net" "nvrh/src/context" + "os" "os/exec" "strings" "github.com/neovim/go-client/nvim" + "golang.org/x/crypto/ssh" ) func BuildRemoteNvimCmd(nvrhContext *context.NvrhContext) *exec.Cmd { - nvimCommandString := buildRemoteCommandString(nvrhContext) + nvimCommandString := BuildRemoteCommandString(nvrhContext) slog.Info("Starting remote nvim", "nvimCommandString", nvimCommandString) tunnel := fmt.Sprintf("%s:%s", nvrhContext.LocalSocketPath, nvrhContext.RemoteSocketPath) @@ -48,7 +52,7 @@ func BuildRemoteNvimCmd(nvrhContext *context.NvrhContext) *exec.Cmd { return sshCommand } -func buildRemoteCommandString(nvrhContext *context.NvrhContext) string { +func BuildRemoteCommandString(nvrhContext *context.NvrhContext) string { envPairsString := "" if len(nvrhContext.RemoteEnv) > 0 { envPairsString = strings.Join(nvrhContext.RemoteEnv, " ") @@ -64,31 +68,110 @@ func buildRemoteCommandString(nvrhContext *context.NvrhContext) string { func MakeRpcTunnelHandler(nvrhContext *context.NvrhContext) func(*nvim.Nvim, []string) { return func(v *nvim.Nvim, args []string) { - go func() { - slog.Info("Tunneling", "server", nvrhContext.Server, "port", args[0]) - - sshCommand := exec.Command( - nvrhContext.SshPath, - "-NL", - fmt.Sprintf("%s:0.0.0.0:%s", args[0], args[0]), - nvrhContext.Server, - ) - - // sshCommand.Stdout = os.Stdout - // sshCommand.Stderr = os.Stderr - // sshCommand.Stdin = os.Stdin - nvrhContext.CommandsToKill = append(nvrhContext.CommandsToKill, sshCommand) - - if err := sshCommand.Start(); err != nil { - slog.Error("Error starting tunnel", "err", err) - return - } - - defer sshCommand.Process.Kill() - - if err := sshCommand.Wait(); err != nil { - slog.Error("Error running tunnel", "err", err) - } - }() + go TunnelSshSocket(nvrhContext, SshTunnelInfo{ + Mode: "port", + LocalSocket: fmt.Sprintf("%s", args[0]), + RemoteSocket: fmt.Sprintf("%s", args[0]), + }) } } + +type SshTunnelInfo struct { + Mode string + LocalSocket string + RemoteSocket string +} + +func (ti SshTunnelInfo) LocalListener() (net.Listener, error) { + switch ti.Mode { + case "unix": + return net.Listen("unix", ti.LocalSocket) + case "port": + return net.Listen("tcp", fmt.Sprintf("localhost:%s", ti.LocalSocket)) + } + + return nil, fmt.Errorf("Invalid mode: %s", ti.Mode) +} + +func (ti SshTunnelInfo) RemoteListener(sshClient *ssh.Client) (net.Conn, error) { + switch ti.Mode { + case "unix": + return sshClient.Dial("unix", ti.RemoteSocket) + case "port": + return sshClient.Dial("tcp", fmt.Sprintf("localhost:%s", ti.RemoteSocket)) + } + + return nil, fmt.Errorf("Invalid mode: %s", ti.Mode) +} + +func TunnelSshSocket(nvrhContext *context.NvrhContext, tunnelInfo SshTunnelInfo) { + // Listen on the local Unix socket + localListener, err := tunnelInfo.LocalListener() + if err != nil { + slog.Error("Failed to listen on local socket", "err", err) + return + } + + defer localListener.Close() + + defer func() { + // Clean up local socket file + os.Remove(tunnelInfo.LocalSocket) + }() + + slog.Info("Tunneling SSH socket", "LocalSocke", tunnelInfo.LocalSocket, "RemoteSocket", tunnelInfo.RemoteSocket) + + for { + // Accept incoming connections + localConn, err := localListener.Accept() + if err != nil { + slog.Error("Failed to accept connection", "err", err) + continue + } + + // Establish a connection to the remote socket via SSH + remoteConn, err := tunnelInfo.RemoteListener(nvrhContext.SshClient) + if err != nil { + slog.Error("Failed to dial remote socket", "err", err) + localConn.Close() + continue + } + + // Start a goroutine to handle the connection + go handleConnection(localConn, remoteConn) + } +} + +func handleConnection(localConn net.Conn, remoteConn net.Conn) { + // Close connections when done + defer localConn.Close() + defer remoteConn.Close() + + // Copy data from local to remote + go io.Copy(remoteConn, localConn) + // Copy data from remote to local + io.Copy(localConn, remoteConn) +} + +func RunCommand(nvrhContext *context.NvrhContext, command string) error { + session, err := nvrhContext.SshClient.NewSession() + + if err != nil { + slog.Error("Failed to create session", "err", err) + return err + } + + defer session.Close() + + if nvrhContext.Debug { + session.Stdout = os.Stdout + session.Stderr = os.Stderr + } + + if err := session.Run(command); err != nil { + slog.Error("Failed to run command", "err", err) + return err + } + + return nil +} From 8ba25c902ef426405844b32c095f8e69fdcf2bbf Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Sun, 20 Oct 2024 11:05:13 -0300 Subject: [PATCH 04/31] disable authkey for now --- src/client/main.go | 68 ++++++++++++++++------------------------------ 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index 4c6586b..aadf1f8 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -378,55 +378,35 @@ func getSshClientForServer(endpoint *Endpoint) (*ssh.Client, error) { authMethods := []ssh.AuthMethod{} - // identityFile := ssh_config.Get(endpoint.Host, "IdentityFile") - // if identityFile != "" { - // if _, err := os.Stat(identityFile); os.IsExist(err) { - // slog.Debug("Using identity file", "identityFile", identityFile) - - // key, err := os.ReadFile(identityFile) - // if err != nil { - // slog.Error("unable to read private key", "err", err) - // return nil, err - // } - - // // Create the Signer for this private key. - // signer, err := ssh.ParsePrivateKey(key) - // if err != nil { - // slog.Error("unable to parse private key", "err", err) - // } - - // authMethods = append(authMethods, ssh.PublicKeys(signer)) - // } - // } - - authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { - identityFile := ssh_config.Get(endpoint.Host, "IdentityFile") + // authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { + // identityFile := ssh_config.Get(endpoint.Host, "IdentityFile") - if identityFile == "" { - return nil, nil - } + // if identityFile == "" { + // return nil, nil + // } - if _, err := os.Stat(identityFile); os.IsNotExist(err) { - slog.Error("Identity file does not exist", "identityFile", identityFile) - return nil, nil - } + // if _, err := os.Stat(identityFile); os.IsNotExist(err) { + // slog.Error("Identity file does not exist", "identityFile", identityFile) + // return nil, err + // } - slog.Debug("Using identity file", "identityFile", identityFile) + // slog.Info("Using identity file", "identityFile", identityFile) - key, err := os.ReadFile(identityFile) - if err != nil { - slog.Error("unable to read private key", "err", err) - return nil, err - } + // key, err := os.ReadFile(identityFile) + // if err != nil { + // slog.Error("unable to read private key", "err", err) + // return nil, err + // } - // Create the Signer for this private key. - signer, err := ssh.ParsePrivateKey(key) - if err != nil { - slog.Error("unable to parse private key", "err", err) - } + // // Create the Signer for this private key. + // signer, err := ssh.ParsePrivateKey(key) + // if err != nil { + // slog.Error("unable to parse private key", "err", err) + // return nil, err + // } - return []ssh.Signer{signer}, nil - })) + // return []ssh.Signer{signer}, nil + // })) sshAuthSock := ssh_config.Get(endpoint.Host, "IdentityAgent") if sshAuthSock == "" { @@ -442,11 +422,11 @@ func getSshClientForServer(endpoint *Endpoint) (*ssh.Client, error) { sshAuthSock = strings.ReplaceAll(sshAuthSock, "$HOME", userHomeDir) sshAuthSock = strings.ReplaceAll(sshAuthSock, "~", userHomeDir) - slog.Debug("Using ssh agent", "socket", sshAuthSock) conn, err := net.Dial("unix", sshAuthSock) if err != nil { slog.Error("Failed to open SSH auth socket", "err", err) } else { + slog.Info("Using ssh agent", "socket", sshAuthSock) agentClient := agent.NewClient(conn) authMethods = append(authMethods, ssh.PublicKeysCallback(agentClient.Signers)) } From ac82ac3b108729662e3f3366dedf955005a79d41 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Sun, 20 Oct 2024 11:05:21 -0300 Subject: [PATCH 05/31] cleanup files on remote --- src/client/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/main.go b/src/client/main.go index aadf1f8..06a0090 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -169,6 +169,9 @@ var CliClientOpenCommand = cli.Command{ if err := ssh_helpers.RunCommand(&nvrhContext, nvimCommandString); err != nil { doneChan <- err } + + ssh_helpers.RunCommand(&nvrhContext, fmt.Sprintf("rm -f '%s'", nvrhContext.RemoteSocketPath)) + ssh_helpers.RunCommand(&nvrhContext, fmt.Sprintf("rm -f '%s'", nvrhContext.BrowserScriptPath)) }() // Prepare client instance. From 14a91ffca42a1b6e1804ccca3f278395c6a0ef7d Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Sun, 20 Oct 2024 21:02:53 -0300 Subject: [PATCH 06/31] wip, password, agent, and passphrase-less pubkey seem to work! --- src/client/main.go | 141 +++++++++++++++++++++++++++++---------------- 1 file changed, 90 insertions(+), 51 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index 06a0090..d946f7d 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -381,64 +381,24 @@ func getSshClientForServer(endpoint *Endpoint) (*ssh.Client, error) { authMethods := []ssh.AuthMethod{} - // authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { - // identityFile := ssh_config.Get(endpoint.Host, "IdentityFile") + authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { + allSigners := []ssh.Signer{} - // if identityFile == "" { - // return nil, nil - // } - - // if _, err := os.Stat(identityFile); os.IsNotExist(err) { - // slog.Error("Identity file does not exist", "identityFile", identityFile) - // return nil, err - // } - - // slog.Info("Using identity file", "identityFile", identityFile) - - // key, err := os.ReadFile(identityFile) - // if err != nil { - // slog.Error("unable to read private key", "err", err) - // return nil, err - // } - - // // Create the Signer for this private key. - // signer, err := ssh.ParsePrivateKey(key) - // if err != nil { - // slog.Error("unable to parse private key", "err", err) - // return nil, err - // } - - // return []ssh.Signer{signer}, nil - // })) - - sshAuthSock := ssh_config.Get(endpoint.Host, "IdentityAgent") - if sshAuthSock == "" { - sshAuthSock = os.Getenv("SSH_AUTH_SOCK") - } - - if sshAuthSock != "" { - userHomeDir, err := os.UserHomeDir() - if err != nil { - slog.Error("Error getting user home dir", "err", err) - } else { - sshAuthSock = strings.ReplaceAll(sshAuthSock, "\"", "") - sshAuthSock = strings.ReplaceAll(sshAuthSock, "$HOME", userHomeDir) - sshAuthSock = strings.ReplaceAll(sshAuthSock, "~", userHomeDir) + if identitySigner, _ := getSignerForIdentityFile(endpoint.Host); identitySigner != nil { + allSigners = append(allSigners, identitySigner) + } - conn, err := net.Dial("unix", sshAuthSock) - if err != nil { - slog.Error("Failed to open SSH auth socket", "err", err) - } else { - slog.Info("Using ssh agent", "socket", sshAuthSock) - agentClient := agent.NewClient(conn) - authMethods = append(authMethods, ssh.PublicKeysCallback(agentClient.Signers)) - } + if agentSigners, _ := getSignersForIdentityAgent(endpoint.Host); agentSigners != nil { + allSigners = append(allSigners, agentSigners...) } - } + + return allSigners, nil + })) authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) { fmt.Printf("Password for %s: ", endpoint) password, err := terminal.ReadPassword(0) + fmt.Println() if err != nil { slog.Error("Error reading password", "err", err) return "", err @@ -463,6 +423,85 @@ func getSshClientForServer(endpoint *Endpoint) (*ssh.Client, error) { return client, nil } +func getSignerForIdentityFile(hostname string) (ssh.Signer, error) { + identityFile := ssh_config.Get(hostname, "IdentityFile") + + if identityFile == "" { + return nil, nil + } + + identityFile = cleanupSshConfigValue(identityFile) + + if _, err := os.Stat(identityFile); os.IsNotExist(err) { + slog.Error("Identity file does not exist", "identityFile", identityFile) + return nil, err + } + + slog.Info("Using identity file", "identityFile", identityFile) + + key, err := os.ReadFile(identityFile) + if err != nil { + slog.Error("Unable to read private key", "err", err) + return nil, err + } + + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + slog.Error("Unable to parse private key", "err", err) + return nil, err + } + + return signer, nil +} + +func getSignersForIdentityAgent(hostname string) ([]ssh.Signer, error) { + sshAuthSock := ssh_config.Get(hostname, "IdentityAgent") + + if sshAuthSock == "" { + sshAuthSock = os.Getenv("SSH_AUTH_SOCK") + } + + if sshAuthSock == "" { + return nil, nil + } + + sshAuthSock = cleanupSshConfigValue(sshAuthSock) + + conn, err := net.Dial("unix", sshAuthSock) + if err != nil { + slog.Error("Failed to open SSH auth socket", "err", err) + return nil, err + } + + slog.Info("Using ssh agent", "socket", sshAuthSock) + agentClient := agent.NewClient(conn) + agentSigners, err := agentClient.Signers() + if err != nil { + slog.Error("Error getting signers from agent", "err", err) + return nil, err + } + + return agentSigners, nil +} + +func cleanupSshConfigValue(value string) string { + replaced := strings.Trim(value, "\"") + + userHomeDir, err := os.UserHomeDir() + if err != nil { + slog.Warn("Error getting user home dir", "err", err) + return replaced + } + + replaced = strings.ReplaceAll(replaced, "$HOME", userHomeDir) + if strings.HasPrefix(replaced, "~/") { + replaced = strings.Replace(replaced, "~", userHomeDir, 1) + } + + return replaced +} + +// TODO Really needs "GivenHostName", "ResolvedHostName", etc type Endpoint struct { User string Host string From 3b00fd59d2ab60fd1f6ff5676b79f7b29fb942a3 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Mon, 21 Oct 2024 00:44:07 -0300 Subject: [PATCH 07/31] support passphrases --- src/client/main.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index d946f7d..c4752a3 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -23,7 +23,7 @@ import ( "github.com/urfave/cli/v2" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" - "golang.org/x/crypto/ssh/terminal" + "golang.org/x/term" "nvrh/src/context" "nvrh/src/nvim_helpers" @@ -397,7 +397,7 @@ func getSshClientForServer(endpoint *Endpoint) (*ssh.Client, error) { authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) { fmt.Printf("Password for %s: ", endpoint) - password, err := terminal.ReadPassword(0) + password, err := term.ReadPassword(0) fmt.Println() if err != nil { slog.Error("Error reading password", "err", err) @@ -447,8 +447,19 @@ func getSignerForIdentityFile(hostname string) (ssh.Signer, error) { signer, err := ssh.ParsePrivateKey(key) if err != nil { - slog.Error("Unable to parse private key", "err", err) - return nil, err + slog.Error("Unable to parse private key, maybe it's password protected", "err", err) + + fmt.Printf("Passphrase for %s: ", identityFile) + passPhrase, _ := term.ReadPassword(0) + fmt.Println() + + signer, signerErr := ssh.ParsePrivateKeyWithPassphrase(key, passPhrase) + if signerErr != nil { + slog.Error("Unable to parse private key", "err", signerErr) + return nil, signerErr + } + + return signer, nil } return signer, nil From bc19c09f873298d801df8e42a9dfc14ca2f77cfa Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Mon, 21 Oct 2024 18:10:20 -0300 Subject: [PATCH 08/31] better check for passphrase --- src/client/main.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index c4752a3..e0d8ea8 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -447,19 +447,20 @@ func getSignerForIdentityFile(hostname string) (ssh.Signer, error) { signer, err := ssh.ParsePrivateKey(key) if err != nil { - slog.Error("Unable to parse private key, maybe it's password protected", "err", err) + if _, ok := err.(*ssh.PassphraseMissingError); ok { + passPhrase, _ := askForPassword(fmt.Sprintf("Passphrase for %s: ", identityFile)) - fmt.Printf("Passphrase for %s: ", identityFile) - passPhrase, _ := term.ReadPassword(0) - fmt.Println() + signer, signerErr := ssh.ParsePrivateKeyWithPassphrase(key, passPhrase) + if signerErr != nil { + slog.Error("Unable to parse private key", "err", signerErr) + return nil, signerErr + } - signer, signerErr := ssh.ParsePrivateKeyWithPassphrase(key, passPhrase) - if signerErr != nil { - slog.Error("Unable to parse private key", "err", signerErr) - return nil, signerErr + return signer, nil } - return signer, nil + slog.Error("Unable to parse private key", "err", err) + return nil, err } return signer, nil From 656f15d839bd70a30ad77b40c6c1dbcc610197f3 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Mon, 21 Oct 2024 18:10:29 -0300 Subject: [PATCH 09/31] extract askForPassword --- src/client/main.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index e0d8ea8..cd04aed 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -396,9 +396,7 @@ func getSshClientForServer(endpoint *Endpoint) (*ssh.Client, error) { })) authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) { - fmt.Printf("Password for %s: ", endpoint) - password, err := term.ReadPassword(0) - fmt.Println() + password, err := askForPassword(fmt.Sprintf("Password for %s: ", endpoint)) if err != nil { slog.Error("Error reading password", "err", err) return "", err @@ -572,3 +570,15 @@ func parseServerString(server string) (*Endpoint, error) { Port: finalPort, }, nil } + +func askForPassword(message string) ([]byte, error) { + fmt.Print(message) + password, err := term.ReadPassword(0) + fmt.Println() + + if err != nil { + return nil, err + } + + return password, nil +} From 917a7ffff6732a356ea4ce5e06570026e3199d87 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Mon, 21 Oct 2024 18:46:16 -0300 Subject: [PATCH 10/31] oh I've botched this merge --- src/client/main.go | 50 ++++++++++++++++------------------------- src/context/main.go | 6 ++--- src/logger/main.go | 27 ++++++++++++++++++++++ src/ssh_helpers/main.go | 7 ++++-- 4 files changed, 54 insertions(+), 36 deletions(-) create mode 100644 src/logger/main.go diff --git a/src/client/main.go b/src/client/main.go index cd04aed..2c37f6d 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -26,6 +26,7 @@ import ( "golang.org/x/term" "nvrh/src/context" + "nvrh/src/logger" "nvrh/src/nvim_helpers" "nvrh/src/ssh_helpers" ) @@ -87,6 +88,9 @@ var CliClientOpenCommand = cli.Command{ }, Action: func(c *cli.Context) error { + isDebug := c.Bool("debug") + logger.PrepareLogger(isDebug) + // Prepare the context. sessionId := fmt.Sprintf("%d", time.Now().Unix()) @@ -100,7 +104,7 @@ var CliClientOpenCommand = cli.Command{ return err } - nvrhContext := context.NvrhContext{ + nvrhContext := &context.NvrhContext{ SessionId: sessionId, Server: c.Args().Get(0), RemoteDirectory: c.Args().Get(1), @@ -111,31 +115,16 @@ var CliClientOpenCommand = cli.Command{ ShouldUsePorts: c.Bool("use-ports"), RemoteSocketPath: fmt.Sprintf("/tmp/nvrh-socket-%s", sessionId), - LocalSocketPath: path.Join(os.TempDir(), fmt.Sprintf("nvrh-socket-%s", sessionId)), + LocalSocketPath: filepath.Join(os.TempDir(), fmt.Sprintf("nvrh-socket-%s", sessionId)), BrowserScriptPath: fmt.Sprintf("/tmp/nvrh-browser-%s", sessionId), SshPath: c.String("ssh-path"), - Debug: c.Bool("debug"), + Debug: isDebug, SshClient: sshClient, } - // Prepare the logger. - logLevel := slog.LevelInfo - if nvrhContext.Debug { - logLevel = slog.LevelDebug - } - log := slog.New(prettylog.New( - &slog.HandlerOptions{ - Level: logLevel, - AddSource: nvrhContext.Debug, - }, - prettylog.WithDestinationWriter(os.Stderr), - prettylog.WithColor(), - )) - slog.SetDefault(log) - if nvrhContext.ShouldUsePorts { min := 1025 max := 65535 @@ -157,27 +146,27 @@ var CliClientOpenCommand = cli.Command{ // Prepare remote instance. go func() { - go ssh_helpers.TunnelSshSocket(&nvrhContext, ssh_helpers.SshTunnelInfo{ + go ssh_helpers.TunnelSshSocket(nvrhContext, ssh_helpers.SshTunnelInfo{ Mode: "unix", LocalSocket: nvrhContext.LocalSocketPath, RemoteSocket: nvrhContext.RemoteSocketPath, }) - nvimCommandString := ssh_helpers.BuildRemoteCommandString(&nvrhContext) + nvimCommandString := ssh_helpers.BuildRemoteCommandString(nvrhContext) nvimCommandString = fmt.Sprintf("$SHELL -i -c '%s'", nvimCommandString) slog.Info("Starting remote nvim", "nvimCommandString", nvimCommandString) - if err := ssh_helpers.RunCommand(&nvrhContext, nvimCommandString); err != nil { + if err := ssh_helpers.RunCommand(nvrhContext, nvimCommandString); err != nil { doneChan <- err } - ssh_helpers.RunCommand(&nvrhContext, fmt.Sprintf("rm -f '%s'", nvrhContext.RemoteSocketPath)) - ssh_helpers.RunCommand(&nvrhContext, fmt.Sprintf("rm -f '%s'", nvrhContext.BrowserScriptPath)) + ssh_helpers.RunCommand(nvrhContext, fmt.Sprintf("rm -f '%s'", nvrhContext.RemoteSocketPath)) + ssh_helpers.RunCommand(nvrhContext, fmt.Sprintf("rm -f '%s'", nvrhContext.BrowserScriptPath)) }() // Prepare client instance. nvChan := make(chan *nvim.Nvim, 1) go func() { - nv, err := nvim_helpers.WaitForNvim(&nvrhContext) + nv, err := nvim_helpers.WaitForNvim(nvrhContext) if err != nil { slog.Error("Error connecting to nvim", "err", err) @@ -187,11 +176,11 @@ var CliClientOpenCommand = cli.Command{ slog.Info("Connected to nvim") nvChan <- nv - if err := prepareRemoteNvim(&nvrhContext, nv); err != nil { - slog.Error("Error preparing remote nvim", "err", err) + if err := prepareRemoteNvim(nvrhContext, nv); err != nil { + slog.Warn("Error preparing remote nvim", "err", err) } - clientCmd := BuildClientNvimCmd(&nvrhContext) + clientCmd := BuildClientNvimCmd(nvrhContext) if nvrhContext.Debug { clientCmd.Stdout = os.Stdout clientCmd.Stderr = os.Stderr @@ -230,6 +219,7 @@ var CliClientOpenCommand = cli.Command{ slog.Info("Closing nvrh") closeNvimSocket(nv) killAllCmds(nvrhContext.CommandsToKill) + os.Remove(nvrhContext.LocalSocketPath) if err != nil { return err @@ -269,8 +259,6 @@ func prepareRemoteNvim(nvrhContext *context.NvrhContext, nv *nvim.Nvim) error { batch.Command(fmt.Sprintf(`let $BROWSER="%s"`, nvrhContext.BrowserScriptPath)) // Add command to tunnel port. - // TODO use `vim.api.nvim_create_user_command`, and check to see if the - // port is already mapped somehow. batch.ExecLua(` vim.api.nvim_create_user_command( 'NvrhTunnelPort', @@ -353,7 +341,7 @@ func killAllCmds(cmds []*exec.Cmd) { slog.Debug("Killing command", "cmd", cmd.Args) if cmd.Process != nil { if err := cmd.Process.Kill(); err != nil { - slog.Error("Error killing command", "err", err) + slog.Warn("Error killing command", "err", err) } } } @@ -366,7 +354,7 @@ func closeNvimSocket(nv *nvim.Nvim) { slog.Info("Closing nvim") if err := nv.ExecLua("vim.cmd('qall!')", nil, nil); err != nil { - slog.Error("Error closing remote nvim", "err", err) + slog.Warn("Error closing remote nvim", "err", err) } nv.Close() } diff --git a/src/context/main.go b/src/context/main.go index eb6c285..74f0093 100644 --- a/src/context/main.go +++ b/src/context/main.go @@ -30,7 +30,7 @@ type NvrhContext struct { SshClient *ssh.Client } -func (nc NvrhContext) LocalSocketOrPort() string { +func (nc *NvrhContext) LocalSocketOrPort() string { if nc.ShouldUsePorts { // nvim-qt, at least on Windows (and might have something to do with // running in a VM) seems to prefer `127.0.0.1` to `0.0.0.0`, and I think @@ -41,9 +41,9 @@ func (nc NvrhContext) LocalSocketOrPort() string { return nc.LocalSocketPath } -func (nc NvrhContext) RemoteSocketOrPort() string { +func (nc *NvrhContext) RemoteSocketOrPort() string { if nc.ShouldUsePorts { - return fmt.Sprintf("0.0.0.0:%d", nc.PortNumber) + return fmt.Sprintf("127.0.0.1:%d", nc.PortNumber) } return nc.RemoteSocketPath diff --git a/src/logger/main.go b/src/logger/main.go new file mode 100644 index 0000000..882ad07 --- /dev/null +++ b/src/logger/main.go @@ -0,0 +1,27 @@ +package logger + +import ( + "log/slog" + "os" + + "github.com/dusted-go/logging/prettylog" +) + +func PrepareLogger(isDebug bool) { + logLevel := slog.LevelInfo + + if isDebug { + logLevel = slog.LevelDebug + } + + log := slog.New(prettylog.New( + &slog.HandlerOptions{ + Level: logLevel, + AddSource: isDebug, + }, + prettylog.WithDestinationWriter(os.Stderr), + prettylog.WithColor(), + )) + + slog.SetDefault(log) +} diff --git a/src/ssh_helpers/main.go b/src/ssh_helpers/main.go index 1a10fa0..ffb0f6e 100644 --- a/src/ssh_helpers/main.go +++ b/src/ssh_helpers/main.go @@ -20,7 +20,7 @@ func BuildRemoteNvimCmd(nvrhContext *context.NvrhContext) *exec.Cmd { tunnel := fmt.Sprintf("%s:%s", nvrhContext.LocalSocketPath, nvrhContext.RemoteSocketPath) if nvrhContext.ShouldUsePorts { - tunnel = fmt.Sprintf("%d:0.0.0.0:%d", nvrhContext.PortNumber, nvrhContext.PortNumber) + tunnel = fmt.Sprintf("%d:127.0.0.1:%d", nvrhContext.PortNumber, nvrhContext.PortNumber) } sshCommand := exec.Command( @@ -59,10 +59,13 @@ func BuildRemoteCommandString(nvrhContext *context.NvrhContext) string { } return fmt.Sprintf( - "%s nvim --headless --listen \"%s\" --cmd \"cd %s\"", + "%s nvim --headless --listen \"%s\" --cmd \"cd %s\"; rm -f \"%s\"; [ %t = true ] && rm -f \"%s\"", envPairsString, nvrhContext.RemoteSocketOrPort(), nvrhContext.RemoteDirectory, + nvrhContext.BrowserScriptPath, + !nvrhContext.ShouldUsePorts, + nvrhContext.RemoteSocketPath, ) } From afd02b2a9f60886fd55670f3f912ccbd4ab6b958 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Mon, 21 Oct 2024 18:50:53 -0300 Subject: [PATCH 11/31] ohno --- src/client/main.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index 2c37f6d..f612afc 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -10,13 +10,11 @@ import ( "os/exec" "os/signal" "os/user" - "path" "path/filepath" "runtime" "strings" "time" - "github.com/dusted-go/logging/prettylog" "github.com/kevinburke/ssh_config" "github.com/neovim/go-client/nvim" "github.com/skeema/knownhosts" From dc44d0e9a646ee35190b5e4858f995c48c120845 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Mon, 21 Oct 2024 19:27:52 -0300 Subject: [PATCH 12/31] flesh out SshEndpoint --- src/client/main.go | 101 ++++++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 43 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index f612afc..7ed4ef9 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -92,7 +92,7 @@ var CliClientOpenCommand = cli.Command{ // Prepare the context. sessionId := fmt.Sprintf("%d", time.Now().Unix()) - endpoint, err := parseServerString(c.Args().Get(0)) + endpoint, err := ParseSshEndpoint(c.Args().Get(0)) if err != nil { return err } @@ -357,7 +357,7 @@ func closeNvimSocket(nv *nvim.Nvim) { nv.Close() } -func getSshClientForServer(endpoint *Endpoint) (*ssh.Client, error) { +func getSshClientForServer(endpoint *SshEndpoint) (*ssh.Client, error) { kh, err := knownhosts.NewDB(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")) if err != nil { return nil, err @@ -392,13 +392,13 @@ func getSshClientForServer(endpoint *Endpoint) (*ssh.Client, error) { })) config := &ssh.ClientConfig{ - User: endpoint.User, + User: endpoint.FinalUser(), Auth: authMethods, HostKeyCallback: kh.HostKeyCallback(), - HostKeyAlgorithms: kh.HostKeyAlgorithms(fmt.Sprintf("%s:%s", endpoint.Host, endpoint.Port)), + HostKeyAlgorithms: kh.HostKeyAlgorithms(fmt.Sprintf("%s:%s", endpoint.FinalHost(), endpoint.FinalPort())), } - client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", endpoint.Host, endpoint.Port), config) + client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", endpoint.FinalHost(), endpoint.FinalPort()), config) if err != nil { slog.Error("Failed to dial", "err", err) return nil, err @@ -498,62 +498,77 @@ func cleanupSshConfigValue(value string) string { } // TODO Really needs "GivenHostName", "ResolvedHostName", etc -type Endpoint struct { - User string - Host string - Port string +type SshEndpoint struct { + GivenUser string + SshConfigUser string + FallbackUser string + + GivenHost string + SshConfigHost string + + GivenPort string + SshConfigPort string } -func (e *Endpoint) String() string { - return fmt.Sprintf("%s@%s:%s", e.User, e.Host, e.Port) +func (e *SshEndpoint) String() string { + return fmt.Sprintf("%s@%s:%s", e.FinalUser(), e.GivenHost, e.FinalPort()) } -func parseServerString(server string) (*Endpoint, error) { - currentUser, err := user.Current() +func (e *SshEndpoint) FinalUser() string { + if e.GivenUser != "" { + return e.GivenUser + } - if err != nil { - slog.Error("Error getting current user", "err", err) - return nil, err + if e.SshConfigUser != "" { + return e.SshConfigUser } - fallbackUsername := currentUser.Username - fallbackPort := "22" + return e.FallbackUser +} - parsed, err := url.Parse(fmt.Sprintf("ssh://%s", server)) - if err != nil { - return nil, err +func (e *SshEndpoint) FinalHost() string { + if e.SshConfigHost != "" { + return e.SshConfigHost } - givenHostname := parsed.Hostname() - givenUsername := parsed.User.Username() - givenPort := parsed.Port() + return e.GivenHost +} - finalUsername := givenUsername - if finalUsername == "" { - finalUsername = ssh_config.Get(givenHostname, "User") - } - if finalUsername == "" { - finalUsername = fallbackUsername +func (e *SshEndpoint) FinalPort() string { + if e.GivenPort != "" { + return e.GivenPort } - finalPort := givenPort - if finalPort == "" { - finalPort = ssh_config.Get(givenHostname, "Port") + if e.SshConfigPort != "" { + return e.SshConfigPort } - if finalPort == "" { - finalPort = fallbackPort + + return "22" +} + +func ParseSshEndpoint(server string) (*SshEndpoint, error) { + currentUser, err := user.Current() + + if err != nil { + slog.Error("Error getting current user", "err", err) + return nil, err } - finalHostname := givenHostname - configHostname := ssh_config.Get(givenHostname, "HostName") - if configHostname != "" { - finalHostname = configHostname + parsed, err := url.Parse(fmt.Sprintf("ssh://%s", server)) + if err != nil { + return nil, err } - return &Endpoint{ - User: finalUsername, - Host: finalHostname, - Port: finalPort, + return &SshEndpoint{ + GivenUser: parsed.User.Username(), + SshConfigUser: ssh_config.Get(parsed.Hostname(), "User"), + FallbackUser: currentUser.Username, + + GivenHost: parsed.Hostname(), + SshConfigHost: ssh_config.Get(parsed.Hostname(), "HostName"), + + GivenPort: parsed.Port(), + SshConfigPort: ssh_config.Get(parsed.Hostname(), "Port"), }, nil } From d53bc1c7ce1727f5e131ce91d2abced574458c02 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Mon, 21 Oct 2024 19:28:05 -0300 Subject: [PATCH 13/31] try agents first (seems to be how SSH does it) --- src/client/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index 7ed4ef9..fc6d8a5 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -370,12 +370,12 @@ func getSshClientForServer(endpoint *SshEndpoint) (*ssh.Client, error) { authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { allSigners := []ssh.Signer{} - if identitySigner, _ := getSignerForIdentityFile(endpoint.Host); identitySigner != nil { - allSigners = append(allSigners, identitySigner) + if agentSigners, _ := getSignersForIdentityAgent(endpoint.GivenHost); agentSigners != nil { + allSigners = append(allSigners, agentSigners...) } - if agentSigners, _ := getSignersForIdentityAgent(endpoint.Host); agentSigners != nil { - allSigners = append(allSigners, agentSigners...) + if identitySigner, _ := getSignerForIdentityFile(endpoint.GivenHost); identitySigner != nil { + allSigners = append(allSigners, identitySigner) } return allSigners, nil From 62e53cca33320b6c906dec8ecf8f99d271e59366 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Mon, 21 Oct 2024 19:40:04 -0300 Subject: [PATCH 14/31] extract a bunch of stuff to nvrh_ssh --- src/client/main.go | 253 +------------------------- src/nvrh_ssh/internal_ssh.go | 150 +++++++++++++++ src/{ssh_helpers => nvrh_ssh}/main.go | 2 +- src/nvrh_ssh/ssh_config.go | 24 +++ src/nvrh_ssh/ssh_endpoint.go | 84 +++++++++ 5 files changed, 268 insertions(+), 245 deletions(-) create mode 100644 src/nvrh_ssh/internal_ssh.go rename src/{ssh_helpers => nvrh_ssh}/main.go (99%) create mode 100644 src/nvrh_ssh/ssh_config.go create mode 100644 src/nvrh_ssh/ssh_endpoint.go diff --git a/src/client/main.go b/src/client/main.go index fc6d8a5..890154e 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -4,29 +4,21 @@ import ( "fmt" "log/slog" "math/rand/v2" - "net" - "net/url" "os" "os/exec" "os/signal" - "os/user" "path/filepath" "runtime" "strings" "time" - "github.com/kevinburke/ssh_config" "github.com/neovim/go-client/nvim" - "github.com/skeema/knownhosts" "github.com/urfave/cli/v2" - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" - "golang.org/x/term" "nvrh/src/context" "nvrh/src/logger" "nvrh/src/nvim_helpers" - "nvrh/src/ssh_helpers" + "nvrh/src/nvrh_ssh" ) func defaultSshPath() string { @@ -92,12 +84,12 @@ var CliClientOpenCommand = cli.Command{ // Prepare the context. sessionId := fmt.Sprintf("%d", time.Now().Unix()) - endpoint, err := ParseSshEndpoint(c.Args().Get(0)) + endpoint, err := nvrh_ssh.ParseSshEndpoint(c.Args().Get(0)) if err != nil { return err } - sshClient, err := getSshClientForServer(endpoint) + sshClient, err := nvrh_ssh.GetSshClientForServer(endpoint) if err != nil { return err } @@ -144,21 +136,21 @@ var CliClientOpenCommand = cli.Command{ // Prepare remote instance. go func() { - go ssh_helpers.TunnelSshSocket(nvrhContext, ssh_helpers.SshTunnelInfo{ + go nvrh_ssh.TunnelSshSocket(nvrhContext, nvrh_ssh.SshTunnelInfo{ Mode: "unix", LocalSocket: nvrhContext.LocalSocketPath, RemoteSocket: nvrhContext.RemoteSocketPath, }) - nvimCommandString := ssh_helpers.BuildRemoteCommandString(nvrhContext) + nvimCommandString := nvrh_ssh.BuildRemoteCommandString(nvrhContext) nvimCommandString = fmt.Sprintf("$SHELL -i -c '%s'", nvimCommandString) slog.Info("Starting remote nvim", "nvimCommandString", nvimCommandString) - if err := ssh_helpers.RunCommand(nvrhContext, nvimCommandString); err != nil { + if err := nvrh_ssh.RunCommand(nvrhContext, nvimCommandString); err != nil { doneChan <- err } - ssh_helpers.RunCommand(nvrhContext, fmt.Sprintf("rm -f '%s'", nvrhContext.RemoteSocketPath)) - ssh_helpers.RunCommand(nvrhContext, fmt.Sprintf("rm -f '%s'", nvrhContext.BrowserScriptPath)) + nvrh_ssh.RunCommand(nvrhContext, fmt.Sprintf("rm -f '%s'", nvrhContext.RemoteSocketPath)) + nvrh_ssh.RunCommand(nvrhContext, fmt.Sprintf("rm -f '%s'", nvrhContext.BrowserScriptPath)) }() // Prepare client instance. @@ -246,7 +238,7 @@ func BuildClientNvimCmd(nvrhContext *context.NvrhContext) *exec.Cmd { } func prepareRemoteNvim(nvrhContext *context.NvrhContext, nv *nvim.Nvim) error { - nv.RegisterHandler("tunnel-port", ssh_helpers.MakeRpcTunnelHandler(nvrhContext)) + nv.RegisterHandler("tunnel-port", nvrh_ssh.MakeRpcTunnelHandler(nvrhContext)) nv.RegisterHandler("open-url", RpcHandleOpenUrl) batch := nv.NewBatch() @@ -356,230 +348,3 @@ func closeNvimSocket(nv *nvim.Nvim) { } nv.Close() } - -func getSshClientForServer(endpoint *SshEndpoint) (*ssh.Client, error) { - kh, err := knownhosts.NewDB(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")) - if err != nil { - return nil, err - } - - slog.Debug("Connecting to server", "endpoint", endpoint) - - authMethods := []ssh.AuthMethod{} - - authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { - allSigners := []ssh.Signer{} - - if agentSigners, _ := getSignersForIdentityAgent(endpoint.GivenHost); agentSigners != nil { - allSigners = append(allSigners, agentSigners...) - } - - if identitySigner, _ := getSignerForIdentityFile(endpoint.GivenHost); identitySigner != nil { - allSigners = append(allSigners, identitySigner) - } - - return allSigners, nil - })) - - authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) { - password, err := askForPassword(fmt.Sprintf("Password for %s: ", endpoint)) - if err != nil { - slog.Error("Error reading password", "err", err) - return "", err - } - - return string(password), nil - })) - - config := &ssh.ClientConfig{ - User: endpoint.FinalUser(), - Auth: authMethods, - HostKeyCallback: kh.HostKeyCallback(), - HostKeyAlgorithms: kh.HostKeyAlgorithms(fmt.Sprintf("%s:%s", endpoint.FinalHost(), endpoint.FinalPort())), - } - - client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", endpoint.FinalHost(), endpoint.FinalPort()), config) - if err != nil { - slog.Error("Failed to dial", "err", err) - return nil, err - } - - return client, nil -} - -func getSignerForIdentityFile(hostname string) (ssh.Signer, error) { - identityFile := ssh_config.Get(hostname, "IdentityFile") - - if identityFile == "" { - return nil, nil - } - - identityFile = cleanupSshConfigValue(identityFile) - - if _, err := os.Stat(identityFile); os.IsNotExist(err) { - slog.Error("Identity file does not exist", "identityFile", identityFile) - return nil, err - } - - slog.Info("Using identity file", "identityFile", identityFile) - - key, err := os.ReadFile(identityFile) - if err != nil { - slog.Error("Unable to read private key", "err", err) - return nil, err - } - - signer, err := ssh.ParsePrivateKey(key) - if err != nil { - if _, ok := err.(*ssh.PassphraseMissingError); ok { - passPhrase, _ := askForPassword(fmt.Sprintf("Passphrase for %s: ", identityFile)) - - signer, signerErr := ssh.ParsePrivateKeyWithPassphrase(key, passPhrase) - if signerErr != nil { - slog.Error("Unable to parse private key", "err", signerErr) - return nil, signerErr - } - - return signer, nil - } - - slog.Error("Unable to parse private key", "err", err) - return nil, err - } - - return signer, nil -} - -func getSignersForIdentityAgent(hostname string) ([]ssh.Signer, error) { - sshAuthSock := ssh_config.Get(hostname, "IdentityAgent") - - if sshAuthSock == "" { - sshAuthSock = os.Getenv("SSH_AUTH_SOCK") - } - - if sshAuthSock == "" { - return nil, nil - } - - sshAuthSock = cleanupSshConfigValue(sshAuthSock) - - conn, err := net.Dial("unix", sshAuthSock) - if err != nil { - slog.Error("Failed to open SSH auth socket", "err", err) - return nil, err - } - - slog.Info("Using ssh agent", "socket", sshAuthSock) - agentClient := agent.NewClient(conn) - agentSigners, err := agentClient.Signers() - if err != nil { - slog.Error("Error getting signers from agent", "err", err) - return nil, err - } - - return agentSigners, nil -} - -func cleanupSshConfigValue(value string) string { - replaced := strings.Trim(value, "\"") - - userHomeDir, err := os.UserHomeDir() - if err != nil { - slog.Warn("Error getting user home dir", "err", err) - return replaced - } - - replaced = strings.ReplaceAll(replaced, "$HOME", userHomeDir) - if strings.HasPrefix(replaced, "~/") { - replaced = strings.Replace(replaced, "~", userHomeDir, 1) - } - - return replaced -} - -// TODO Really needs "GivenHostName", "ResolvedHostName", etc -type SshEndpoint struct { - GivenUser string - SshConfigUser string - FallbackUser string - - GivenHost string - SshConfigHost string - - GivenPort string - SshConfigPort string -} - -func (e *SshEndpoint) String() string { - return fmt.Sprintf("%s@%s:%s", e.FinalUser(), e.GivenHost, e.FinalPort()) -} - -func (e *SshEndpoint) FinalUser() string { - if e.GivenUser != "" { - return e.GivenUser - } - - if e.SshConfigUser != "" { - return e.SshConfigUser - } - - return e.FallbackUser -} - -func (e *SshEndpoint) FinalHost() string { - if e.SshConfigHost != "" { - return e.SshConfigHost - } - - return e.GivenHost -} - -func (e *SshEndpoint) FinalPort() string { - if e.GivenPort != "" { - return e.GivenPort - } - - if e.SshConfigPort != "" { - return e.SshConfigPort - } - - return "22" -} - -func ParseSshEndpoint(server string) (*SshEndpoint, error) { - currentUser, err := user.Current() - - if err != nil { - slog.Error("Error getting current user", "err", err) - return nil, err - } - - parsed, err := url.Parse(fmt.Sprintf("ssh://%s", server)) - if err != nil { - return nil, err - } - - return &SshEndpoint{ - GivenUser: parsed.User.Username(), - SshConfigUser: ssh_config.Get(parsed.Hostname(), "User"), - FallbackUser: currentUser.Username, - - GivenHost: parsed.Hostname(), - SshConfigHost: ssh_config.Get(parsed.Hostname(), "HostName"), - - GivenPort: parsed.Port(), - SshConfigPort: ssh_config.Get(parsed.Hostname(), "Port"), - }, nil -} - -func askForPassword(message string) ([]byte, error) { - fmt.Print(message) - password, err := term.ReadPassword(0) - fmt.Println() - - if err != nil { - return nil, err - } - - return password, nil -} diff --git a/src/nvrh_ssh/internal_ssh.go b/src/nvrh_ssh/internal_ssh.go new file mode 100644 index 0000000..1ec6a4c --- /dev/null +++ b/src/nvrh_ssh/internal_ssh.go @@ -0,0 +1,150 @@ +package nvrh_ssh + +import ( + "fmt" + "log/slog" + "net" + "os" + "path/filepath" + + "github.com/kevinburke/ssh_config" + "github.com/skeema/knownhosts" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" + "golang.org/x/term" +) + +func GetSshClientForServer(endpoint *SshEndpoint) (*ssh.Client, error) { + kh, err := knownhosts.NewDB(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")) + if err != nil { + return nil, err + } + + slog.Debug("Connecting to server", "endpoint", endpoint) + + authMethods := []ssh.AuthMethod{} + + authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { + allSigners := []ssh.Signer{} + + if agentSigners, _ := getSignersForIdentityAgent(endpoint.GivenHost); agentSigners != nil { + allSigners = append(allSigners, agentSigners...) + } + + if identitySigner, _ := getSignerForIdentityFile(endpoint.GivenHost); identitySigner != nil { + allSigners = append(allSigners, identitySigner) + } + + return allSigners, nil + })) + + authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) { + password, err := askForPassword(fmt.Sprintf("Password for %s: ", endpoint)) + if err != nil { + slog.Error("Error reading password", "err", err) + return "", err + } + + return string(password), nil + })) + + config := &ssh.ClientConfig{ + User: endpoint.FinalUser(), + Auth: authMethods, + HostKeyCallback: kh.HostKeyCallback(), + HostKeyAlgorithms: kh.HostKeyAlgorithms(fmt.Sprintf("%s:%s", endpoint.FinalHost(), endpoint.FinalPort())), + } + + client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%s", endpoint.FinalHost(), endpoint.FinalPort()), config) + if err != nil { + slog.Error("Failed to dial", "err", err) + return nil, err + } + + return client, nil +} + +func getSignerForIdentityFile(hostname string) (ssh.Signer, error) { + identityFile := ssh_config.Get(hostname, "IdentityFile") + + if identityFile == "" { + return nil, nil + } + + identityFile = CleanupSshConfigValue(identityFile) + + if _, err := os.Stat(identityFile); os.IsNotExist(err) { + slog.Error("Identity file does not exist", "identityFile", identityFile) + return nil, err + } + + slog.Info("Using identity file", "identityFile", identityFile) + + key, err := os.ReadFile(identityFile) + if err != nil { + slog.Error("Unable to read private key", "err", err) + return nil, err + } + + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + if _, ok := err.(*ssh.PassphraseMissingError); ok { + passPhrase, _ := askForPassword(fmt.Sprintf("Passphrase for %s: ", identityFile)) + + signer, signerErr := ssh.ParsePrivateKeyWithPassphrase(key, passPhrase) + if signerErr != nil { + slog.Error("Unable to parse private key", "err", signerErr) + return nil, signerErr + } + + return signer, nil + } + + slog.Error("Unable to parse private key", "err", err) + return nil, err + } + + return signer, nil +} + +func getSignersForIdentityAgent(hostname string) ([]ssh.Signer, error) { + sshAuthSock := ssh_config.Get(hostname, "IdentityAgent") + + if sshAuthSock == "" { + sshAuthSock = os.Getenv("SSH_AUTH_SOCK") + } + + if sshAuthSock == "" { + return nil, nil + } + + sshAuthSock = CleanupSshConfigValue(sshAuthSock) + + conn, err := net.Dial("unix", sshAuthSock) + if err != nil { + slog.Error("Failed to open SSH auth socket", "err", err) + return nil, err + } + + slog.Info("Using ssh agent", "socket", sshAuthSock) + agentClient := agent.NewClient(conn) + agentSigners, err := agentClient.Signers() + if err != nil { + slog.Error("Error getting signers from agent", "err", err) + return nil, err + } + + return agentSigners, nil +} + +func askForPassword(message string) ([]byte, error) { + fmt.Print(message) + password, err := term.ReadPassword(0) + fmt.Println() + + if err != nil { + return nil, err + } + + return password, nil +} diff --git a/src/ssh_helpers/main.go b/src/nvrh_ssh/main.go similarity index 99% rename from src/ssh_helpers/main.go rename to src/nvrh_ssh/main.go index ffb0f6e..7a064bc 100644 --- a/src/ssh_helpers/main.go +++ b/src/nvrh_ssh/main.go @@ -1,4 +1,4 @@ -package ssh_helpers +package nvrh_ssh import ( "fmt" diff --git a/src/nvrh_ssh/ssh_config.go b/src/nvrh_ssh/ssh_config.go new file mode 100644 index 0000000..9e49fb7 --- /dev/null +++ b/src/nvrh_ssh/ssh_config.go @@ -0,0 +1,24 @@ +package nvrh_ssh + +import ( + "log/slog" + "os" + "strings" +) + +func CleanupSshConfigValue(value string) string { + replaced := strings.Trim(value, "\"") + + userHomeDir, err := os.UserHomeDir() + if err != nil { + slog.Warn("Error getting user home dir", "err", err) + return replaced + } + + replaced = strings.ReplaceAll(replaced, "$HOME", userHomeDir) + if strings.HasPrefix(replaced, "~/") { + replaced = strings.Replace(replaced, "~", userHomeDir, 1) + } + + return replaced +} diff --git a/src/nvrh_ssh/ssh_endpoint.go b/src/nvrh_ssh/ssh_endpoint.go new file mode 100644 index 0000000..e927bb8 --- /dev/null +++ b/src/nvrh_ssh/ssh_endpoint.go @@ -0,0 +1,84 @@ +package nvrh_ssh + +import ( + "fmt" + "log/slog" + "net/url" + "os/user" + + "github.com/kevinburke/ssh_config" +) + +type SshEndpoint struct { + GivenUser string + SshConfigUser string + FallbackUser string + + GivenHost string + SshConfigHost string + + GivenPort string + SshConfigPort string +} + +func (e *SshEndpoint) String() string { + return fmt.Sprintf("%s@%s:%s", e.FinalUser(), e.GivenHost, e.FinalPort()) +} + +func (e *SshEndpoint) FinalUser() string { + if e.GivenUser != "" { + return e.GivenUser + } + + if e.SshConfigUser != "" { + return e.SshConfigUser + } + + return e.FallbackUser +} + +func (e *SshEndpoint) FinalHost() string { + if e.SshConfigHost != "" { + return e.SshConfigHost + } + + return e.GivenHost +} + +func (e *SshEndpoint) FinalPort() string { + if e.GivenPort != "" { + return e.GivenPort + } + + if e.SshConfigPort != "" { + return e.SshConfigPort + } + + return "22" +} + +func ParseSshEndpoint(server string) (*SshEndpoint, error) { + currentUser, err := user.Current() + + if err != nil { + slog.Error("Error getting current user", "err", err) + return nil, err + } + + parsed, err := url.Parse(fmt.Sprintf("ssh://%s", server)) + if err != nil { + return nil, err + } + + return &SshEndpoint{ + GivenUser: parsed.User.Username(), + SshConfigUser: ssh_config.Get(parsed.Hostname(), "User"), + FallbackUser: currentUser.Username, + + GivenHost: parsed.Hostname(), + SshConfigHost: ssh_config.Get(parsed.Hostname(), "HostName"), + + GivenPort: parsed.Port(), + SshConfigPort: ssh_config.Get(parsed.Hostname(), "Port"), + }, nil +} From 8fa983bf488342bcf1f70439587a415ccb47c6e4 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Mon, 21 Oct 2024 19:46:34 -0300 Subject: [PATCH 15/31] match SSH questions --- src/nvrh_ssh/internal_ssh.go | 4 ++-- src/nvrh_ssh/ssh_endpoint.go | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/nvrh_ssh/internal_ssh.go b/src/nvrh_ssh/internal_ssh.go index 1ec6a4c..8a62a34 100644 --- a/src/nvrh_ssh/internal_ssh.go +++ b/src/nvrh_ssh/internal_ssh.go @@ -39,7 +39,7 @@ func GetSshClientForServer(endpoint *SshEndpoint) (*ssh.Client, error) { })) authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) { - password, err := askForPassword(fmt.Sprintf("Password for %s: ", endpoint)) + password, err := askForPassword(fmt.Sprintf("%s's password: ", endpoint)) if err != nil { slog.Error("Error reading password", "err", err) return "", err @@ -89,7 +89,7 @@ func getSignerForIdentityFile(hostname string) (ssh.Signer, error) { signer, err := ssh.ParsePrivateKey(key) if err != nil { if _, ok := err.(*ssh.PassphraseMissingError); ok { - passPhrase, _ := askForPassword(fmt.Sprintf("Passphrase for %s: ", identityFile)) + passPhrase, _ := askForPassword(fmt.Sprintf("Enter passphrase for key '%s': ", identityFile)) signer, signerErr := ssh.ParsePrivateKeyWithPassphrase(key, passPhrase) if signerErr != nil { diff --git a/src/nvrh_ssh/ssh_endpoint.go b/src/nvrh_ssh/ssh_endpoint.go index e927bb8..56f3c84 100644 --- a/src/nvrh_ssh/ssh_endpoint.go +++ b/src/nvrh_ssh/ssh_endpoint.go @@ -22,7 +22,12 @@ type SshEndpoint struct { } func (e *SshEndpoint) String() string { - return fmt.Sprintf("%s@%s:%s", e.FinalUser(), e.GivenHost, e.FinalPort()) + portPart := "" + if e.FinalPort() != "22" { + portPart = fmt.Sprintf(":%s", e.FinalPort()) + } + + return fmt.Sprintf("%s@%s%s", e.FinalUser(), e.FinalHost(), portPart) } func (e *SshEndpoint) FinalUser() string { From 1d0696e61e3bd9c852a90310305f46ccfabb2d96 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 22 Oct 2024 01:07:52 -0300 Subject: [PATCH 16/31] yup that's a mess --- src/client/main.go | 90 ++++++--- src/context/main.go | 7 +- src/nvim_helpers/main.go | 16 ++ src/nvrh_base_ssh/main.go | 11 ++ src/nvrh_binary_ssh/main.go | 70 +++++++ src/nvrh_internal_ssh/main.go | 108 +++++++++++ src/nvrh_ssh/internal_ssh.go | 4 +- src/nvrh_ssh/main.go | 180 ------------------ .../ssh_endpoint.go => ssh_endpoint/main.go} | 33 +++- src/ssh_tunnel_info/main.go | 55 ++++++ 10 files changed, 361 insertions(+), 213 deletions(-) create mode 100644 src/nvrh_base_ssh/main.go create mode 100644 src/nvrh_binary_ssh/main.go create mode 100644 src/nvrh_internal_ssh/main.go delete mode 100644 src/nvrh_ssh/main.go rename src/{nvrh_ssh/ssh_endpoint.go => ssh_endpoint/main.go} (80%) create mode 100644 src/ssh_tunnel_info/main.go diff --git a/src/client/main.go b/src/client/main.go index 890154e..dccff38 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -18,7 +18,12 @@ import ( "nvrh/src/context" "nvrh/src/logger" "nvrh/src/nvim_helpers" + "nvrh/src/nvrh_base_ssh" + "nvrh/src/nvrh_binary_ssh" + "nvrh/src/nvrh_internal_ssh" "nvrh/src/nvrh_ssh" + "nvrh/src/ssh_endpoint" + "nvrh/src/ssh_tunnel_info" ) func defaultSshPath() string { @@ -82,21 +87,22 @@ var CliClientOpenCommand = cli.Command{ logger.PrepareLogger(isDebug) // Prepare the context. - sessionId := fmt.Sprintf("%d", time.Now().Unix()) - - endpoint, err := nvrh_ssh.ParseSshEndpoint(c.Args().Get(0)) - if err != nil { - return err + server := c.Args().Get(0) + if server == "" { + return fmt.Errorf(" is required") } - sshClient, err := nvrh_ssh.GetSshClientForServer(endpoint) - if err != nil { - return err + sessionId := fmt.Sprintf("%d", time.Now().Unix()) + sshPath := c.String("ssh-path") + + endpoint, endpointErr := ssh_endpoint.ParseSshEndpoint(server) + if endpointErr != nil { + return endpointErr } nvrhContext := &context.NvrhContext{ SessionId: sessionId, - Server: c.Args().Get(0), + Endpoint: endpoint, RemoteDirectory: c.Args().Get(1), RemoteEnv: c.StringSlice("server-env"), @@ -111,22 +117,32 @@ var CliClientOpenCommand = cli.Command{ SshPath: c.String("ssh-path"), Debug: isDebug, + } + + if sshPath == "internal" { + sshClient, err := nvrh_ssh.GetSshClientForEndpoint(endpoint) + if err != nil { + return err + } - SshClient: sshClient, + nvrhContext.SshClient = nvrh_base_ssh.BaseNvrhSshClient(&nvrh_internal_ssh.NvrhInternalSshClient{ + Ctx: nvrhContext, + SshClient: sshClient, + }) + } else { + nvrhContext.SshClient = nvrh_base_ssh.BaseNvrhSshClient(&nvrh_binary_ssh.NvrhBinarySshClient{ + Ctx: nvrhContext, + }) } + defer nvrhContext.SshClient.Close() + if nvrhContext.ShouldUsePorts { min := 1025 max := 65535 nvrhContext.PortNumber = rand.IntN(max-min) + min } - if nvrhContext.Server == "" { - return fmt.Errorf(" is required") - } - - defer nvrhContext.SshClient.Close() - var nv *nvim.Nvim doneChan := make(chan error) @@ -136,21 +152,32 @@ var CliClientOpenCommand = cli.Command{ // Prepare remote instance. go func() { - go nvrh_ssh.TunnelSshSocket(nvrhContext, nvrh_ssh.SshTunnelInfo{ - Mode: "unix", - LocalSocket: nvrhContext.LocalSocketPath, - RemoteSocket: nvrhContext.RemoteSocketPath, - }) - - nvimCommandString := nvrh_ssh.BuildRemoteCommandString(nvrhContext) + go func() { + tunnelInfo := &ssh_tunnel_info.SshTunnelInfo{ + Mode: "unix", + LocalSocket: nvrhContext.LocalSocketOrPort(), + RemoteSocket: nvrhContext.RemoteSocketOrPort(), + Public: false, + } + + if nvrhContext.ShouldUsePorts { + tunnelInfo.Mode = "port" + tunnelInfo.LocalSocket = fmt.Sprintf("%d", nvrhContext.PortNumber) + tunnelInfo.RemoteSocket = fmt.Sprintf("%d", nvrhContext.PortNumber) + } + + nvrhContext.SshClient.TunnelSocket(tunnelInfo) + }() + + nvimCommandString := nvim_helpers.BuildRemoteCommandString(nvrhContext) nvimCommandString = fmt.Sprintf("$SHELL -i -c '%s'", nvimCommandString) slog.Info("Starting remote nvim", "nvimCommandString", nvimCommandString) - if err := nvrh_ssh.RunCommand(nvrhContext, nvimCommandString); err != nil { + if err := nvrhContext.SshClient.Run(nvimCommandString); err != nil { doneChan <- err } - nvrh_ssh.RunCommand(nvrhContext, fmt.Sprintf("rm -f '%s'", nvrhContext.RemoteSocketPath)) - nvrh_ssh.RunCommand(nvrhContext, fmt.Sprintf("rm -f '%s'", nvrhContext.BrowserScriptPath)) + nvrhContext.SshClient.Run(fmt.Sprintf("rm -f '%s'", nvrhContext.RemoteSocketPath)) + nvrhContext.SshClient.Run(fmt.Sprintf("rm -f '%s'", nvrhContext.BrowserScriptPath)) }() // Prepare client instance. @@ -204,7 +231,7 @@ var CliClientOpenCommand = cli.Command{ } }() - err = <-doneChan + err := <-doneChan slog.Info("Closing nvrh") closeNvimSocket(nv) @@ -238,7 +265,14 @@ func BuildClientNvimCmd(nvrhContext *context.NvrhContext) *exec.Cmd { } func prepareRemoteNvim(nvrhContext *context.NvrhContext, nv *nvim.Nvim) error { - nv.RegisterHandler("tunnel-port", nvrh_ssh.MakeRpcTunnelHandler(nvrhContext)) + nv.RegisterHandler("tunnel-port", func(v *nvim.Nvim, args []string) { + go nvrhContext.SshClient.TunnelSocket(&ssh_tunnel_info.SshTunnelInfo{ + Mode: "port", + LocalSocket: fmt.Sprintf("%s", args[0]), + RemoteSocket: fmt.Sprintf("%s", args[0]), + Public: true, + }) + }) nv.RegisterHandler("open-url", RpcHandleOpenUrl) batch := nv.NewBatch() diff --git a/src/context/main.go b/src/context/main.go index 74f0093..168fcc5 100644 --- a/src/context/main.go +++ b/src/context/main.go @@ -4,12 +4,13 @@ import ( "fmt" "os/exec" - "golang.org/x/crypto/ssh" + "nvrh/src/nvrh_base_ssh" + "nvrh/src/ssh_endpoint" ) type NvrhContext struct { SessionId string - Server string + Endpoint *ssh_endpoint.SshEndpoint RemoteDirectory string LocalSocketPath string @@ -27,7 +28,7 @@ type NvrhContext struct { SshPath string Debug bool - SshClient *ssh.Client + SshClient nvrh_base_ssh.BaseNvrhSshClient } func (nc *NvrhContext) LocalSocketOrPort() string { diff --git a/src/nvim_helpers/main.go b/src/nvim_helpers/main.go index 3108d26..31dcca8 100644 --- a/src/nvim_helpers/main.go +++ b/src/nvim_helpers/main.go @@ -1,6 +1,8 @@ package nvim_helpers import ( + "fmt" + "strings" "time" "github.com/neovim/go-client/nvim" @@ -27,3 +29,17 @@ func WaitForNvim(nvrhContext *context.NvrhContext) (*nvim.Nvim, error) { // return nil, errors.New("Timed out waiting for nvim") } + +func BuildRemoteCommandString(nvrhContext *context.NvrhContext) string { + envPairsString := "" + if len(nvrhContext.RemoteEnv) > 0 { + envPairsString = strings.Join(nvrhContext.RemoteEnv, " ") + } + + return fmt.Sprintf( + "%s nvim --headless --listen \"%s\" --cmd \"cd %s\"", + envPairsString, + nvrhContext.RemoteSocketOrPort(), + nvrhContext.RemoteDirectory, + ) +} diff --git a/src/nvrh_base_ssh/main.go b/src/nvrh_base_ssh/main.go new file mode 100644 index 0000000..c349910 --- /dev/null +++ b/src/nvrh_base_ssh/main.go @@ -0,0 +1,11 @@ +package nvrh_base_ssh + +import ( + "nvrh/src/ssh_tunnel_info" +) + +type BaseNvrhSshClient interface { + Run(command string) error + TunnelSocket(tunnelInfo *ssh_tunnel_info.SshTunnelInfo) + Close() error +} diff --git a/src/nvrh_binary_ssh/main.go b/src/nvrh_binary_ssh/main.go new file mode 100644 index 0000000..b033415 --- /dev/null +++ b/src/nvrh_binary_ssh/main.go @@ -0,0 +1,70 @@ +package nvrh_binary_ssh + +import ( + "log/slog" + "os" + "os/exec" + + "nvrh/src/context" + "nvrh/src/ssh_tunnel_info" +) + +type NvrhBinarySshClient struct { + Ctx *context.NvrhContext +} + +func (c *NvrhBinarySshClient) Close() error { + return nil +} + +func (c *NvrhBinarySshClient) Run(command string) error { + sshCommand := exec.Command( + c.Ctx.SshPath, + "-t", + c.Ctx.Endpoint.Given, + command, + ) + + c.Ctx.CommandsToKill = append(c.Ctx.CommandsToKill, sshCommand) + if c.Ctx.Debug { + sshCommand.Stdout = os.Stdout + sshCommand.Stderr = os.Stderr + } + + if err := sshCommand.Start(); err != nil { + return err + } + + if err := sshCommand.Wait(); err != nil { + return err + } + + return nil +} + +func (c *NvrhBinarySshClient) TunnelSocket(tunnelInfo *ssh_tunnel_info.SshTunnelInfo) { + sshCommand := exec.Command( + c.Ctx.SshPath, + "-NL", + tunnelInfo.BoundToIp(), + c.Ctx.Endpoint.Given, + ) + + slog.Info("Tunneling SSH socket", "tunnelInfo", tunnelInfo) + + c.Ctx.CommandsToKill = append(c.Ctx.CommandsToKill, sshCommand) + if c.Ctx.Debug { + sshCommand.Stdout = os.Stdout + sshCommand.Stderr = os.Stderr + } + + if err := sshCommand.Start(); err != nil { + return + } + + defer sshCommand.Process.Kill() + + if err := sshCommand.Wait(); err != nil { + return + } +} diff --git a/src/nvrh_internal_ssh/main.go b/src/nvrh_internal_ssh/main.go new file mode 100644 index 0000000..e66f8ff --- /dev/null +++ b/src/nvrh_internal_ssh/main.go @@ -0,0 +1,108 @@ +package nvrh_internal_ssh + +import ( + "fmt" + "io" + "log/slog" + "net" + "os" + + "golang.org/x/crypto/ssh" + + "nvrh/src/context" + "nvrh/src/ssh_tunnel_info" +) + +type NvrhInternalSshClient struct { + Ctx *context.NvrhContext + SshClient *ssh.Client +} + +func (c *NvrhInternalSshClient) Close() error { + if c.SshClient == nil { + return fmt.Errorf("ssh client not initialized") + } + + return c.SshClient.Close() +} + +func (c *NvrhInternalSshClient) Run(command string) error { + if c.SshClient == nil { + return fmt.Errorf("ssh client not initialized") + } + + session, err := c.SshClient.NewSession() + + if err != nil { + return err + } + + defer session.Close() + + if c.Ctx.Debug { + session.Stdout = os.Stdout + session.Stderr = os.Stderr + } + + if err := session.Run(command); err != nil { + return err + } + + return nil +} + +func (c *NvrhInternalSshClient) TunnelSocket(tunnelInfo *ssh_tunnel_info.SshTunnelInfo) { + if c.SshClient == nil { + return + } + + // Listen on the local Unix socket + localListener, err := tunnelInfo.LocalListener(tunnelInfo.Public) + if err != nil { + slog.Error("Failed to listen on local socket", "err", err) + return + } + + defer localListener.Close() + + // Clean up local socket file + defer func() { + if tunnelInfo.Mode == "unix" { + os.Remove(tunnelInfo.LocalSocket) + } + }() + + slog.Info("Tunneling SSH socket", "tunnelInfo", tunnelInfo) + + for { + // Accept incoming connections + localConn, err := localListener.Accept() + if err != nil { + slog.Error("Failed to accept connection", "err", err) + continue + } + + // Establish a connection to the remote socket via SSH + remoteConn, err := tunnelInfo.RemoteListener(c.SshClient) + if err != nil { + slog.Error("Failed to dial remote socket", "err", err) + localConn.Close() + continue + } + + // Start a goroutine to handle the connection + go handleConnection(localConn, remoteConn) + } + +} + +func handleConnection(localConn net.Conn, remoteConn net.Conn) { + // Close connections when done + defer localConn.Close() + defer remoteConn.Close() + + // Copy data from local to remote + go io.Copy(remoteConn, localConn) + // Copy data from remote to local + io.Copy(localConn, remoteConn) +} diff --git a/src/nvrh_ssh/internal_ssh.go b/src/nvrh_ssh/internal_ssh.go index 8a62a34..f8ca764 100644 --- a/src/nvrh_ssh/internal_ssh.go +++ b/src/nvrh_ssh/internal_ssh.go @@ -12,9 +12,11 @@ import ( "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" "golang.org/x/term" + + "nvrh/src/ssh_endpoint" ) -func GetSshClientForServer(endpoint *SshEndpoint) (*ssh.Client, error) { +func GetSshClientForEndpoint(endpoint *ssh_endpoint.SshEndpoint) (*ssh.Client, error) { kh, err := knownhosts.NewDB(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")) if err != nil { return nil, err diff --git a/src/nvrh_ssh/main.go b/src/nvrh_ssh/main.go deleted file mode 100644 index 7a064bc..0000000 --- a/src/nvrh_ssh/main.go +++ /dev/null @@ -1,180 +0,0 @@ -package nvrh_ssh - -import ( - "fmt" - "io" - "log/slog" - "net" - "nvrh/src/context" - "os" - "os/exec" - "strings" - - "github.com/neovim/go-client/nvim" - "golang.org/x/crypto/ssh" -) - -func BuildRemoteNvimCmd(nvrhContext *context.NvrhContext) *exec.Cmd { - nvimCommandString := BuildRemoteCommandString(nvrhContext) - slog.Info("Starting remote nvim", "nvimCommandString", nvimCommandString) - - tunnel := fmt.Sprintf("%s:%s", nvrhContext.LocalSocketPath, nvrhContext.RemoteSocketPath) - if nvrhContext.ShouldUsePorts { - tunnel = fmt.Sprintf("%d:127.0.0.1:%d", nvrhContext.PortNumber, nvrhContext.PortNumber) - } - - sshCommand := exec.Command( - nvrhContext.SshPath, - "-L", - tunnel, - "-t", - nvrhContext.Server, - // TODO Not really sure if this is better than piping it as exampled - // below. - fmt.Sprintf("$SHELL -i -c '%s'", nvimCommandString), - ) - - // Create a pipe to write to the command's stdin - // stdinPipe, err := sshCommand.StdinPipe() - // if err != nil { - // fmt.Fprintf(os.Stderr, "Error creating stdin pipe: %v\n", err) - // return - // } - // Write the predetermined string to the pipe - // command := buildRemoteCommand(socketPath, directory) - // if _, err := stdinPipe.Write([]byte(command)); err != nil { - // fmt.Fprintf(os.Stderr, "Error writing to stdin pipe: %v\n", err) - // return - // } - // Close the pipe after writing - // stdinPipe.Close() - - return sshCommand -} - -func BuildRemoteCommandString(nvrhContext *context.NvrhContext) string { - envPairsString := "" - if len(nvrhContext.RemoteEnv) > 0 { - envPairsString = strings.Join(nvrhContext.RemoteEnv, " ") - } - - return fmt.Sprintf( - "%s nvim --headless --listen \"%s\" --cmd \"cd %s\"; rm -f \"%s\"; [ %t = true ] && rm -f \"%s\"", - envPairsString, - nvrhContext.RemoteSocketOrPort(), - nvrhContext.RemoteDirectory, - nvrhContext.BrowserScriptPath, - !nvrhContext.ShouldUsePorts, - nvrhContext.RemoteSocketPath, - ) -} - -func MakeRpcTunnelHandler(nvrhContext *context.NvrhContext) func(*nvim.Nvim, []string) { - return func(v *nvim.Nvim, args []string) { - go TunnelSshSocket(nvrhContext, SshTunnelInfo{ - Mode: "port", - LocalSocket: fmt.Sprintf("%s", args[0]), - RemoteSocket: fmt.Sprintf("%s", args[0]), - }) - } -} - -type SshTunnelInfo struct { - Mode string - LocalSocket string - RemoteSocket string -} - -func (ti SshTunnelInfo) LocalListener() (net.Listener, error) { - switch ti.Mode { - case "unix": - return net.Listen("unix", ti.LocalSocket) - case "port": - return net.Listen("tcp", fmt.Sprintf("localhost:%s", ti.LocalSocket)) - } - - return nil, fmt.Errorf("Invalid mode: %s", ti.Mode) -} - -func (ti SshTunnelInfo) RemoteListener(sshClient *ssh.Client) (net.Conn, error) { - switch ti.Mode { - case "unix": - return sshClient.Dial("unix", ti.RemoteSocket) - case "port": - return sshClient.Dial("tcp", fmt.Sprintf("localhost:%s", ti.RemoteSocket)) - } - - return nil, fmt.Errorf("Invalid mode: %s", ti.Mode) -} - -func TunnelSshSocket(nvrhContext *context.NvrhContext, tunnelInfo SshTunnelInfo) { - // Listen on the local Unix socket - localListener, err := tunnelInfo.LocalListener() - if err != nil { - slog.Error("Failed to listen on local socket", "err", err) - return - } - - defer localListener.Close() - - defer func() { - // Clean up local socket file - os.Remove(tunnelInfo.LocalSocket) - }() - - slog.Info("Tunneling SSH socket", "LocalSocke", tunnelInfo.LocalSocket, "RemoteSocket", tunnelInfo.RemoteSocket) - - for { - // Accept incoming connections - localConn, err := localListener.Accept() - if err != nil { - slog.Error("Failed to accept connection", "err", err) - continue - } - - // Establish a connection to the remote socket via SSH - remoteConn, err := tunnelInfo.RemoteListener(nvrhContext.SshClient) - if err != nil { - slog.Error("Failed to dial remote socket", "err", err) - localConn.Close() - continue - } - - // Start a goroutine to handle the connection - go handleConnection(localConn, remoteConn) - } -} - -func handleConnection(localConn net.Conn, remoteConn net.Conn) { - // Close connections when done - defer localConn.Close() - defer remoteConn.Close() - - // Copy data from local to remote - go io.Copy(remoteConn, localConn) - // Copy data from remote to local - io.Copy(localConn, remoteConn) -} - -func RunCommand(nvrhContext *context.NvrhContext, command string) error { - session, err := nvrhContext.SshClient.NewSession() - - if err != nil { - slog.Error("Failed to create session", "err", err) - return err - } - - defer session.Close() - - if nvrhContext.Debug { - session.Stdout = os.Stdout - session.Stderr = os.Stderr - } - - if err := session.Run(command); err != nil { - slog.Error("Failed to run command", "err", err) - return err - } - - return nil -} diff --git a/src/nvrh_ssh/ssh_endpoint.go b/src/ssh_endpoint/main.go similarity index 80% rename from src/nvrh_ssh/ssh_endpoint.go rename to src/ssh_endpoint/main.go index 56f3c84..3a36340 100644 --- a/src/nvrh_ssh/ssh_endpoint.go +++ b/src/ssh_endpoint/main.go @@ -1,4 +1,4 @@ -package nvrh_ssh +package ssh_endpoint import ( "fmt" @@ -10,6 +10,8 @@ import ( ) type SshEndpoint struct { + Given string + GivenUser string SshConfigUser string FallbackUser string @@ -76,6 +78,8 @@ func ParseSshEndpoint(server string) (*SshEndpoint, error) { } return &SshEndpoint{ + Given: server, + GivenUser: parsed.User.Username(), SshConfigUser: ssh_config.Get(parsed.Hostname(), "User"), FallbackUser: currentUser.Username, @@ -87,3 +91,30 @@ func ParseSshEndpoint(server string) (*SshEndpoint, error) { SshConfigPort: ssh_config.Get(parsed.Hostname(), "Port"), }, nil } + +// type Wut struct { +// } + +// func (w *Wut) DoIt() { +// fmt.Println("Doing it!") +// } + +// type Huh struct { +// } + +// func (w *Huh) DoIt() { +// fmt.Println("Doing it!") +// } + +// type Luh interface { +// DoIt() +// } + +// func luh(w Luh) { +// w.DoIt() +// } + +// func giver() { +// luh(Luh(&Wut{})) +// luh(Luh(&Huh{})) +// } diff --git a/src/ssh_tunnel_info/main.go b/src/ssh_tunnel_info/main.go new file mode 100644 index 0000000..f8483ba --- /dev/null +++ b/src/ssh_tunnel_info/main.go @@ -0,0 +1,55 @@ +package ssh_tunnel_info + +import ( + "fmt" + "net" + + "golang.org/x/crypto/ssh" +) + +type SshTunnelInfo struct { + Mode string + LocalSocket string + RemoteSocket string + Public bool +} + +func (ti *SshTunnelInfo) LocalListener(public bool) (net.Listener, error) { + switch ti.Mode { + case "unix": + return net.Listen("unix", ti.LocalSocket) + case "port": + ip := "localhost" + if public { + ip = "0.0.0.0" + } + + return net.Listen("tcp", fmt.Sprintf("%s:%s", ip, ti.LocalSocket)) + } + + return nil, fmt.Errorf("Invalid mode: %s", ti.Mode) +} + +func (ti *SshTunnelInfo) RemoteListener(sshClient *ssh.Client) (net.Conn, error) { + switch ti.Mode { + case "unix": + return sshClient.Dial("unix", ti.RemoteSocket) + case "port": + return sshClient.Dial("tcp", fmt.Sprintf("localhost:%s", ti.RemoteSocket)) + } + + return nil, fmt.Errorf("Invalid mode: %s", ti.Mode) +} + +func (ti *SshTunnelInfo) BoundToIp() string { + if ti.Mode == "unix" { + return fmt.Sprintf("%s:%s", ti.LocalSocket, ti.RemoteSocket) + } + + ip := "localhost" + if ti.Public { + ip = "0.0.0.0" + } + + return fmt.Sprintf("%s:%s:%s", ti.LocalSocket, ip, ti.RemoteSocket) +} From 74814c1b468de6876fb1a40d5798b351f7775cf1 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 22 Oct 2024 18:45:49 -0300 Subject: [PATCH 17/31] more mess --- src/client/main.go | 28 ++++++++++++---------------- src/nvrh_base_ssh/main.go | 2 +- src/nvrh_binary_ssh/main.go | 15 +++++++++++---- src/nvrh_internal_ssh/main.go | 6 +++++- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index dccff38..9726d78 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -152,22 +152,18 @@ var CliClientOpenCommand = cli.Command{ // Prepare remote instance. go func() { - go func() { - tunnelInfo := &ssh_tunnel_info.SshTunnelInfo{ - Mode: "unix", - LocalSocket: nvrhContext.LocalSocketOrPort(), - RemoteSocket: nvrhContext.RemoteSocketOrPort(), - Public: false, - } - - if nvrhContext.ShouldUsePorts { - tunnelInfo.Mode = "port" - tunnelInfo.LocalSocket = fmt.Sprintf("%d", nvrhContext.PortNumber) - tunnelInfo.RemoteSocket = fmt.Sprintf("%d", nvrhContext.PortNumber) - } - - nvrhContext.SshClient.TunnelSocket(tunnelInfo) - }() + tunnelInfo := &ssh_tunnel_info.SshTunnelInfo{ + Mode: "unix", + LocalSocket: nvrhContext.LocalSocketPath, + RemoteSocket: nvrhContext.RemoteSocketPath, + Public: false, + } + + if nvrhContext.ShouldUsePorts { + tunnelInfo.Mode = "port" + tunnelInfo.LocalSocket = fmt.Sprintf("%d", nvrhContext.PortNumber) + tunnelInfo.RemoteSocket = fmt.Sprintf("%d", nvrhContext.PortNumber) + } nvimCommandString := nvim_helpers.BuildRemoteCommandString(nvrhContext) nvimCommandString = fmt.Sprintf("$SHELL -i -c '%s'", nvimCommandString) diff --git a/src/nvrh_base_ssh/main.go b/src/nvrh_base_ssh/main.go index c349910..2df64fa 100644 --- a/src/nvrh_base_ssh/main.go +++ b/src/nvrh_base_ssh/main.go @@ -5,7 +5,7 @@ import ( ) type BaseNvrhSshClient interface { - Run(command string) error + Run(command string, tunnelInfo *ssh_tunnel_info.SshTunnelInfo) error TunnelSocket(tunnelInfo *ssh_tunnel_info.SshTunnelInfo) Close() error } diff --git a/src/nvrh_binary_ssh/main.go b/src/nvrh_binary_ssh/main.go index b033415..45fac29 100644 --- a/src/nvrh_binary_ssh/main.go +++ b/src/nvrh_binary_ssh/main.go @@ -1,6 +1,7 @@ package nvrh_binary_ssh import ( + "fmt" "log/slog" "os" "os/exec" @@ -17,12 +18,18 @@ func (c *NvrhBinarySshClient) Close() error { return nil } -func (c *NvrhBinarySshClient) Run(command string) error { +func (c *NvrhBinarySshClient) Run(command string, tunnelInfo *ssh_tunnel_info.SshTunnelInfo) error { + args := []string{} + + if tunnelInfo != nil { + args = append(args, "-L", tunnelInfo.BoundToIp()) + } + + args = append(args, "-t", c.Ctx.Endpoint.Given, command) + sshCommand := exec.Command( c.Ctx.SshPath, - "-t", - c.Ctx.Endpoint.Given, - command, + args..., ) c.Ctx.CommandsToKill = append(c.Ctx.CommandsToKill, sshCommand) diff --git a/src/nvrh_internal_ssh/main.go b/src/nvrh_internal_ssh/main.go index e66f8ff..80e0ef6 100644 --- a/src/nvrh_internal_ssh/main.go +++ b/src/nvrh_internal_ssh/main.go @@ -26,11 +26,15 @@ func (c *NvrhInternalSshClient) Close() error { return c.SshClient.Close() } -func (c *NvrhInternalSshClient) Run(command string) error { +func (c *NvrhInternalSshClient) Run(command string, tunnelInfo *ssh_tunnel_info.SshTunnelInfo) error { if c.SshClient == nil { return fmt.Errorf("ssh client not initialized") } + if tunnelInfo != nil { + go c.TunnelSocket(tunnelInfo) + } + session, err := c.SshClient.NewSession() if err != nil { From f4c2c8b03ba5a88063e77183d4623d551208e0e1 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 22 Oct 2024 18:57:56 -0300 Subject: [PATCH 18/31] more mess --- src/client/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index 9726d78..6fb480f 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -168,12 +168,12 @@ var CliClientOpenCommand = cli.Command{ nvimCommandString := nvim_helpers.BuildRemoteCommandString(nvrhContext) nvimCommandString = fmt.Sprintf("$SHELL -i -c '%s'", nvimCommandString) slog.Info("Starting remote nvim", "nvimCommandString", nvimCommandString) - if err := nvrhContext.SshClient.Run(nvimCommandString); err != nil { + if err := nvrhContext.SshClient.Run(nvimCommandString, tunnelInfo); err != nil { doneChan <- err } - nvrhContext.SshClient.Run(fmt.Sprintf("rm -f '%s'", nvrhContext.RemoteSocketPath)) - nvrhContext.SshClient.Run(fmt.Sprintf("rm -f '%s'", nvrhContext.BrowserScriptPath)) + nvrhContext.SshClient.Run(fmt.Sprintf("rm -f '%s'", nvrhContext.RemoteSocketPath), nil) + nvrhContext.SshClient.Run(fmt.Sprintf("rm -f '%s'", nvrhContext.BrowserScriptPath), nil) }() // Prepare client instance. From 7b48e91cc8ddbfa4bf37da8c744ba4591690e56e Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 22 Oct 2024 19:26:02 -0300 Subject: [PATCH 19/31] lint --- src/nvrh_binary_ssh/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nvrh_binary_ssh/main.go b/src/nvrh_binary_ssh/main.go index 45fac29..c89ab0a 100644 --- a/src/nvrh_binary_ssh/main.go +++ b/src/nvrh_binary_ssh/main.go @@ -1,7 +1,6 @@ package nvrh_binary_ssh import ( - "fmt" "log/slog" "os" "os/exec" From 04a828d1bab8dc8498d8337187144ffb20e6f93d Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 22 Oct 2024 19:27:36 -0300 Subject: [PATCH 20/31] cleanup --- src/ssh_endpoint/main.go | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/ssh_endpoint/main.go b/src/ssh_endpoint/main.go index 3a36340..01e28ac 100644 --- a/src/ssh_endpoint/main.go +++ b/src/ssh_endpoint/main.go @@ -91,30 +91,3 @@ func ParseSshEndpoint(server string) (*SshEndpoint, error) { SshConfigPort: ssh_config.Get(parsed.Hostname(), "Port"), }, nil } - -// type Wut struct { -// } - -// func (w *Wut) DoIt() { -// fmt.Println("Doing it!") -// } - -// type Huh struct { -// } - -// func (w *Huh) DoIt() { -// fmt.Println("Doing it!") -// } - -// type Luh interface { -// DoIt() -// } - -// func luh(w Luh) { -// w.DoIt() -// } - -// func giver() { -// luh(Luh(&Wut{})) -// luh(Luh(&Huh{})) -// } From 12493b8ef2f72e6296b0ffa9ab40484f2d64ceb8 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 22 Oct 2024 20:50:03 -0300 Subject: [PATCH 21/31] more changes --- src/client/main.go | 6 ++-- .../internal_ssh.go => go_ssh_ext/main.go} | 2 +- src/{nvrh_ssh => go_ssh_ext}/ssh_config.go | 2 +- src/nvrh_internal_ssh/main.go | 31 +++++++++++++++++-- src/ssh_tunnel_info/main.go | 30 ------------------ 5 files changed, 34 insertions(+), 37 deletions(-) rename src/{nvrh_ssh/internal_ssh.go => go_ssh_ext/main.go} (99%) rename src/{nvrh_ssh => go_ssh_ext}/ssh_config.go (95%) diff --git a/src/client/main.go b/src/client/main.go index 6fb480f..9d2dde4 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -16,12 +16,12 @@ import ( "github.com/urfave/cli/v2" "nvrh/src/context" + "nvrh/src/go_ssh_ext" "nvrh/src/logger" "nvrh/src/nvim_helpers" "nvrh/src/nvrh_base_ssh" "nvrh/src/nvrh_binary_ssh" "nvrh/src/nvrh_internal_ssh" - "nvrh/src/nvrh_ssh" "nvrh/src/ssh_endpoint" "nvrh/src/ssh_tunnel_info" ) @@ -115,12 +115,12 @@ var CliClientOpenCommand = cli.Command{ BrowserScriptPath: fmt.Sprintf("/tmp/nvrh-browser-%s", sessionId), - SshPath: c.String("ssh-path"), + SshPath: sshPath, Debug: isDebug, } if sshPath == "internal" { - sshClient, err := nvrh_ssh.GetSshClientForEndpoint(endpoint) + sshClient, err := go_ssh_ext.GetSshClientForEndpoint(endpoint) if err != nil { return err } diff --git a/src/nvrh_ssh/internal_ssh.go b/src/go_ssh_ext/main.go similarity index 99% rename from src/nvrh_ssh/internal_ssh.go rename to src/go_ssh_ext/main.go index f8ca764..e056f30 100644 --- a/src/nvrh_ssh/internal_ssh.go +++ b/src/go_ssh_ext/main.go @@ -1,4 +1,4 @@ -package nvrh_ssh +package go_ssh_ext import ( "fmt" diff --git a/src/nvrh_ssh/ssh_config.go b/src/go_ssh_ext/ssh_config.go similarity index 95% rename from src/nvrh_ssh/ssh_config.go rename to src/go_ssh_ext/ssh_config.go index 9e49fb7..a094b6f 100644 --- a/src/nvrh_ssh/ssh_config.go +++ b/src/go_ssh_ext/ssh_config.go @@ -1,4 +1,4 @@ -package nvrh_ssh +package go_ssh_ext import ( "log/slog" diff --git a/src/nvrh_internal_ssh/main.go b/src/nvrh_internal_ssh/main.go index 80e0ef6..f08d784 100644 --- a/src/nvrh_internal_ssh/main.go +++ b/src/nvrh_internal_ssh/main.go @@ -61,7 +61,7 @@ func (c *NvrhInternalSshClient) TunnelSocket(tunnelInfo *ssh_tunnel_info.SshTunn } // Listen on the local Unix socket - localListener, err := tunnelInfo.LocalListener(tunnelInfo.Public) + localListener, err := LocalListenerFromTunnelInfo(tunnelInfo) if err != nil { slog.Error("Failed to listen on local socket", "err", err) return @@ -87,7 +87,7 @@ func (c *NvrhInternalSshClient) TunnelSocket(tunnelInfo *ssh_tunnel_info.SshTunn } // Establish a connection to the remote socket via SSH - remoteConn, err := tunnelInfo.RemoteListener(c.SshClient) + remoteConn, err := RemoteListenerFromTunnelInfo(tunnelInfo, c.SshClient) if err != nil { slog.Error("Failed to dial remote socket", "err", err) localConn.Close() @@ -110,3 +110,30 @@ func handleConnection(localConn net.Conn, remoteConn net.Conn) { // Copy data from remote to local io.Copy(localConn, remoteConn) } + +func LocalListenerFromTunnelInfo(ti *ssh_tunnel_info.SshTunnelInfo) (net.Listener, error) { + switch ti.Mode { + case "unix": + return net.Listen("unix", ti.LocalSocket) + case "port": + ip := "localhost" + if ti.Public { + ip = "0.0.0.0" + } + + return net.Listen("tcp", fmt.Sprintf("%s:%s", ip, ti.LocalSocket)) + } + + return nil, fmt.Errorf("Invalid mode: %s", ti.Mode) +} + +func RemoteListenerFromTunnelInfo(ti *ssh_tunnel_info.SshTunnelInfo, sshClient *ssh.Client) (net.Conn, error) { + switch ti.Mode { + case "unix": + return sshClient.Dial("unix", ti.RemoteSocket) + case "port": + return sshClient.Dial("tcp", fmt.Sprintf("localhost:%s", ti.RemoteSocket)) + } + + return nil, fmt.Errorf("Invalid mode: %s", ti.Mode) +} diff --git a/src/ssh_tunnel_info/main.go b/src/ssh_tunnel_info/main.go index f8483ba..1d58195 100644 --- a/src/ssh_tunnel_info/main.go +++ b/src/ssh_tunnel_info/main.go @@ -2,9 +2,6 @@ package ssh_tunnel_info import ( "fmt" - "net" - - "golang.org/x/crypto/ssh" ) type SshTunnelInfo struct { @@ -14,33 +11,6 @@ type SshTunnelInfo struct { Public bool } -func (ti *SshTunnelInfo) LocalListener(public bool) (net.Listener, error) { - switch ti.Mode { - case "unix": - return net.Listen("unix", ti.LocalSocket) - case "port": - ip := "localhost" - if public { - ip = "0.0.0.0" - } - - return net.Listen("tcp", fmt.Sprintf("%s:%s", ip, ti.LocalSocket)) - } - - return nil, fmt.Errorf("Invalid mode: %s", ti.Mode) -} - -func (ti *SshTunnelInfo) RemoteListener(sshClient *ssh.Client) (net.Conn, error) { - switch ti.Mode { - case "unix": - return sshClient.Dial("unix", ti.RemoteSocket) - case "port": - return sshClient.Dial("tcp", fmt.Sprintf("localhost:%s", ti.RemoteSocket)) - } - - return nil, fmt.Errorf("Invalid mode: %s", ti.Mode) -} - func (ti *SshTunnelInfo) BoundToIp() string { if ti.Mode == "unix" { return fmt.Sprintf("%s:%s", ti.LocalSocket, ti.RemoteSocket) From 25d5a6d63b79792a944ad9956ef161e3cc1b7671 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 22 Oct 2024 21:59:44 -0300 Subject: [PATCH 22/31] allow `--ssh-path binary` --- src/client/main.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index 9d2dde4..32d851b 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -52,9 +52,9 @@ var CliClientOpenCommand = cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "ssh-path", - Usage: "Path to SSH binary. Defaults to ssh on Unix, C:\\Windows\\System32\\OpenSSH\\ssh.exe on Windows", + Usage: "Path to SSH binary. 'binary' will use the default system SSH binary. 'internal' will use the internal SSH client. Anything else will be used as the path to the SSH binary", EnvVars: []string{"NVRH_CLIENT_SSH_PATH"}, - Value: defaultSshPath(), + Value: "binary", }, &cli.BoolFlag{ @@ -94,6 +94,9 @@ var CliClientOpenCommand = cli.Command{ sessionId := fmt.Sprintf("%d", time.Now().Unix()) sshPath := c.String("ssh-path") + if sshPath == "binary" { + sshPath = defaultSshPath() + } endpoint, endpointErr := ssh_endpoint.ParseSshEndpoint(server) if endpointErr != nil { @@ -119,7 +122,7 @@ var CliClientOpenCommand = cli.Command{ Debug: isDebug, } - if sshPath == "internal" { + if nvrhContext.SshPath == "internal" { sshClient, err := go_ssh_ext.GetSshClientForEndpoint(endpoint) if err != nil { return err From 7ff7fff6a2720a23b12784a7c2cee0f5594d17f9 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 22 Oct 2024 22:02:57 -0300 Subject: [PATCH 23/31] prefer localhost --- src/context/main.go | 4 ++-- src/nvrh_binary_ssh/main.go | 2 ++ src/nvrh_internal_ssh/main.go | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/context/main.go b/src/context/main.go index 168fcc5..9738e4e 100644 --- a/src/context/main.go +++ b/src/context/main.go @@ -36,7 +36,7 @@ func (nc *NvrhContext) LocalSocketOrPort() string { // nvim-qt, at least on Windows (and might have something to do with // running in a VM) seems to prefer `127.0.0.1` to `0.0.0.0`, and I think // that's safe on other OSes. - return fmt.Sprintf("127.0.0.1:%d", nc.PortNumber) + return fmt.Sprintf("localhost:%d", nc.PortNumber) } return nc.LocalSocketPath @@ -44,7 +44,7 @@ func (nc *NvrhContext) LocalSocketOrPort() string { func (nc *NvrhContext) RemoteSocketOrPort() string { if nc.ShouldUsePorts { - return fmt.Sprintf("127.0.0.1:%d", nc.PortNumber) + return fmt.Sprintf("localhost:%d", nc.PortNumber) } return nc.RemoteSocketPath diff --git a/src/nvrh_binary_ssh/main.go b/src/nvrh_binary_ssh/main.go index c89ab0a..2302da3 100644 --- a/src/nvrh_binary_ssh/main.go +++ b/src/nvrh_binary_ssh/main.go @@ -26,6 +26,8 @@ func (c *NvrhBinarySshClient) Run(command string, tunnelInfo *ssh_tunnel_info.Ss args = append(args, "-t", c.Ctx.Endpoint.Given, command) + slog.Debug("Running command via SSH", "command", command) + sshCommand := exec.Command( c.Ctx.SshPath, args..., diff --git a/src/nvrh_internal_ssh/main.go b/src/nvrh_internal_ssh/main.go index f08d784..2ec3d2e 100644 --- a/src/nvrh_internal_ssh/main.go +++ b/src/nvrh_internal_ssh/main.go @@ -31,6 +31,8 @@ func (c *NvrhInternalSshClient) Run(command string, tunnelInfo *ssh_tunnel_info. return fmt.Errorf("ssh client not initialized") } + slog.Debug("Running command via SSH", "command", command) + if tunnelInfo != nil { go c.TunnelSocket(tunnelInfo) } From 4478a7a6e46e0fb657ae08c44e0711b04708899c Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Tue, 22 Oct 2024 22:03:03 -0300 Subject: [PATCH 24/31] cleanup --- src/client/main.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client/main.go b/src/client/main.go index 32d851b..57c316c 100644 --- a/src/client/main.go +++ b/src/client/main.go @@ -138,8 +138,6 @@ var CliClientOpenCommand = cli.Command{ }) } - defer nvrhContext.SshClient.Close() - if nvrhContext.ShouldUsePorts { min := 1025 max := 65535 @@ -171,12 +169,8 @@ var CliClientOpenCommand = cli.Command{ nvimCommandString := nvim_helpers.BuildRemoteCommandString(nvrhContext) nvimCommandString = fmt.Sprintf("$SHELL -i -c '%s'", nvimCommandString) slog.Info("Starting remote nvim", "nvimCommandString", nvimCommandString) - if err := nvrhContext.SshClient.Run(nvimCommandString, tunnelInfo); err != nil { - doneChan <- err - } - nvrhContext.SshClient.Run(fmt.Sprintf("rm -f '%s'", nvrhContext.RemoteSocketPath), nil) - nvrhContext.SshClient.Run(fmt.Sprintf("rm -f '%s'", nvrhContext.BrowserScriptPath), nil) + nvrhContext.SshClient.Run(nvimCommandString, tunnelInfo) }() // Prepare client instance. @@ -236,6 +230,12 @@ var CliClientOpenCommand = cli.Command{ closeNvimSocket(nv) killAllCmds(nvrhContext.CommandsToKill) os.Remove(nvrhContext.LocalSocketPath) + if nvrhContext.SshClient != nil { + nvrhContext.SshClient.Run(fmt.Sprintf("rm -f '%s'", nvrhContext.RemoteSocketPath), nil) + nvrhContext.SshClient.Run(fmt.Sprintf("rm -f '%s'", nvrhContext.BrowserScriptPath), nil) + + nvrhContext.SshClient.Close() + } if err != nil { return err From 7a7257716296543b95c12bca23080f814935c4bb Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Thu, 24 Oct 2024 00:59:38 -0300 Subject: [PATCH 25/31] manage known_hosts --- src/go_ssh_ext/main.go | 57 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/go_ssh_ext/main.go b/src/go_ssh_ext/main.go index e056f30..a8846d3 100644 --- a/src/go_ssh_ext/main.go +++ b/src/go_ssh_ext/main.go @@ -17,11 +17,64 @@ import ( ) func GetSshClientForEndpoint(endpoint *ssh_endpoint.SshEndpoint) (*ssh.Client, error) { - kh, err := knownhosts.NewDB(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")) + knownhostsPath := filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts") + + if _, err := os.Stat(knownhostsPath); os.IsNotExist(err) { + f, ferr := os.Create(knownhostsPath) + os.OpenFile(knownhostsPath, os.O_CREATE|os.O_RDONLY, 0600) + if ferr != nil { + return nil, ferr + } + + f.Close() + } + + kh, err := knownhosts.NewDB(knownhostsPath) + if err != nil { return nil, err } + hostKeyCallback := ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { + slog.Debug("Checking host key", "hostname", hostname, "remote", remote, "key", key) + err := kh.HostKeyCallback()(hostname, remote, key) + + if knownhosts.IsHostKeyChanged(err) { + return fmt.Errorf("REMOTE HOST IDENTIFICATION HAS CHANGED for host %s! This may indicate a MitM attack.", hostname) + } + + if knownhosts.IsHostUnknown(err) { + fmt.Print(fmt.Sprintf(`The authenticity of host '%s (%s)' can't be established. +%s key fingerprint is %s. +Are you sure you want to continue connecting (yes/no)? `, + hostname, remote, key.Type(), ssh.FingerprintSHA256(key), + )) + var response string + fmt.Scanf("%v", &response) + + if response != "yes" { + return fmt.Errorf("Host key verification failed.") + } + + f, ferr := os.OpenFile(knownhostsPath, os.O_APPEND|os.O_WRONLY, 0600) + if ferr == nil { + defer f.Close() + ferr = knownhosts.WriteKnownHost(f, hostname, remote, key) + } + if ferr == nil { + slog.Info("Added host to known_hosts\n", "hostname", hostname) + } else { + slog.Error("Failed to add host to known_hosts\n", "hostname", hostname, "ferr", ferr) + return ferr + } + + // permit previously-unknown hosts (warning: may be insecure) + return nil + } + + return err + }) + slog.Debug("Connecting to server", "endpoint", endpoint) authMethods := []ssh.AuthMethod{} @@ -53,7 +106,7 @@ func GetSshClientForEndpoint(endpoint *ssh_endpoint.SshEndpoint) (*ssh.Client, e config := &ssh.ClientConfig{ User: endpoint.FinalUser(), Auth: authMethods, - HostKeyCallback: kh.HostKeyCallback(), + HostKeyCallback: hostKeyCallback, HostKeyAlgorithms: kh.HostKeyAlgorithms(fmt.Sprintf("%s:%s", endpoint.FinalHost(), endpoint.FinalPort())), } From 591576dde1697373f4acab518f09059a7427c7b0 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Sat, 9 Nov 2024 16:33:23 -0400 Subject: [PATCH 26/31] fix askForPassword on Windows --- src/go_ssh_ext/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/go_ssh_ext/main.go b/src/go_ssh_ext/main.go index a8846d3..c5919c7 100644 --- a/src/go_ssh_ext/main.go +++ b/src/go_ssh_ext/main.go @@ -6,6 +6,7 @@ import ( "net" "os" "path/filepath" + "syscall" "github.com/kevinburke/ssh_config" "github.com/skeema/knownhosts" @@ -194,7 +195,7 @@ func getSignersForIdentityAgent(hostname string) ([]ssh.Signer, error) { func askForPassword(message string) ([]byte, error) { fmt.Print(message) - password, err := term.ReadPassword(0) + password, err := term.ReadPassword(int(syscall.Stdin)) fmt.Println() if err != nil { From 370cec3baf1442ceeef535a850c1c0fda5666b2c Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Sat, 9 Nov 2024 16:33:52 -0400 Subject: [PATCH 27/31] default ssh agent pipe on windows --- src/go_ssh_ext/main.go | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/go_ssh_ext/main.go b/src/go_ssh_ext/main.go index c5919c7..6b08ab4 100644 --- a/src/go_ssh_ext/main.go +++ b/src/go_ssh_ext/main.go @@ -6,8 +6,10 @@ import ( "net" "os" "path/filepath" + "runtime" "syscall" + "github.com/Microsoft/go-winio" "github.com/kevinburke/ssh_config" "github.com/skeema/knownhosts" "golang.org/x/crypto/ssh" @@ -170,16 +172,33 @@ func getSignersForIdentityAgent(hostname string) ([]ssh.Signer, error) { sshAuthSock = os.Getenv("SSH_AUTH_SOCK") } + if runtime.GOOS == "windows" && sshAuthSock == "" { + sshAuthSock = `\\.\pipe\openssh-ssh-agent` + } + if sshAuthSock == "" { return nil, nil } sshAuthSock = CleanupSshConfigValue(sshAuthSock) - conn, err := net.Dial("unix", sshAuthSock) - if err != nil { - slog.Error("Failed to open SSH auth socket", "err", err) - return nil, err + var conn net.Conn + if runtime.GOOS == "windows" { + //inner_conn, err := namedpipe.DialContext(context.Background(), sshAuthSock) + // inner_conn, err := npipe.Dial(sshAuthSock) + inner_conn, err := winio.DialPipe(sshAuthSock, nil) + if err != nil { + slog.Error("Failed to open SSH auth socket", "err", err) + return nil, err + } + conn = inner_conn + } else { + inner_conn, err := net.Dial("unix", sshAuthSock) + if err != nil { + slog.Error("Failed to open SSH auth socket", "err", err) + return nil, err + } + conn = inner_conn } slog.Info("Using ssh agent", "socket", sshAuthSock) From 220c23194bd181b13e2d8cf68d4e1af0f270634f Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Sat, 9 Nov 2024 16:34:02 -0400 Subject: [PATCH 28/31] fix default user name on windows --- src/ssh_endpoint/main.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ssh_endpoint/main.go b/src/ssh_endpoint/main.go index 01e28ac..dbb1518 100644 --- a/src/ssh_endpoint/main.go +++ b/src/ssh_endpoint/main.go @@ -5,6 +5,8 @@ import ( "log/slog" "net/url" "os/user" + "runtime" + "strings" "github.com/kevinburke/ssh_config" ) @@ -77,12 +79,18 @@ func ParseSshEndpoint(server string) (*SshEndpoint, error) { return nil, err } + fallbackUser := currentUser.Username + // If on Windows, get the user name without the domain portion. + if runtime.GOOS == "windows" { + fallbackUser = fallbackUser[strings.LastIndex(fallbackUser, `\`)+1:] + } + return &SshEndpoint{ Given: server, GivenUser: parsed.User.Username(), SshConfigUser: ssh_config.Get(parsed.Hostname(), "User"), - FallbackUser: currentUser.Username, + FallbackUser: fallbackUser, GivenHost: parsed.Hostname(), SshConfigHost: ssh_config.Get(parsed.Hostname(), "HostName"), From 4b6830411742815019ecd0a0435a2778a7eddbb7 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Sat, 9 Nov 2024 16:36:24 -0400 Subject: [PATCH 29/31] go mod tidy --- go.mod | 18 +++++++++++------- go.sum | 8 ++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index afd3aa2..339a967 100644 --- a/go.mod +++ b/go.mod @@ -2,16 +2,20 @@ module nvrh go 1.23.1 +require ( + github.com/Microsoft/go-winio v0.6.2 + github.com/dusted-go/logging v1.3.0 + github.com/kevinburke/ssh_config v1.2.0 + github.com/neovim/go-client v1.2.2-0.20240514170004-863141a115a5 + github.com/skeema/knownhosts v1.3.0 + github.com/urfave/cli/v2 v2.27.4 + golang.org/x/crypto v0.28.0 + golang.org/x/term v0.25.0 +) + require ( github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect - github.com/dusted-go/logging v1.3.0 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/neovim/go-client v1.2.2-0.20240514170004-863141a115a5 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/skeema/knownhosts v1.3.0 // indirect - github.com/urfave/cli/v2 v2.27.4 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - golang.org/x/crypto v0.28.0 // indirect golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.25.0 // indirect ) diff --git a/go.sum b/go.sum index c5f1332..479fd61 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/dusted-go/logging v1.3.0 h1:SL/EH1Rp27oJQIte+LjWvWACSnYDTqNx5gZULin0XRY= github.com/dusted-go/logging v1.3.0/go.mod h1:s58+s64zE5fxSWWZfp+b8ZV0CHyKHjamITGyuY1wzGg= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/neovim/go-client v1.2.1 h1:kl3PgYgbnBfvaIoGYi3ojyXH0ouY6dJY/rYUCssZKqI= -github.com/neovim/go-client v1.2.1/go.mod h1:EeqCP3z1vJd70JTaH/KXz9RMZ/nIgEFveX83hYnh/7c= github.com/neovim/go-client v1.2.2-0.20240514170004-863141a115a5 h1:bDKPFxHFy0ApEmtUFFQzbxMGgywlKrpyNJ2opMX4hjc= github.com/neovim/go-client v1.2.2-0.20240514170004-863141a115a5/go.mod h1:UBsOERb5epbeQT0nyPTZkmUPTffRYBcHvrXXidr1NQQ= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -16,12 +16,8 @@ github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= From 7cfd20f4871e09e4e5af20b2a82408b164d04f14 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Sat, 9 Nov 2024 23:56:17 -0400 Subject: [PATCH 30/31] ugly refactor for golang platform builds --- src/go_ssh_ext/conn_unix.go | 12 ++++++++++++ src/go_ssh_ext/conn_windows.go | 14 ++++++++++++++ src/go_ssh_ext/main.go | 22 ++++------------------ 3 files changed, 30 insertions(+), 18 deletions(-) create mode 100644 src/go_ssh_ext/conn_unix.go create mode 100644 src/go_ssh_ext/conn_windows.go diff --git a/src/go_ssh_ext/conn_unix.go b/src/go_ssh_ext/conn_unix.go new file mode 100644 index 0000000..09aa364 --- /dev/null +++ b/src/go_ssh_ext/conn_unix.go @@ -0,0 +1,12 @@ +//go:build !windows +// +build !windows + +package go_ssh_ext + +import ( + "net" +) + +func getConnectionForAgent(sshAuthSock string) (net.Conn, error) { + return net.Dial("unix", sshAuthSock) +} diff --git a/src/go_ssh_ext/conn_windows.go b/src/go_ssh_ext/conn_windows.go new file mode 100644 index 0000000..4359d77 --- /dev/null +++ b/src/go_ssh_ext/conn_windows.go @@ -0,0 +1,14 @@ +//go:build windows +// +build windows + +package go_ssh_ext + +import ( + "net" + + "github.com/Microsoft/go-winio" +) + +func getConnectionForAgent(sshAuthSock string) (net.Conn, error) { + return winio.DialPipe(sshAuthSock, nil) +} diff --git a/src/go_ssh_ext/main.go b/src/go_ssh_ext/main.go index 6b08ab4..8cb7143 100644 --- a/src/go_ssh_ext/main.go +++ b/src/go_ssh_ext/main.go @@ -9,7 +9,6 @@ import ( "runtime" "syscall" - "github.com/Microsoft/go-winio" "github.com/kevinburke/ssh_config" "github.com/skeema/knownhosts" "golang.org/x/crypto/ssh" @@ -182,23 +181,10 @@ func getSignersForIdentityAgent(hostname string) ([]ssh.Signer, error) { sshAuthSock = CleanupSshConfigValue(sshAuthSock) - var conn net.Conn - if runtime.GOOS == "windows" { - //inner_conn, err := namedpipe.DialContext(context.Background(), sshAuthSock) - // inner_conn, err := npipe.Dial(sshAuthSock) - inner_conn, err := winio.DialPipe(sshAuthSock, nil) - if err != nil { - slog.Error("Failed to open SSH auth socket", "err", err) - return nil, err - } - conn = inner_conn - } else { - inner_conn, err := net.Dial("unix", sshAuthSock) - if err != nil { - slog.Error("Failed to open SSH auth socket", "err", err) - return nil, err - } - conn = inner_conn + conn, err := getConnectionForAgent(sshAuthSock) + if err != nil { + slog.Error("Failed to open SSH auth socket", "err", err) + return nil, err } slog.Info("Using ssh agent", "socket", sshAuthSock) From f6d09fe4dc28adeb65f2ab7a26571d1d906b0033 Mon Sep 17 00:00:00 2001 From: Mike Wyatt Date: Sun, 10 Nov 2024 14:40:52 -0400 Subject: [PATCH 31/31] try and trim down binary size --- script/build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/build b/script/build index 4fe21f9..7124b2a 100755 --- a/script/build +++ b/script/build @@ -15,7 +15,7 @@ cp manifest.json src/ for os in "${OSES[@]}"; do for arch in "${ARCHS[@]}"; do - GOOS="$os" GOARCH="$arch" go build -o "dist/$(basename "$PWD")-$os-$arch" ./src/main.go + GOOS="$os" GOARCH="$arch" go build -ldflags "-s -w" -o "dist/$(basename "$PWD")-$os-$arch" ./src/main.go done done