From ff049771df28169049f5ad90fc18eab6505c5d47 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Mon, 27 May 2019 16:13:52 +1000 Subject: [PATCH 01/25] add x86_64 check in downloadScript --- sshcode.go | 1 + 1 file changed, 1 insertion(+) diff --git a/sshcode.go b/sshcode.go index a31e3d8..e185d15 100644 --- a/sshcode.go +++ b/sshcode.go @@ -345,6 +345,7 @@ func downloadScript(codeServerPath string) string { return fmt.Sprintf( `set -euxo pipefail || exit 1 +[ "$(uname -m)" != "x86_64" ] && echo "Unsupported server architecture $(uname -m). code-server only has releases for x86_64 systems." && exit 1 pkill -f %v || true mkdir -p ~/.local/share/code-server %v cd %v From 4195d9966553a1b98d794e82624d589ee19f102d Mon Sep 17 00:00:00 2001 From: Christos KK Loverdos Date: Sun, 2 Jun 2019 21:08:42 +0300 Subject: [PATCH 02/25] Use bash in a more portable way --- sshcode.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sshcode.go b/sshcode.go index a31e3d8..97e7502 100644 --- a/sshcode.go +++ b/sshcode.go @@ -56,7 +56,7 @@ func sshCode(host, dir string, o options) error { dlScript := downloadScript(codeServerPath) // Downloads the latest code-server and allows it to be executed. - sshCmdStr := fmt.Sprintf("ssh %v %v /bin/bash", o.sshFlags, host) + sshCmdStr := fmt.Sprintf("ssh %v %v '/usr/bin/env bash'", o.sshFlags, host) sshCmd := exec.Command("sh", "-c", sshCmdStr) sshCmd.Stdout = os.Stdout From e646d57b0a8c8aba960a7d54c5972971aa5faa6f Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 12 Jun 2019 16:22:52 +1000 Subject: [PATCH 03/25] add SSH master connection feature By default, sshcode will now start a master connection with no command so that users only need to authenticate once and multiple connections don't need to be established. This speeds up load times significantly as there are less handshakes required. To disable this behaviour you can use `--no-reuse-connection`. --- main.go | 23 +++++++----- sshcode.go | 108 ++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 104 insertions(+), 27 deletions(-) diff --git a/main.go b/main.go index bc0de4a..ddd8eb3 100644 --- a/main.go +++ b/main.go @@ -34,11 +34,12 @@ var _ interface { } = new(rootCmd) type rootCmd struct { - skipSync bool - syncBack bool - printVersion bool - bindAddr string - sshFlags string + skipSync bool + syncBack bool + printVersion bool + noReuseConnection bool + bindAddr string + sshFlags string } func (c *rootCmd) Spec() cli.CommandSpec { @@ -53,6 +54,7 @@ func (c *rootCmd) RegisterFlags(fl *flag.FlagSet) { fl.BoolVar(&c.skipSync, "skipsync", false, "skip syncing local settings and extensions to remote host") fl.BoolVar(&c.syncBack, "b", false, "sync extensions back on termination") fl.BoolVar(&c.printVersion, "version", false, "print version information and exit") + fl.BoolVar(&c.noReuseConnection, "no-reuse-connection", false, "do not reuse SSH connection via control socket") fl.StringVar(&c.bindAddr, "bind", "", "local bind address for ssh tunnel") fl.StringVar(&c.sshFlags, "ssh-flags", "", "custom SSH flags") } @@ -76,10 +78,11 @@ func (c *rootCmd) Run(fl *flag.FlagSet) { } err := sshCode(host, dir, options{ - skipSync: c.skipSync, - sshFlags: c.sshFlags, - bindAddr: c.bindAddr, - syncBack: c.syncBack, + skipSync: c.skipSync, + sshFlags: c.sshFlags, + bindAddr: c.bindAddr, + syncBack: c.syncBack, + noReuseConnection: c.noReuseConnection, }) if err != nil { @@ -101,7 +104,7 @@ Environment variables: More info: https://github.com/cdr/sshcode Arguments: -%vHOST is passed into the ssh command. Valid formats are '' or 'gcp:'. +%vHOST is passed into the ssh command. Valid formats are '' or 'gcp:'. %vDIR is optional.`, helpTab, vsCodeConfigDirEnv, helpTab, vsCodeExtensionsDirEnv, diff --git a/sshcode.go b/sshcode.go index 97e7502..859651a 100644 --- a/sshcode.go +++ b/sshcode.go @@ -20,14 +20,16 @@ import ( ) const codeServerPath = "~/.cache/sshcode/sshcode-server" +const sshControlPath = "~/.ssh/control-%h-%p-%r" type options struct { - skipSync bool - syncBack bool - noOpen bool - bindAddr string - remotePort string - sshFlags string + skipSync bool + syncBack bool + noOpen bool + noReuseConnection bool + bindAddr string + remotePort string + sshFlags string } func sshCode(host, dir string, o options) error { @@ -53,6 +55,41 @@ func sshCode(host, dir string, o options) error { return xerrors.Errorf("failed to find available remote port: %w", err) } + // Start SSH master connection socket. This prevents multiple password prompts from appearing as authentication + // only happens on the initial connection. + var sshMasterCmd *exec.Cmd + if !o.noReuseConnection { + newSSHFlags := fmt.Sprintf(`%v -o "ControlPath=%v"`, o.sshFlags, sshControlPath) + + // -MN means "start a master socket and don't open a session, just connect". + sshMasterCmdStr := fmt.Sprintf(`ssh %v -MN %v`, newSSHFlags, host) + sshMasterCmd = exec.Command("sh", "-c", sshMasterCmdStr) + sshMasterCmd.Stdin = os.Stdin + sshMasterCmd.Stdout = os.Stdout + sshMasterCmd.Stderr = os.Stderr + err = sshMasterCmd.Start() + if err != nil { + flog.Error("failed to start SSH master connection, disabling connection reuse feature: %v", err) + o.noReuseConnection = true + } else { + // Wait for master to be ready. + err = checkSSHMaster(newSSHFlags, host) + if err != nil { + flog.Error("SSH master failed to start in time, disabling connection reuse feature: %v", err) + o.noReuseConnection = true + if sshMasterCmd.Process != nil { + err = sshMasterCmd.Process.Kill() + if err != nil { + flog.Error("failed to kill SSH master connection, ignoring: %v", err) + } + } + } else { + sshMasterCmd.Stdin = nil + o.sshFlags = newSSHFlags + } + } + } + dlScript := downloadScript(codeServerPath) // Downloads the latest code-server and allows it to be executed. @@ -146,22 +183,39 @@ func sshCode(host, dir string, o options) error { case <-ctx.Done(): case <-c: } + flog.Info("exiting") - if !o.syncBack || o.skipSync { - flog.Info("shutting down") - return nil - } + if o.syncBack && !o.skipSync { + flog.Info("synchronizing VS Code back to local") - flog.Info("synchronizing VS Code back to local") + err = syncExtensions(o.sshFlags, host, true) + if err != nil { + flog.Error("failed to sync extensions back: %v", err) + } - err = syncExtensions(o.sshFlags, host, true) - if err != nil { - return xerrors.Errorf("failed to sync extensions back: %w", err) + err = syncUserSettings(o.sshFlags, host, true) + if err != nil { + flog.Error("failed to sync user settings settings back: %v", err) + } } - err = syncUserSettings(o.sshFlags, host, true) - if err != nil { - return xerrors.Errorf("failed to sync user settings settings back: %w", err) + // Kill the master connection if we made one. + if !o.noReuseConnection { + // Try using the -O exit syntax first before killing the master. + sshCmdStr = fmt.Sprintf(`ssh %v -O exit %v`, o.sshFlags, host) + sshCmd = exec.Command("sh", "-c", sshCmdStr) + sshCmd.Stdout = os.Stdout + sshCmd.Stderr = os.Stderr + err = sshCmd.Run() + if err != nil { + flog.Error("failed to gracefully stop SSH master connection, killing: %v", err) + if sshMasterCmd.Process != nil { + err = sshMasterCmd.Process.Kill() + if err != nil { + flog.Error("failed to kill SSH master connection, ignoring: %v", err) + } + } + } } return nil @@ -263,6 +317,26 @@ func randomPort() (string, error) { return "", xerrors.Errorf("max number of tries exceeded: %d", maxTries) } +// checkSSHMaster polls every second for 30 seconds to check if the SSH master +// is ready. +func checkSSHMaster(sshFlags string, host string) (err error) { + maxTries := 30 + check := func() error { + sshCmdStr := fmt.Sprintf(`ssh %v -O check %v`, sshFlags, host) + sshCmd := exec.Command("sh", "-c", sshCmdStr) + return sshCmd.Run() + } + + for i := 0; i < maxTries; i++ { + err = check() + if err == nil { + return nil + } + time.Sleep(time.Second) + } + return err +} + func syncUserSettings(sshFlags string, host string, back bool) error { localConfDir, err := configDir() if err != nil { From 3141f7f4bb93be3a3614e21b2b5510916c81dbdd Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 13 Jun 2019 18:53:51 +1000 Subject: [PATCH 04/25] move SSH master process tidyup to a deferred func - Replace the `ssh -O exit` tidyup command with just a SIGTERM on the master - Add `exec` to the front of the SSH master cmd so it replaces the `sh` process (so we can send SIGTERM to it easier) --- sshcode.go | 66 ++++++++++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/sshcode.go b/sshcode.go index 859651a..51a432e 100644 --- a/sshcode.go +++ b/sshcode.go @@ -11,6 +11,7 @@ import ( "os/signal" "path/filepath" "strconv" + "syscall" "strings" "time" @@ -57,32 +58,36 @@ func sshCode(host, dir string, o options) error { // Start SSH master connection socket. This prevents multiple password prompts from appearing as authentication // only happens on the initial connection. - var sshMasterCmd *exec.Cmd if !o.noReuseConnection { newSSHFlags := fmt.Sprintf(`%v -o "ControlPath=%v"`, o.sshFlags, sshControlPath) // -MN means "start a master socket and don't open a session, just connect". - sshMasterCmdStr := fmt.Sprintf(`ssh %v -MN %v`, newSSHFlags, host) - sshMasterCmd = exec.Command("sh", "-c", sshMasterCmdStr) + sshCmdStr := fmt.Sprintf(`exec ssh %v -MN %v`, newSSHFlags, host) + sshMasterCmd := exec.Command("sh", "-c", sshCmdStr) sshMasterCmd.Stdin = os.Stdin sshMasterCmd.Stdout = os.Stdout sshMasterCmd.Stderr = os.Stderr + stopSSHMaster := func () { + if sshMasterCmd.Process != nil { + err := sshMasterCmd.Process.Signal(syscall.SIGTERM) + if err != nil { + flog.Error("failed to send SIGTERM to SSH master process: %v", err) + } + } + } + defer stopSSHMaster() + err = sshMasterCmd.Start() if err != nil { flog.Error("failed to start SSH master connection, disabling connection reuse feature: %v", err) o.noReuseConnection = true + stopSSHMaster() } else { - // Wait for master to be ready. err = checkSSHMaster(newSSHFlags, host) if err != nil { - flog.Error("SSH master failed to start in time, disabling connection reuse feature: %v", err) + flog.Error("SSH master failed to be ready in time, disabling connection reuse feature: %v", err) o.noReuseConnection = true - if sshMasterCmd.Process != nil { - err = sshMasterCmd.Process.Kill() - if err != nil { - flog.Error("failed to kill SSH master connection, ignoring: %v", err) - } - } + stopSSHMaster() } else { sshMasterCmd.Stdin = nil o.sshFlags = newSSHFlags @@ -183,39 +188,22 @@ func sshCode(host, dir string, o options) error { case <-ctx.Done(): case <-c: } - flog.Info("exiting") - if o.syncBack && !o.skipSync { - flog.Info("synchronizing VS Code back to local") + flog.Info("shutting down") + if !o.syncBack || o.skipSync { + return nil + } - err = syncExtensions(o.sshFlags, host, true) - if err != nil { - flog.Error("failed to sync extensions back: %v", err) - } + flog.Info("synchronizing VS Code back to local") - err = syncUserSettings(o.sshFlags, host, true) - if err != nil { - flog.Error("failed to sync user settings settings back: %v", err) - } + err = syncExtensions(o.sshFlags, host, true) + if err != nil { + return xerrors.Errorf("failed to sync extensions back: %v", err) } - // Kill the master connection if we made one. - if !o.noReuseConnection { - // Try using the -O exit syntax first before killing the master. - sshCmdStr = fmt.Sprintf(`ssh %v -O exit %v`, o.sshFlags, host) - sshCmd = exec.Command("sh", "-c", sshCmdStr) - sshCmd.Stdout = os.Stdout - sshCmd.Stderr = os.Stderr - err = sshCmd.Run() - if err != nil { - flog.Error("failed to gracefully stop SSH master connection, killing: %v", err) - if sshMasterCmd.Process != nil { - err = sshMasterCmd.Process.Kill() - if err != nil { - flog.Error("failed to kill SSH master connection, ignoring: %v", err) - } - } - } + err = syncUserSettings(o.sshFlags, host, true) + if err != nil { + return xerrors.Errorf("failed to sync user settings settings back: %v", err) } return nil From 4d64fc0393636307296b2bd5db7577ecb5ea28a4 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 13 Jun 2019 19:11:31 +1000 Subject: [PATCH 05/25] add "process is running" check to checkSSHMaster() Checks if the master process is running by sending signal 0 to it. According to kill(2), sending a signal of 0 will send no signal but will still perform error checking. To prevent the SSH master from becoming a zombie process, a wait call was added in a goroutine. --- sshcode.go | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/sshcode.go b/sshcode.go index 51a432e..41f6e23 100644 --- a/sshcode.go +++ b/sshcode.go @@ -11,8 +11,8 @@ import ( "os/signal" "path/filepath" "strconv" - "syscall" "strings" + "syscall" "time" "github.com/pkg/browser" @@ -67,9 +67,13 @@ func sshCode(host, dir string, o options) error { sshMasterCmd.Stdin = os.Stdin sshMasterCmd.Stdout = os.Stdout sshMasterCmd.Stderr = os.Stderr - stopSSHMaster := func () { + stopSSHMaster := func() { if sshMasterCmd.Process != nil { - err := sshMasterCmd.Process.Signal(syscall.SIGTERM) + err := sshMasterCmd.Process.Signal(syscall.Signal(0)) + if err != nil { + return + } + err = sshMasterCmd.Process.Signal(syscall.SIGTERM) if err != nil { flog.Error("failed to send SIGTERM to SSH master process: %v", err) } @@ -78,12 +82,13 @@ func sshCode(host, dir string, o options) error { defer stopSSHMaster() err = sshMasterCmd.Start() + go sshMasterCmd.Wait() if err != nil { flog.Error("failed to start SSH master connection, disabling connection reuse feature: %v", err) o.noReuseConnection = true stopSSHMaster() } else { - err = checkSSHMaster(newSSHFlags, host) + err = checkSSHMaster(sshMasterCmd, newSSHFlags, host) if err != nil { flog.Error("SSH master failed to be ready in time, disabling connection reuse feature: %v", err) o.noReuseConnection = true @@ -307,22 +312,32 @@ func randomPort() (string, error) { // checkSSHMaster polls every second for 30 seconds to check if the SSH master // is ready. -func checkSSHMaster(sshFlags string, host string) (err error) { - maxTries := 30 - check := func() error { +func checkSSHMaster(sshMasterCmd *exec.Cmd, sshFlags string, host string) error { + var ( + maxTries = 30 + sleepDur = time.Second + err error + ) + for i := 0; i < maxTries; i++ { + // Check if the master is running + if sshMasterCmd.Process == nil { + return xerrors.Errorf("SSH master process not running") + } + err = sshMasterCmd.Process.Signal(syscall.Signal(0)) + if err != nil { + return xerrors.Errorf("failed to check if SSH master process was alive: %v", err) + } + + // Check if it's ready sshCmdStr := fmt.Sprintf(`ssh %v -O check %v`, sshFlags, host) sshCmd := exec.Command("sh", "-c", sshCmdStr) - return sshCmd.Run() - } - - for i := 0; i < maxTries; i++ { - err = check() + err = sshCmd.Run() if err == nil { return nil } - time.Sleep(time.Second) + time.Sleep(sleepDur) } - return err + return xerrors.Errorf("max number of tries exceeded: %d", maxTries) } func syncUserSettings(sshFlags string, host string, back bool) error { From eee34f58bd4cac785dd6ef58eb8e1a4d80975a4a Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Sun, 16 Jun 2019 03:12:09 +1000 Subject: [PATCH 06/25] add ~/.ssh directory sanity check before starting Checks: - if it exists - if it's a directory (if not warn and disable reuse connection feature) - if it has safe permissions (not writable by anyone except the owner, if not then warn) --- sshcode.go | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/sshcode.go b/sshcode.go index 41f6e23..92b7f3c 100644 --- a/sshcode.go +++ b/sshcode.go @@ -21,7 +21,9 @@ import ( ) const codeServerPath = "~/.cache/sshcode/sshcode-server" -const sshControlPath = "~/.ssh/control-%h-%p-%r" +const sshDirectory = "~/.ssh" +const sshDirectoryUnsafeModeMask = 0022 +const sshControlPath = sshDirectory + "/control-%h-%p-%r" type options struct { skipSync bool @@ -34,8 +36,6 @@ type options struct { } func sshCode(host, dir string, o options) error { - flog.Info("ensuring code-server is updated...") - host, extraSSHFlags, err := parseHost(host) if err != nil { return xerrors.Errorf("failed to parse host IP: %w", err) @@ -56,6 +56,28 @@ func sshCode(host, dir string, o options) error { return xerrors.Errorf("failed to find available remote port: %w", err) } + // Check the SSH directory's permissions and warn the user if it is not safe. + sshDirectoryMode, err := os.Lstat(expandPath(sshDirectory)) + if err != nil { + if !o.noReuseConnection { + flog.Info("failed to stat %v directory, disabling connection reuse feature: %v", sshDirectory, err) + o.noReuseConnection = true + } + } else { + if !sshDirectoryMode.IsDir() { + if !o.noReuseConnection { + flog.Info("%v is not a directory, disabling connection reuse feature", sshDirectory) + o.noReuseConnection = true + } else { + flog.Info("warning: %v is not a directory", sshDirectory) + } + } + if sshDirectoryMode.Mode().Perm()&sshDirectoryUnsafeModeMask != 0 { + flog.Info("warning: the %v directory has unsafe permissions, they should only be writable by "+ + "the owner (and files inside should be set to 0600)", sshDirectory) + } + } + // Start SSH master connection socket. This prevents multiple password prompts from appearing as authentication // only happens on the initial connection. if !o.noReuseConnection { @@ -100,6 +122,7 @@ func sshCode(host, dir string, o options) error { } } + flog.Info("ensuring code-server is updated...") dlScript := downloadScript(codeServerPath) // Downloads the latest code-server and allows it to be executed. @@ -214,6 +237,23 @@ func sshCode(host, dir string, o options) error { return nil } +// expandPath returns an expanded version of path. +func expandPath(path string) string { + path = filepath.Clean(os.ExpandEnv(path)) + + // Replace tilde notation in path with the home directory. + homedir := os.Getenv("HOME") + if homedir != "" { + if path == "~" { + path = homedir + } else if strings.HasPrefix(path, "~/") { + path = filepath.Join(homedir, path[2:]) + } + } + + return filepath.Clean(path) +} + func parseBindAddr(bindAddr string) (string, error) { if bindAddr == "" { bindAddr = ":" From c8ca9fc372b78484544bc96c787b0f484cf7c749 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Sun, 16 Jun 2019 20:24:46 +1000 Subject: [PATCH 07/25] update --bind flag help text Clarify how to specify --bind without a host by providing the syntax for it. --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index bc0de4a..51433aa 100644 --- a/main.go +++ b/main.go @@ -53,7 +53,7 @@ func (c *rootCmd) RegisterFlags(fl *flag.FlagSet) { fl.BoolVar(&c.skipSync, "skipsync", false, "skip syncing local settings and extensions to remote host") fl.BoolVar(&c.syncBack, "b", false, "sync extensions back on termination") fl.BoolVar(&c.printVersion, "version", false, "print version information and exit") - fl.StringVar(&c.bindAddr, "bind", "", "local bind address for ssh tunnel") + fl.StringVar(&c.bindAddr, "bind", "", `local bind address for ssh tunnel, in [HOST]:PORT syntax (default: 127.0.0.1)`) fl.StringVar(&c.sshFlags, "ssh-flags", "", "custom SSH flags") } From 65655b960ed1f04aa241f17de45965e8506f3df1 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Sun, 16 Jun 2019 20:28:41 +1000 Subject: [PATCH 08/25] undo changing quotes to backticks --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 51433aa..0d16036 100644 --- a/main.go +++ b/main.go @@ -53,7 +53,7 @@ func (c *rootCmd) RegisterFlags(fl *flag.FlagSet) { fl.BoolVar(&c.skipSync, "skipsync", false, "skip syncing local settings and extensions to remote host") fl.BoolVar(&c.syncBack, "b", false, "sync extensions back on termination") fl.BoolVar(&c.printVersion, "version", false, "print version information and exit") - fl.StringVar(&c.bindAddr, "bind", "", `local bind address for ssh tunnel, in [HOST]:PORT syntax (default: 127.0.0.1)`) + fl.StringVar(&c.bindAddr, "bind", "", "local bind address for ssh tunnel, in [HOST]:PORT syntax (default: 127.0.0.1)") fl.StringVar(&c.sshFlags, "ssh-flags", "", "custom SSH flags") } From 5ea05ebdfaad49bf3fcf0e076f97c15d17910bcd Mon Sep 17 00:00:00 2001 From: Eduardo Argollo Date: Mon, 17 Jun 2019 20:36:49 -0700 Subject: [PATCH 09/25] Solves proxy issue, closes #74 when proxy is set at .profile --- sshcode.go | 8 ++++---- sshcode_test.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sshcode.go b/sshcode.go index 97e7502..c519420 100644 --- a/sshcode.go +++ b/sshcode.go @@ -56,9 +56,9 @@ func sshCode(host, dir string, o options) error { dlScript := downloadScript(codeServerPath) // Downloads the latest code-server and allows it to be executed. - sshCmdStr := fmt.Sprintf("ssh %v %v '/usr/bin/env bash'", o.sshFlags, host) + sshCmdStr := fmt.Sprintf("ssh %v %v '/usr/bin/env bash -l'", o.sshFlags, host) - sshCmd := exec.Command("sh", "-c", sshCmdStr) + sshCmd := exec.Command("sh", "-l", "-c", sshCmdStr) sshCmd.Stdout = os.Stdout sshCmd.Stderr = os.Stderr sshCmd.Stdin = strings.NewReader(dlScript) @@ -99,7 +99,7 @@ func sshCode(host, dir string, o options) error { ) // Starts code-server and forwards the remote port. - sshCmd = exec.Command("sh", "-c", sshCmdStr) + sshCmd = exec.Command("sh", "-l", "-c", sshCmdStr) sshCmd.Stdin = os.Stdin sshCmd.Stdout = os.Stdout sshCmd.Stderr = os.Stderr @@ -396,7 +396,7 @@ func parseHost(host string) (parsedHost string, additionalFlags string, err erro func parseGCPSSHCmd(instance string) (ip, sshFlags string, err error) { dryRunCmd := fmt.Sprintf("gcloud compute ssh --dry-run %v", instance) - out, err := exec.Command("sh", "-c", dryRunCmd).CombinedOutput() + out, err := exec.Command("sh", "-l", "-c", dryRunCmd).CombinedOutput() if err != nil { return "", "", xerrors.Errorf("%s: %w", out, err) } diff --git a/sshcode_test.go b/sshcode_test.go index fc6eb7d..096bff6 100644 --- a/sshcode_test.go +++ b/sshcode_test.go @@ -48,7 +48,7 @@ func TestSSHCode(t *testing.T) { waitForSSHCode(t, remotePort, time.Second*30) // Typically we'd do an os.Stat call here but the os package doesn't expand '~' - out, err := exec.Command("sh", "-c", "stat "+codeServerPath).CombinedOutput() + out, err := exec.Command("sh", "-l", "-c", "stat "+codeServerPath).CombinedOutput() require.NoError(t, err, "%s", out) out, err = exec.Command("pkill", filepath.Base(codeServerPath)).CombinedOutput() @@ -200,7 +200,7 @@ func handleSession(ch ssh.Channel, in <-chan *ssh.Request, t *testing.T) { return } - cmd := exec.Command("sh", "-c", exReq.Command) + cmd := exec.Command("sh", "-l", "-c", exReq.Command) stdin, err := cmd.StdinPipe() require.NoError(t, err) From 780633703d399ebe8574460021e9dd649aa03349 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 27 Jun 2019 03:15:40 +0000 Subject: [PATCH 10/25] add remote OS support paragraph to README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index a5db262..dddd19e 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,11 @@ We currently support: - MacOS - WSL +For the remote server, we currently only support Linux `x86_64` (64-bit) +servers with `glibc`. `musl` libc (which is most notably used by Alpine Linux) +is currently not supported on the remote server: +[#122](https://github.com/cdr/sshcode/issues/122). + ## Usage ```bash From 0b43319a1239a71532e94d3747006dc676fddd97 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 27 Jun 2019 14:15:24 +1000 Subject: [PATCH 11/25] make :PORT optional in parseBindAddr --- main.go | 2 +- sshcode.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 0d16036..f637438 100644 --- a/main.go +++ b/main.go @@ -53,7 +53,7 @@ func (c *rootCmd) RegisterFlags(fl *flag.FlagSet) { fl.BoolVar(&c.skipSync, "skipsync", false, "skip syncing local settings and extensions to remote host") fl.BoolVar(&c.syncBack, "b", false, "sync extensions back on termination") fl.BoolVar(&c.printVersion, "version", false, "print version information and exit") - fl.StringVar(&c.bindAddr, "bind", "", "local bind address for ssh tunnel, in [HOST]:PORT syntax (default: 127.0.0.1)") + fl.StringVar(&c.bindAddr, "bind", "", "local bind address for SSH tunnel, in [HOST][:PORT] syntax (default: 127.0.0.1)") fl.StringVar(&c.sshFlags, "ssh-flags", "", "custom SSH flags") } diff --git a/sshcode.go b/sshcode.go index 97e7502..c43f0f7 100644 --- a/sshcode.go +++ b/sshcode.go @@ -168,8 +168,8 @@ func sshCode(host, dir string, o options) error { } func parseBindAddr(bindAddr string) (string, error) { - if bindAddr == "" { - bindAddr = ":" + if !strings.Contains(bindAddr, ":") { + bindAddr += ":" } host, port, err := net.SplitHostPort(bindAddr) From 379475593da8f175df7d40394b8efb22156b9235 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 27 Jun 2019 16:12:48 +1000 Subject: [PATCH 12/25] restructure SSH master code, apply requested fixes --- main.go | 10 ++-- sshcode.go | 172 +++++++++++++++++++++++++++++------------------------ 2 files changed, 100 insertions(+), 82 deletions(-) diff --git a/main.go b/main.go index ddd8eb3..b53b21f 100644 --- a/main.go +++ b/main.go @@ -78,11 +78,11 @@ func (c *rootCmd) Run(fl *flag.FlagSet) { } err := sshCode(host, dir, options{ - skipSync: c.skipSync, - sshFlags: c.sshFlags, - bindAddr: c.bindAddr, - syncBack: c.syncBack, - noReuseConnection: c.noReuseConnection, + skipSync: c.skipSync, + sshFlags: c.sshFlags, + bindAddr: c.bindAddr, + syncBack: c.syncBack, + reuseConnection: !c.noReuseConnection, }) if err != nil { diff --git a/sshcode.go b/sshcode.go index 92b7f3c..9f80fe0 100644 --- a/sshcode.go +++ b/sshcode.go @@ -21,18 +21,21 @@ import ( ) const codeServerPath = "~/.cache/sshcode/sshcode-server" -const sshDirectory = "~/.ssh" -const sshDirectoryUnsafeModeMask = 0022 -const sshControlPath = sshDirectory + "/control-%h-%p-%r" + +const ( + sshDirectory = "~/.ssh" + sshDirectoryUnsafeModeMask = 0022 + sshControlPath = sshDirectory + "/control-%h-%p-%r" +) type options struct { - skipSync bool - syncBack bool - noOpen bool - noReuseConnection bool - bindAddr string - remotePort string - sshFlags string + skipSync bool + syncBack bool + noOpen bool + reuseConnection bool + bindAddr string + remotePort string + sshFlags string } func sshCode(host, dir string, o options) error { @@ -57,68 +60,19 @@ func sshCode(host, dir string, o options) error { } // Check the SSH directory's permissions and warn the user if it is not safe. - sshDirectoryMode, err := os.Lstat(expandPath(sshDirectory)) - if err != nil { - if !o.noReuseConnection { - flog.Info("failed to stat %v directory, disabling connection reuse feature: %v", sshDirectory, err) - o.noReuseConnection = true - } - } else { - if !sshDirectoryMode.IsDir() { - if !o.noReuseConnection { - flog.Info("%v is not a directory, disabling connection reuse feature", sshDirectory) - o.noReuseConnection = true - } else { - flog.Info("warning: %v is not a directory", sshDirectory) - } - } - if sshDirectoryMode.Mode().Perm()&sshDirectoryUnsafeModeMask != 0 { - flog.Info("warning: the %v directory has unsafe permissions, they should only be writable by "+ - "the owner (and files inside should be set to 0600)", sshDirectory) - } - } + o.reuseConnection = checkSSHDirectory(sshDirectory, o.reuseConnection) // Start SSH master connection socket. This prevents multiple password prompts from appearing as authentication // only happens on the initial connection. - if !o.noReuseConnection { - newSSHFlags := fmt.Sprintf(`%v -o "ControlPath=%v"`, o.sshFlags, sshControlPath) - - // -MN means "start a master socket and don't open a session, just connect". - sshCmdStr := fmt.Sprintf(`exec ssh %v -MN %v`, newSSHFlags, host) - sshMasterCmd := exec.Command("sh", "-c", sshCmdStr) - sshMasterCmd.Stdin = os.Stdin - sshMasterCmd.Stdout = os.Stdout - sshMasterCmd.Stderr = os.Stderr - stopSSHMaster := func() { - if sshMasterCmd.Process != nil { - err := sshMasterCmd.Process.Signal(syscall.Signal(0)) - if err != nil { - return - } - err = sshMasterCmd.Process.Signal(syscall.SIGTERM) - if err != nil { - flog.Error("failed to send SIGTERM to SSH master process: %v", err) - } - } - } - defer stopSSHMaster() - - err = sshMasterCmd.Start() - go sshMasterCmd.Wait() + if o.reuseConnection { + flog.Info("starting SSH master connection...") + newSSHFlags, cancel, err := startSSHMaster(o.sshFlags, sshControlPath, host) + defer cancel() if err != nil { - flog.Error("failed to start SSH master connection, disabling connection reuse feature: %v", err) - o.noReuseConnection = true - stopSSHMaster() + flog.Error("failed to start SSH master connection: %v", err) + o.reuseConnection = false } else { - err = checkSSHMaster(sshMasterCmd, newSSHFlags, host) - if err != nil { - flog.Error("SSH master failed to be ready in time, disabling connection reuse feature: %v", err) - o.noReuseConnection = true - stopSSHMaster() - } else { - sshMasterCmd.Stdin = nil - o.sshFlags = newSSHFlags - } + o.sshFlags = newSSHFlags } } @@ -226,12 +180,12 @@ func sshCode(host, dir string, o options) error { err = syncExtensions(o.sshFlags, host, true) if err != nil { - return xerrors.Errorf("failed to sync extensions back: %v", err) + return xerrors.Errorf("failed to sync extensions back: %w", err) } err = syncUserSettings(o.sshFlags, host, true) if err != nil { - return xerrors.Errorf("failed to sync user settings settings back: %v", err) + return xerrors.Errorf("failed to sync user settings settings back: %w", err) } return nil @@ -350,6 +304,74 @@ func randomPort() (string, error) { return "", xerrors.Errorf("max number of tries exceeded: %d", maxTries) } +// checkSSHDirectory performs sanity and safety checks on sshDirectory, and +// returns a new value for o.reuseConnection depending on the checks. +func checkSSHDirectory(sshDirectory string, reuseConnection bool) bool { + sshDirectoryMode, err := os.Lstat(expandPath(sshDirectory)) + if err != nil { + if reuseConnection { + flog.Info("failed to stat %v directory, disabling connection reuse feature: %v", sshDirectory, err) + } + reuseConnection = false + } else { + if !sshDirectoryMode.IsDir() { + if reuseConnection { + flog.Info("%v is not a directory, disabling connection reuse feature", sshDirectory) + } else { + flog.Info("warning: %v is not a directory", sshDirectory) + } + reuseConnection = false + } + if sshDirectoryMode.Mode().Perm()&sshDirectoryUnsafeModeMask != 0 { + flog.Info("warning: the %v directory has unsafe permissions, they should only be writable by "+ + "the owner (and files inside should be set to 0600)", sshDirectory) + } + } + return reuseConnection +} + +// startSSHMaster starts an SSH master connection and waits for it to be ready. +// It returns a new set of SSH flags for child SSH processes to use. +func startSSHMaster(sshFlags string, sshControlPath string, host string) (string, func(), error) { + ctx, cancel := context.WithCancel(context.Background()) + + newSSHFlags := fmt.Sprintf(`%v -o "ControlPath=%v"`, sshFlags, sshControlPath) + + // -MN means "start a master socket and don't open a session, just connect". + sshCmdStr := fmt.Sprintf(`exec ssh %v -MNq %v`, newSSHFlags, host) + sshMasterCmd := exec.CommandContext(ctx, "sh", "-c", sshCmdStr) + sshMasterCmd.Stdin = os.Stdin + sshMasterCmd.Stderr = os.Stderr + + // Gracefully stop the SSH master. + stopSSHMaster := func() { + if sshMasterCmd.Process != nil { + if sshMasterCmd.ProcessState != nil && sshMasterCmd.ProcessState.Exited() { + return + } + err := sshMasterCmd.Process.Signal(syscall.SIGTERM) + if err != nil { + flog.Error("failed to send SIGTERM to SSH master process: %v", err) + } + } + cancel() + } + + // Start ssh master and wait. Waiting prevents the process from becoming a zombie process if it dies before + // sshcode does, and allows sshMasterCmd.ProcessState to be populated. + err := sshMasterCmd.Start() + go sshMasterCmd.Wait() + if err != nil { + return "", stopSSHMaster, err + } + err = checkSSHMaster(sshMasterCmd, newSSHFlags, host) + if err != nil { + stopSSHMaster() + return "", stopSSHMaster, xerrors.Errorf("SSH master wasn't ready on time: %w", err) + } + return newSSHFlags, stopSSHMaster, nil +} + // checkSSHMaster polls every second for 30 seconds to check if the SSH master // is ready. func checkSSHMaster(sshMasterCmd *exec.Cmd, sshFlags string, host string) error { @@ -359,16 +381,12 @@ func checkSSHMaster(sshMasterCmd *exec.Cmd, sshFlags string, host string) error err error ) for i := 0; i < maxTries; i++ { - // Check if the master is running - if sshMasterCmd.Process == nil { - return xerrors.Errorf("SSH master process not running") - } - err = sshMasterCmd.Process.Signal(syscall.Signal(0)) - if err != nil { - return xerrors.Errorf("failed to check if SSH master process was alive: %v", err) + // Check if the master is running. + if sshMasterCmd.Process == nil || (sshMasterCmd.ProcessState != nil && sshMasterCmd.ProcessState.Exited()) { + return xerrors.Errorf("SSH master process is not running") } - // Check if it's ready + // Check if it's ready. sshCmdStr := fmt.Sprintf(`ssh %v -O check %v`, sshFlags, host) sshCmd := exec.Command("sh", "-c", sshCmdStr) err = sshCmd.Run() From dbf7a484881632d2fc1f0129d756632923cb022e Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 27 Jun 2019 16:21:29 +1000 Subject: [PATCH 13/25] add comment about tildes in expandPath --- sshcode.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sshcode.go b/sshcode.go index 9f80fe0..565ac43 100644 --- a/sshcode.go +++ b/sshcode.go @@ -195,7 +195,8 @@ func sshCode(host, dir string, o options) error { func expandPath(path string) string { path = filepath.Clean(os.ExpandEnv(path)) - // Replace tilde notation in path with the home directory. + // Replace tilde notation in path with the home directory. You can't replace the first instance of `~` in the + // string with the homedir as having a tilde in the middle of a filename is valid. homedir := os.Getenv("HOME") if homedir != "" { if path == "~" { From 1eaed4cacd0f7f3d0262366d0841580007ba0048 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 11 Jul 2019 13:55:57 +1000 Subject: [PATCH 14/25] replace wget with curl --- sshcode.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sshcode.go b/sshcode.go index f525126..ad2235e 100644 --- a/sshcode.go +++ b/sshcode.go @@ -485,7 +485,11 @@ func downloadScript(codeServerPath string) string { pkill -f %v || true mkdir -p ~/.local/share/code-server %v cd %v -wget -N https://codesrv-ci.cdr.sh/latest-linux +curlflags="-o latest-linux" +if [ -f latest-linux ]; then + curlflags="$curlflags -z latest-linux" +fi +curl $curlflags https://codesrv-ci.cdr.sh/latest-linux [ -f %v ] && rm %v ln latest-linux %v chmod +x %v`, From 8c729652acd1d6aa8449b90fb9977e12f620c95c Mon Sep 17 00:00:00 2001 From: Sahil Soni Date: Thu, 18 Jul 2019 19:08:01 +0530 Subject: [PATCH 15/25] Return not the , but the for GCP instances Signed-off-by: Sahil Soni --- sshcode.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sshcode.go b/sshcode.go index ad2235e..ddd3343 100644 --- a/sshcode.go +++ b/sshcode.go @@ -564,5 +564,5 @@ func parseGCPSSHCmd(instance string) (ip, sshFlags string, err error) { return "", "", xerrors.Errorf("parsed invalid ip address %v", ip) } - return ip, sshFlags, nil + return strings.TrimSpace(userIP), sshFlags, nil } From 70153311764c7b95dab2e822adb3aa0eb73bcba8 Mon Sep 17 00:00:00 2001 From: Sahil Soni Date: Fri, 26 Jul 2019 23:46:51 +0530 Subject: [PATCH 16/25] Remove the IP address check in parseGCPSSHCmd This is not needed since dry-run will provide a working host, or will throw error anyways --- sshcode.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/sshcode.go b/sshcode.go index ddd3343..e4a623d 100644 --- a/sshcode.go +++ b/sshcode.go @@ -552,17 +552,6 @@ func parseGCPSSHCmd(instance string) (ip, sshFlags string, err error) { // E.g. foo@1.2.3.4. userIP := toks[len(toks)-1] - toks = strings.Split(userIP, "@") - // Assume the '@' is missing. - if len(toks) < 2 { - ip = strings.TrimSpace(toks[0]) - } else { - ip = strings.TrimSpace(toks[1]) - } - - if net.ParseIP(ip) == nil { - return "", "", xerrors.Errorf("parsed invalid ip address %v", ip) - } return strings.TrimSpace(userIP), sshFlags, nil } From d5dc7c7d177603afb3305c13777486af4cdbf6b6 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 26 Aug 2019 17:23:21 -0500 Subject: [PATCH 17/25] Update go.coder.com/cli to v0.4.0 --- go.mod | 3 ++- go.sum | 6 ++++-- main.go | 7 ++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 26daea9..e8637c0 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,9 @@ go 1.12 require ( github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/pkg/errors v0.8.1 // indirect + github.com/spf13/pflag v1.0.3 github.com/stretchr/testify v1.3.0 - go.coder.com/cli v0.1.0 + go.coder.com/cli v0.4.0 go.coder.com/flog v0.0.0-20190129195112-eaed154a0db8 go.coder.com/retry v0.0.0-20180926062817-cf12c95974ac golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd diff --git a/go.sum b/go.sum index c24ed7b..df0d6a1 100644 --- a/go.sum +++ b/go.sum @@ -13,12 +13,14 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -go.coder.com/cli v0.1.0 h1:ZAjpjXJxMnwj1TqXUi7nnXXuxiPRfwfoC2kViN93oMM= -go.coder.com/cli v0.1.0/go.mod h1:pbVagI9YH/HHMManxPFML4P527GDREwsb+yciZ7mtB8= +go.coder.com/cli v0.4.0 h1:PruDGwm/CPFndyK/eMowZG3vzg5CgohRWeXWCTr3zi8= +go.coder.com/cli v0.4.0/go.mod h1:hRTOURCR3LJF1FRW9arecgrzX+AHG7mfYMwThPIgq+w= go.coder.com/flog v0.0.0-20190129195112-eaed154a0db8 h1:PtQ3moPi4EAz3cyQhkUs1IGIXa2QgJpP60yMjOdu0kk= go.coder.com/flog v0.0.0-20190129195112-eaed154a0db8/go.mod h1:83JsYgXYv0EOaXjIMnaZ1Fl6ddNB3fJnDZ/8845mUJ8= go.coder.com/retry v0.0.0-20180926062817-cf12c95974ac h1:ekdpsuykRy/E+SDq5BquFomNhRCk8OOyhtnACW9Bi50= diff --git a/main.go b/main.go index 2a63211..d2c2b8e 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,14 @@ package main import ( - "flag" "fmt" "math/rand" "os" "strings" "time" + "github.com/spf13/pflag" + "go.coder.com/cli" "go.coder.com/flog" ) @@ -50,7 +51,7 @@ func (c *rootCmd) Spec() cli.CommandSpec { } } -func (c *rootCmd) RegisterFlags(fl *flag.FlagSet) { +func (c *rootCmd) RegisterFlags(fl *pflag.FlagSet) { fl.BoolVar(&c.skipSync, "skipsync", false, "skip syncing local settings and extensions to remote host") fl.BoolVar(&c.syncBack, "b", false, "sync extensions back on termination") fl.BoolVar(&c.printVersion, "version", false, "print version information and exit") @@ -59,7 +60,7 @@ func (c *rootCmd) RegisterFlags(fl *flag.FlagSet) { fl.StringVar(&c.sshFlags, "ssh-flags", "", "custom SSH flags") } -func (c *rootCmd) Run(fl *flag.FlagSet) { +func (c *rootCmd) Run(fl *pflag.FlagSet) { if c.printVersion { fmt.Printf("%v\n", version) os.Exit(0) From 1985f23b500598a30b772e5b6356fde8a93c0190 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 23 Aug 2019 23:53:00 +0000 Subject: [PATCH 18/25] Add ability to upload local binary --- main.go | 3 ++ sshcode.go | 87 +++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/main.go b/main.go index d2c2b8e..f3a9b58 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,7 @@ type rootCmd struct { noReuseConnection bool bindAddr string sshFlags string + codeServerPath string } func (c *rootCmd) Spec() cli.CommandSpec { @@ -58,6 +59,7 @@ func (c *rootCmd) RegisterFlags(fl *pflag.FlagSet) { fl.BoolVar(&c.noReuseConnection, "no-reuse-connection", false, "do not reuse SSH connection via control socket") fl.StringVar(&c.bindAddr, "bind", "", "local bind address for SSH tunnel, in [HOST][:PORT] syntax (default: 127.0.0.1)") fl.StringVar(&c.sshFlags, "ssh-flags", "", "custom SSH flags") + fl.StringVar(&c.codeServerPath, "code-server-path", "", "custom code-server binary to upload") } func (c *rootCmd) Run(fl *pflag.FlagSet) { @@ -84,6 +86,7 @@ func (c *rootCmd) Run(fl *pflag.FlagSet) { bindAddr: c.bindAddr, syncBack: c.syncBack, reuseConnection: !c.noReuseConnection, + codeServerPath: c.codeServerPath, }) if err != nil { diff --git a/sshcode.go b/sshcode.go index e4a623d..2e47332 100644 --- a/sshcode.go +++ b/sshcode.go @@ -36,6 +36,7 @@ type options struct { bindAddr string remotePort string sshFlags string + codeServerPath string } func sshCode(host, dir string, o options) error { @@ -76,23 +77,49 @@ func sshCode(host, dir string, o options) error { } } - flog.Info("ensuring code-server is updated...") - dlScript := downloadScript(codeServerPath) + // Upload local code-server or download code-server from CI server. + if o.codeServerPath != "" { + flog.Info("uploading local code-server binary...") + err = copyCodeServerBinary(o.sshFlags, host, o.codeServerPath, codeServerPath) + if err != nil { + return xerrors.Errorf("failed to upload local code-server binary to remote server: %w", err) + } - // Downloads the latest code-server and allows it to be executed. - sshCmdStr := fmt.Sprintf("ssh %v %v '/usr/bin/env bash -l'", o.sshFlags, host) + sshCmdStr := + fmt.Sprintf("ssh %v %v 'chmod +x %v'", + o.sshFlags, host, codeServerPath, + ) - sshCmd := exec.Command("sh", "-l", "-c", sshCmdStr) - sshCmd.Stdout = os.Stdout - sshCmd.Stderr = os.Stderr - sshCmd.Stdin = strings.NewReader(dlScript) - err = sshCmd.Run() - if err != nil { - return xerrors.Errorf("failed to update code-server: \n---ssh cmd---\n%s\n---download script---\n%s: %w", - sshCmdStr, - dlScript, - err, - ) + sshCmd := exec.Command("sh", "-l", "-c", sshCmdStr) + sshCmd.Stdout = os.Stdout + sshCmd.Stderr = os.Stderr + err = sshCmd.Run() + if err != nil { + return xerrors.Errorf("failed to make code-server binary executable:\n---ssh cmd---\n%s: %w", + sshCmdStr, + err, + ) + } + } else { + flog.Info("ensuring code-server is updated...") + dlScript := downloadScript(codeServerPath) + + // Downloads the latest code-server and allows it to be executed. + sshCmdStr := fmt.Sprintf("ssh %v %v '/usr/bin/env bash -l'", o.sshFlags, host) + + sshCmd := exec.Command("sh", "-l", "-c", sshCmdStr) + sshCmd.Stdout = os.Stdout + sshCmd.Stderr = os.Stderr + sshCmd.Stdin = strings.NewReader(dlScript) + err = sshCmd.Run() + if err != nil { + return xerrors.Errorf("failed to update code-server:\n---ssh cmd---\n%s"+ + "\n---download script---\n%s: %w", + sshCmdStr, + dlScript, + err, + ) + } } if !o.skipSync { @@ -117,13 +144,13 @@ func sshCode(host, dir string, o options) error { flog.Info("Tunneling remote port %v to %v", o.remotePort, o.bindAddr) - sshCmdStr = + sshCmdStr := fmt.Sprintf("ssh -tt -q -L %v:localhost:%v %v %v 'cd %v; %v --host 127.0.0.1 --allow-http --no-auth --port=%v'", o.bindAddr, o.remotePort, o.sshFlags, host, dir, codeServerPath, o.remotePort, ) // Starts code-server and forwards the remote port. - sshCmd = exec.Command("sh", "-l", "-c", sshCmdStr) + sshCmd := exec.Command("sh", "-l", "-c", sshCmdStr) sshCmd.Stdin = os.Stdin sshCmd.Stdout = os.Stdout sshCmd.Stderr = os.Stderr @@ -399,6 +426,20 @@ func checkSSHMaster(sshMasterCmd *exec.Cmd, sshFlags string, host string) error return xerrors.Errorf("max number of tries exceeded: %d", maxTries) } +// copyCodeServerBinary copies a code-server binary from local to remote. +func copyCodeServerBinary(sshFlags string, host string, localPath string, remotePath string) error { + if err := ensureFile(localPath); err != nil { + return err + } + + var ( + src = localPath + dest = host + ":" + remotePath + ) + + return rsync(src, dest, sshFlags) +} + func syncUserSettings(sshFlags string, host string, back bool) error { localConfDir, err := configDir() if err != nil { @@ -517,6 +558,18 @@ func ensureDir(path string) error { return nil } +// ensureFile tries to stat the specified path and ensure it's a file. +func ensureFile(path string) error { + info, err := os.Stat(path) + if err != nil { + return err + } + if info.IsDir() { + return xerrors.New("path is a directory") + } + return nil +} + // parseHost parses the host argument. If 'gcp:' is prefixed to the // host then a lookup is done using gcloud to determine the external IP and any // additional SSH arguments that should be used for ssh commands. Otherwise, host From c8e4017bc837a11f88a0cc3546dbcce01fbc15a4 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 6 Sep 2019 16:09:04 +0000 Subject: [PATCH 19/25] Rename --code-server-path to --upload-code-server --- main.go | 16 ++++++++-------- sshcode.go | 26 +++++++++++++------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/main.go b/main.go index f3a9b58..b355dc1 100644 --- a/main.go +++ b/main.go @@ -41,7 +41,7 @@ type rootCmd struct { noReuseConnection bool bindAddr string sshFlags string - codeServerPath string + uploadCodeServer string } func (c *rootCmd) Spec() cli.CommandSpec { @@ -59,7 +59,7 @@ func (c *rootCmd) RegisterFlags(fl *pflag.FlagSet) { fl.BoolVar(&c.noReuseConnection, "no-reuse-connection", false, "do not reuse SSH connection via control socket") fl.StringVar(&c.bindAddr, "bind", "", "local bind address for SSH tunnel, in [HOST][:PORT] syntax (default: 127.0.0.1)") fl.StringVar(&c.sshFlags, "ssh-flags", "", "custom SSH flags") - fl.StringVar(&c.codeServerPath, "code-server-path", "", "custom code-server binary to upload") + fl.StringVar(&c.uploadCodeServer, "upload-code-server", "", "custom code-server binary to upload to the remote host") } func (c *rootCmd) Run(fl *pflag.FlagSet) { @@ -81,12 +81,12 @@ func (c *rootCmd) Run(fl *pflag.FlagSet) { } err := sshCode(host, dir, options{ - skipSync: c.skipSync, - sshFlags: c.sshFlags, - bindAddr: c.bindAddr, - syncBack: c.syncBack, - reuseConnection: !c.noReuseConnection, - codeServerPath: c.codeServerPath, + skipSync: c.skipSync, + sshFlags: c.sshFlags, + bindAddr: c.bindAddr, + syncBack: c.syncBack, + reuseConnection: !c.noReuseConnection, + uploadCodeServer: c.uploadCodeServer, }) if err != nil { diff --git a/sshcode.go b/sshcode.go index 2e47332..accdd0d 100644 --- a/sshcode.go +++ b/sshcode.go @@ -29,14 +29,14 @@ const ( ) type options struct { - skipSync bool - syncBack bool - noOpen bool - reuseConnection bool - bindAddr string - remotePort string - sshFlags string - codeServerPath string + skipSync bool + syncBack bool + noOpen bool + reuseConnection bool + bindAddr string + remotePort string + sshFlags string + uploadCodeServer string } func sshCode(host, dir string, o options) error { @@ -78,9 +78,9 @@ func sshCode(host, dir string, o options) error { } // Upload local code-server or download code-server from CI server. - if o.codeServerPath != "" { + if o.uploadCodeServer != "" { flog.Info("uploading local code-server binary...") - err = copyCodeServerBinary(o.sshFlags, host, o.codeServerPath, codeServerPath) + err = copyCodeServerBinary(o.sshFlags, host, o.uploadCodeServer, codeServerPath) if err != nil { return xerrors.Errorf("failed to upload local code-server binary to remote server: %w", err) } @@ -428,7 +428,7 @@ func checkSSHMaster(sshMasterCmd *exec.Cmd, sshFlags string, host string) error // copyCodeServerBinary copies a code-server binary from local to remote. func copyCodeServerBinary(sshFlags string, host string, localPath string, remotePath string) error { - if err := ensureFile(localPath); err != nil { + if err := validateIsFile(localPath); err != nil { return err } @@ -558,8 +558,8 @@ func ensureDir(path string) error { return nil } -// ensureFile tries to stat the specified path and ensure it's a file. -func ensureFile(path string) error { +// validateIsFile tries to stat the specified path and ensure it's a file. +func validateIsFile(path string) error { info, err := os.Stat(path) if err != nil { return err From 7e6845d1bda59a90b09f90b8fc7815cdc23267c5 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 11 Sep 2019 10:38:11 -0500 Subject: [PATCH 20/25] Update deps --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e8637c0..e378ffe 100644 --- a/go.mod +++ b/go.mod @@ -12,5 +12,5 @@ require ( go.coder.com/retry v0.0.0-20180926062817-cf12c95974ac golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be // indirect - golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 ) diff --git a/go.sum b/go.sum index df0d6a1..19ceba8 100644 --- a/go.sum +++ b/go.sum @@ -34,5 +34,5 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be h1:mI+jhqkn68ybP0ORJqunXn+fq+Eeb4hHKqLQcFICjAc= golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373 h1:PPwnA7z1Pjf7XYaBP9GL1VAMZmcIWyFz7QCMSIIa3Bg= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 6277c6bb0444e3ef9af40c5baad038e162087a3b Mon Sep 17 00:00:00 2001 From: Gwon Seonggwang Date: Sun, 22 Sep 2019 04:17:06 +0900 Subject: [PATCH 21/25] Update tarname and tar option for ci build Closes #142 --- ci/build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/build.sh b/ci/build.sh index 1f14096..9e30b09 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -10,8 +10,8 @@ build(){ go build -ldflags "-X main.version=${tag}" -o $tmpdir/sshcode pushd $tmpdir - tarname=sshcode-$GOOS-$GOARCH.tar - tar -cf $tarname sshcode + tarname=sshcode-$GOOS-$GOARCH.tar.gz + tar -czf $tarname sshcode popd cp $tmpdir/$tarname bin rm -rf $tmpdir From fd95a4079501b8f9dc1080cd79541838d1c05e4c Mon Sep 17 00:00:00 2001 From: Brandon Callifornia Date: Fri, 25 Oct 2019 11:41:18 +0200 Subject: [PATCH 22/25] Fixed allow-http-warning and no auth --- sshcode.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sshcode.go b/sshcode.go index accdd0d..e769c4b 100644 --- a/sshcode.go +++ b/sshcode.go @@ -145,7 +145,7 @@ func sshCode(host, dir string, o options) error { flog.Info("Tunneling remote port %v to %v", o.remotePort, o.bindAddr) sshCmdStr := - fmt.Sprintf("ssh -tt -q -L %v:localhost:%v %v %v 'cd %v; %v --host 127.0.0.1 --allow-http --no-auth --port=%v'", + fmt.Sprintf("ssh -tt -q -L %v:localhost:%v %v %v 'cd %v; %v --host 127.0.0.1 --auth none --port=%v'", o.bindAddr, o.remotePort, o.sshFlags, host, dir, codeServerPath, o.remotePort, ) From 7ebef26504f6390830e436362b3eae72b48c41e2 Mon Sep 17 00:00:00 2001 From: hassieswift621 Date: Sat, 7 Dec 2019 20:02:02 +0000 Subject: [PATCH 23/25] Remove duplicated word from sync user settings error --- sshcode.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sshcode.go b/sshcode.go index e769c4b..8b239aa 100644 --- a/sshcode.go +++ b/sshcode.go @@ -212,7 +212,7 @@ func sshCode(host, dir string, o options) error { err = syncUserSettings(o.sshFlags, host, true) if err != nil { - return xerrors.Errorf("failed to sync user settings settings back: %w", err) + return xerrors.Errorf("failed to sync user settings back: %w", err) } return nil From 50e859cd1084379374420cb5263d053eae3b0254 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 14 Aug 2019 20:02:10 -0700 Subject: [PATCH 24/25] Add git bash and mingw support (#132) Supports git bash and mingw on Windows. Does not support cmd.exe. Signed-off-by: Dean Sheather Co-authored-by: Merith --- .gitignore | 1 + main.go | 6 +++++ settings.go | 4 ++++ sshcode.go | 63 ++++++++++++++++++++++++++++++++++++++++++++--------- 4 files changed, 64 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index dc0daa9..94251e3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ vendor bin .vscode sshcode +sshcode.exe diff --git a/main.go b/main.go index b355dc1..674f983 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "fmt" "math/rand" "os" + "runtime" "strings" "time" @@ -80,6 +81,11 @@ func (c *rootCmd) Run(fl *pflag.FlagSet) { dir = "~" } + // Get linux relative path if on windows. + if runtime.GOOS == "windows" { + dir = gitbashWindowsDir(dir) + } + err := sshCode(host, dir, options{ skipSync: c.skipSync, sshFlags: c.sshFlags, diff --git a/settings.go b/settings.go index ad962a3..e88c260 100644 --- a/settings.go +++ b/settings.go @@ -24,6 +24,8 @@ func configDir() (string, error) { path = os.ExpandEnv("$HOME/.config/Code/User/") case "darwin": path = os.ExpandEnv("$HOME/Library/Application Support/Code/User/") + case "windows": + return os.ExpandEnv("/c/Users/$USERNAME/AppData/Roaming/Code/User"), nil default: return "", xerrors.Errorf("unsupported platform: %s", runtime.GOOS) } @@ -39,6 +41,8 @@ func extensionsDir() (string, error) { switch runtime.GOOS { case "linux", "darwin": path = os.ExpandEnv("$HOME/.vscode/extensions/") + case "windows": + return os.ExpandEnv("/c/Users/$USERNAME/.vscode/extensions"), nil default: return "", xerrors.Errorf("unsupported platform: %s", runtime.GOOS) } diff --git a/sshcode.go b/sshcode.go index 8b239aa..5021c09 100644 --- a/sshcode.go +++ b/sshcode.go @@ -10,6 +10,7 @@ import ( "os/exec" "os/signal" "path/filepath" + "runtime" "strconv" "strings" "syscall" @@ -106,7 +107,6 @@ func sshCode(host, dir string, o options) error { // Downloads the latest code-server and allows it to be executed. sshCmdStr := fmt.Sprintf("ssh %v %v '/usr/bin/env bash -l'", o.sshFlags, host) - sshCmd := exec.Command("sh", "-l", "-c", sshCmdStr) sshCmd.Stdout = os.Stdout sshCmd.Stderr = os.Stderr @@ -145,10 +145,9 @@ func sshCode(host, dir string, o options) error { flog.Info("Tunneling remote port %v to %v", o.remotePort, o.bindAddr) sshCmdStr := - fmt.Sprintf("ssh -tt -q -L %v:localhost:%v %v %v 'cd %v; %v --host 127.0.0.1 --auth none --port=%v'", - o.bindAddr, o.remotePort, o.sshFlags, host, dir, codeServerPath, o.remotePort, + fmt.Sprintf("ssh -tt -q -L %v:localhost:%v %v %v '%v %v --host 127.0.0.1 --auth none --port=%v'", + o.bindAddr, o.remotePort, o.sshFlags, host, codeServerPath, dir, o.remotePort, ) - // Starts code-server and forwards the remote port. sshCmd := exec.Command("sh", "-l", "-c", sshCmdStr) sshCmd.Stdin = os.Stdin @@ -266,9 +265,12 @@ func openBrowser(url string) { const ( macPath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" wslPath = "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe" + winPath = "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe" ) switch { + case commandExists("chrome"): + openCmd = exec.Command("chrome", chromeOptions(url)...) case commandExists("google-chrome"): openCmd = exec.Command("google-chrome", chromeOptions(url)...) case commandExists("google-chrome-stable"): @@ -281,6 +283,8 @@ func openBrowser(url string) { openCmd = exec.Command(macPath, chromeOptions(url)...) case pathExists(wslPath): openCmd = exec.Command(wslPath, chromeOptions(url)...) + case pathExists(winPath): + openCmd = exec.Command(winPath, chromeOptions(url)...) default: err := browser.OpenURL(url) if err != nil { @@ -335,6 +339,11 @@ func randomPort() (string, error) { // checkSSHDirectory performs sanity and safety checks on sshDirectory, and // returns a new value for o.reuseConnection depending on the checks. func checkSSHDirectory(sshDirectory string, reuseConnection bool) bool { + if runtime.GOOS == "windows" { + flog.Info("OS is windows, disabling connection reuse feature") + return false + } + sshDirectoryMode, err := os.Lstat(expandPath(sshDirectory)) if err != nil { if reuseConnection { @@ -451,8 +460,10 @@ func syncUserSettings(sshFlags string, host string, back bool) error { return err } - const remoteSettingsDir = "~/.local/share/code-server/User/" - + var remoteSettingsDir = "~/.local/share/code-server/User/" + if runtime.GOOS == "windows" { + remoteSettingsDir = ".local/share/code-server/User/" + } var ( src = localConfDir + "/" dest = host + ":" + remoteSettingsDir @@ -477,7 +488,10 @@ func syncExtensions(sshFlags string, host string, back bool) error { return err } - const remoteExtensionsDir = "~/.local/share/code-server/extensions/" + var remoteExtensionsDir = "~/.local/share/code-server/extensions/" + if runtime.GOOS == "windows" { + remoteExtensionsDir = ".local/share/code-server/extensions/" + } var ( src = localExtensionsDir + "/" @@ -505,6 +519,7 @@ func rsync(src string, dest string, sshFlags string, excludePaths ...string) err // locally in order to properly delete an extension. "--delete", "--copy-unsafe-links", + "-zz", src, dest, )..., ) @@ -524,7 +539,7 @@ func downloadScript(codeServerPath string) string { [ "$(uname -m)" != "x86_64" ] && echo "Unsupported server architecture $(uname -m). code-server only has releases for x86_64 systems." && exit 1 pkill -f %v || true -mkdir -p ~/.local/share/code-server %v +mkdir -p $HOME/.local/share/code-server %v cd %v curlflags="-o latest-linux" if [ -f latest-linux ]; then @@ -535,8 +550,8 @@ curl $curlflags https://codesrv-ci.cdr.sh/latest-linux ln latest-linux %v chmod +x %v`, codeServerPath, - filepath.Dir(codeServerPath), - filepath.Dir(codeServerPath), + filepath.ToSlash(filepath.Dir(codeServerPath)), + filepath.ToSlash(filepath.Dir(codeServerPath)), codeServerPath, codeServerPath, codeServerPath, @@ -548,6 +563,11 @@ chmod +x %v`, func ensureDir(path string) error { _, err := os.Stat(path) if os.IsNotExist(err) { + // This fixes a issue where Go reads `/c/` as `C:\c\` and creates + // empty directories on the client that don't need to exist. + if runtime.GOOS == "windows" && strings.HasPrefix(path, "/c/") { + path = "C:" + path[2:] + } err = os.MkdirAll(path, 0750) } @@ -608,3 +628,26 @@ func parseGCPSSHCmd(instance string) (ip, sshFlags string, err error) { return strings.TrimSpace(userIP), sshFlags, nil } + +// gitbashWindowsDir strips a the msys2 install directory from the beginning of +// the path. On msys2, if a user provides `/workspace` sshcode will receive +// `C:/msys64/workspace` which won't work on the remote host. +func gitbashWindowsDir(dir string) string { + + // Don't bother figuring out path if it's relative to home dir. + if strings.HasPrefix(dir, "~/") { + if dir == "~" { + return "~/" + } + return dir + } + + mingwPrefix, err := exec.Command("sh", "-c", "{ cd / && pwd -W; }").Output() + if err != nil { + // Default to a sane location. + mingwPrefix = []byte("C:/mingw64") + } + + prefix := strings.TrimSuffix(string(mingwPrefix), "/\n") + return strings.TrimPrefix(dir, prefix) +} From b52faf9528bdaa4cab8a20492065fed358b48b94 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 10 Aug 2020 14:34:05 -0400 Subject: [PATCH 25/25] Add deprecation notice to README See #185 --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index dddd19e..3882f94 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # sshcode +**This project has been deprecated in favour of the [code-server install script](https://github.com/cdr/code-server#quick-install)** + +**See the discussion in [#185](https://github.com/cdr/sshcode/issues/185)** + +--- + [!["Open Issues"](https://img.shields.io/github/issues-raw/cdr/sshcode.svg)](https://github.com/cdr/sshcode/issues) [!["Latest Release"](https://img.shields.io/github/release/cdr/sshcode.svg)](https://github.com/cdr/sshcode/releases/latest) [![MIT license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/cdr/sshcode/blob/master/LICENSE)