From 42e08e18b1d73db6d94fde591666cd981dbb9e0a Mon Sep 17 00:00:00 2001 From: "brian m. carlson" Date: Wed, 3 Feb 2021 22:27:01 +0000 Subject: [PATCH] Move much of SSH code into a separate package In the future, we'll want to call into the SSH code from multiple packages, so let's move it out of the lfshttp package into its own package to avoid package import loops. While we're at it, rename the function names to remove the "ssh" prefix, since it's implied by the fact that they're in a package called "ssh". Move the tests to their own package to prevent an import loop and expose the private functions so we can test them there. --- Makefile | 1 + lfsapi/endpoint_finder_test.go | 21 +- lfshttp/endpoint.go | 3 +- lfshttp/ssh.go | 116 +-------- lfshttp/ssh_test.go | 450 +-------------------------------- ssh/ssh.go | 123 +++++++++ ssh/ssh_test.go | 424 +++++++++++++++++++++++++++++++ 7 files changed, 572 insertions(+), 566 deletions(-) create mode 100644 ssh/ssh.go create mode 100644 ssh/ssh_test.go diff --git a/Makefile b/Makefile index 6d4589b0..d647203f 100644 --- a/Makefile +++ b/Makefile @@ -126,6 +126,7 @@ PKGS += lfs PKGS += lfsapi PKGS += lfshttp PKGS += locking +PKGS += ssh PKGS += subprocess PKGS += tasklog PKGS += tools diff --git a/lfsapi/endpoint_finder_test.go b/lfsapi/endpoint_finder_test.go index a8a98389..1dd2e22f 100644 --- a/lfsapi/endpoint_finder_test.go +++ b/lfsapi/endpoint_finder_test.go @@ -8,6 +8,7 @@ import ( "github.com/git-lfs/git-lfs/creds" "github.com/git-lfs/git-lfs/lfshttp" + "github.com/git-lfs/git-lfs/ssh" "github.com/stretchr/testify/assert" ) @@ -557,7 +558,7 @@ func TestEndpointParsing(t *testing.T) { "git@github.com:git-lfs/git-lfs.git", lfshttp.Endpoint{ Url: "https://github.com/git-lfs/git-lfs.git", - SSHMetadata: lfshttp.SSHMetadata{ + SSHMetadata: ssh.SSHMetadata{ UserAndHost: "git@github.com", Path: "git-lfs/git-lfs.git", Port: "", @@ -569,7 +570,7 @@ func TestEndpointParsing(t *testing.T) { "[git@lfshttp.github.com:443]:git-lfs/git-lfs.git", lfshttp.Endpoint{ Url: "https://lfshttp.github.com/git-lfs/git-lfs.git", - SSHMetadata: lfshttp.SSHMetadata{ + SSHMetadata: ssh.SSHMetadata{ UserAndHost: "git@lfshttp.github.com", Path: "git-lfs/git-lfs.git", Port: "443", @@ -581,7 +582,7 @@ func TestEndpointParsing(t *testing.T) { "github.com:git-lfs/git-lfs.git", lfshttp.Endpoint{ Url: "https://github.com/git-lfs/git-lfs.git", - SSHMetadata: lfshttp.SSHMetadata{ + SSHMetadata: ssh.SSHMetadata{ UserAndHost: "github.com", Path: "git-lfs/git-lfs.git", Port: "", @@ -593,7 +594,7 @@ func TestEndpointParsing(t *testing.T) { "github:git-lfs/git-lfs.git", lfshttp.Endpoint{ Url: "https://github/git-lfs/git-lfs.git", - SSHMetadata: lfshttp.SSHMetadata{ + SSHMetadata: ssh.SSHMetadata{ UserAndHost: "github", Path: "git-lfs/git-lfs.git", Port: "", @@ -605,7 +606,7 @@ func TestEndpointParsing(t *testing.T) { "gh:git-lfs/git-lfs.git", lfshttp.Endpoint{ Url: "https://github.com/git-lfs/git-lfs.git", - SSHMetadata: lfshttp.SSHMetadata{ + SSHMetadata: ssh.SSHMetadata{ UserAndHost: "", Path: "", Port: "", @@ -617,7 +618,7 @@ func TestEndpointParsing(t *testing.T) { "remote::git-lfs/git-lfs.git", lfshttp.Endpoint{ Url: "remote::git-lfs/git-lfs.git", - SSHMetadata: lfshttp.SSHMetadata{ + SSHMetadata: ssh.SSHMetadata{ UserAndHost: "", Path: "", Port: "", @@ -656,7 +657,7 @@ func TestInsteadOf(t *testing.T) { "download", lfshttp.Endpoint{ Url: "https://example.com/git-lfs/git-lfs.git/info/lfs", - SSHMetadata: lfshttp.SSHMetadata{ + SSHMetadata: ssh.SSHMetadata{ UserAndHost: "", Path: "", Port: "", @@ -669,7 +670,7 @@ func TestInsteadOf(t *testing.T) { "upload", lfshttp.Endpoint{ Url: "https://example.com/git-lfs/git-lfs.git/info/lfs", - SSHMetadata: lfshttp.SSHMetadata{ + SSHMetadata: ssh.SSHMetadata{ UserAndHost: "example.com", Path: "git-lfs/git-lfs.git", Port: "", @@ -682,7 +683,7 @@ func TestInsteadOf(t *testing.T) { "download", lfshttp.Endpoint{ Url: "https://example.com/git-lfs/git-lfs.git/info/lfs", - SSHMetadata: lfshttp.SSHMetadata{ + SSHMetadata: ssh.SSHMetadata{ UserAndHost: "example.com", Path: "git-lfs/git-lfs.git", Port: "", @@ -695,7 +696,7 @@ func TestInsteadOf(t *testing.T) { "upload", lfshttp.Endpoint{ Url: "https://example.com/git-lfs/git-lfs.git/info/lfs", - SSHMetadata: lfshttp.SSHMetadata{ + SSHMetadata: ssh.SSHMetadata{ UserAndHost: "example.com", Path: "git-lfs/git-lfs.git", Port: "", diff --git a/lfshttp/endpoint.go b/lfshttp/endpoint.go index 721bb219..6b8406bd 100644 --- a/lfshttp/endpoint.go +++ b/lfshttp/endpoint.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/git-lfs/git-lfs/git" + "github.com/git-lfs/git-lfs/ssh" ) const UrlUnknown = "" @@ -14,7 +15,7 @@ const UrlUnknown = "" // An Endpoint describes how to access a Git LFS server. type Endpoint struct { Url string - SSHMetadata SSHMetadata + SSHMetadata ssh.SSHMetadata Operation string } diff --git a/lfshttp/ssh.go b/lfshttp/ssh.go index 388eb4f0..49a69a5b 100644 --- a/lfshttp/ssh.go +++ b/lfshttp/ssh.go @@ -3,13 +3,11 @@ package lfshttp import ( "bytes" "encoding/json" - "fmt" - "path/filepath" - "regexp" "strings" "time" "github.com/git-lfs/git-lfs/config" + "github.com/git-lfs/git-lfs/ssh" "github.com/git-lfs/git-lfs/subprocess" "github.com/git-lfs/git-lfs/tools" "github.com/rubyist/tracerx" @@ -81,7 +79,7 @@ func (c *sshAuthClient) Resolve(e Endpoint, method string) (sshAuthResponse, err return res, nil } - exe, args := sshGetLFSExeAndArgs(c.os, c.git, &e.SSHMetadata, endpointOperation(e, method), method) + exe, args := ssh.GetLFSExeAndArgs(c.os, c.git, &e.SSHMetadata, endpointOperation(e, method), method) cmd := subprocess.ExecCommand(exe, args...) // Save stdout and stderr in separate buffers @@ -114,113 +112,3 @@ func (c *sshAuthClient) Resolve(e Endpoint, method string) (sshAuthResponse, err return res, err } - -type SSHMetadata struct { - UserAndHost string - Port string - Path string -} - -func sshFormatArgs(cmd string, args []string, needShell bool) (string, []string) { - if !needShell { - return cmd, args - } - - return subprocess.FormatForShellQuotedArgs(cmd, args) -} - -func sshGetLFSExeAndArgs(osEnv config.Environment, gitEnv config.Environment, meta *SSHMetadata, operation, method string) (string, []string) { - exe, args, needShell := sshGetExeAndArgs(osEnv, gitEnv, meta) - args = append(args, fmt.Sprintf("git-lfs-authenticate %s %s", meta.Path, operation)) - exe, args = sshFormatArgs(exe, args, needShell) - tracerx.Printf("run_command: %s %s", exe, strings.Join(args, " ")) - return exe, args -} - -// Parse command, and if it looks like a valid command, return the ssh binary -// name, the command to run, and whether we need a shell. If not, return -// existing as the ssh binary name. -func sshParseShellCommand(command string, existing string) (ssh string, cmd string, needShell bool) { - ssh = existing - if cmdArgs := tools.QuotedFields(command); len(cmdArgs) > 0 { - needShell = true - ssh = cmdArgs[0] - cmd = command - } - return -} - -// Return the executable name for ssh on this machine and the base args -// Base args includes port settings, user/host, everything pre the command to execute -func sshGetExeAndArgs(osEnv config.Environment, gitEnv config.Environment, meta *SSHMetadata) (exe string, baseargs []string, needShell bool) { - var cmd string - - isPlink := false - isTortoise := false - - ssh, _ := osEnv.Get("GIT_SSH") - sshCmd, _ := osEnv.Get("GIT_SSH_COMMAND") - ssh, cmd, needShell = sshParseShellCommand(sshCmd, ssh) - - if ssh == "" { - sshCmd, _ := gitEnv.Get("core.sshcommand") - ssh, cmd, needShell = sshParseShellCommand(sshCmd, defaultSSHCmd) - } - - if cmd == "" { - cmd = ssh - } - - basessh := filepath.Base(ssh) - - if basessh != defaultSSHCmd { - // Strip extension for easier comparison - if ext := filepath.Ext(basessh); len(ext) > 0 { - basessh = basessh[:len(basessh)-len(ext)] - } - isPlink = strings.EqualFold(basessh, "plink") - isTortoise = strings.EqualFold(basessh, "tortoiseplink") - } - - args := make([]string, 0, 7) - - if isTortoise { - // TortoisePlink requires the -batch argument to behave like ssh/plink - args = append(args, "-batch") - } - - if len(meta.Port) > 0 { - if isPlink || isTortoise { - args = append(args, "-P") - } else { - args = append(args, "-p") - } - args = append(args, meta.Port) - } - - if sep, ok := sshSeparators[basessh]; ok { - // inserts a separator between cli -options and host/cmd commands - // example: $ ssh -p 12345 -- user@host.com git-lfs-authenticate ... - args = append(args, sep, meta.UserAndHost) - } else { - // no prefix supported, strip leading - off host to prevent cmd like: - // $ git config lfs.url ssh://-proxycmd=whatever - // $ plink -P 12345 -proxycmd=foo git-lfs-authenticate ... - // - // Instead, it'll attempt this, and eventually return an error - // $ plink -P 12345 proxycmd=foo git-lfs-authenticate ... - args = append(args, sshOptPrefixRE.ReplaceAllString(meta.UserAndHost, "")) - } - - return cmd, args, needShell -} - -const defaultSSHCmd = "ssh" - -var ( - sshOptPrefixRE = regexp.MustCompile(`\A\-+`) - sshSeparators = map[string]string{ - "ssh": "--", - "lfs-ssh-echo": "--", // used in lfs integration tests only - } -) diff --git a/lfshttp/ssh_test.go b/lfshttp/ssh_test.go index 17adc2fa..195f175f 100644 --- a/lfshttp/ssh_test.go +++ b/lfshttp/ssh_test.go @@ -1,14 +1,12 @@ package lfshttp import ( - "net/url" - "path/filepath" "testing" "time" "github.com/git-lfs/git-lfs/errors" + sshp "github.com/git-lfs/git-lfs/ssh" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestSSHCacheResolveFromCache(t *testing.T) { @@ -21,7 +19,7 @@ func TestSSHCacheResolveFromCache(t *testing.T) { ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ - SSHMetadata: SSHMetadata{ + SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", @@ -44,7 +42,7 @@ func TestSSHCacheResolveFromCacheWithFutureExpiresAt(t *testing.T) { ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ - SSHMetadata: SSHMetadata{ + SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", @@ -67,7 +65,7 @@ func TestSSHCacheResolveFromCacheWithFutureExpiresIn(t *testing.T) { ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ - SSHMetadata: SSHMetadata{ + SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", @@ -90,7 +88,7 @@ func TestSSHCacheResolveFromCacheWithPastExpiresAt(t *testing.T) { ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ - SSHMetadata: SSHMetadata{ + SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", @@ -113,7 +111,7 @@ func TestSSHCacheResolveFromCacheWithPastExpiresIn(t *testing.T) { ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ - SSHMetadata: SSHMetadata{ + SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", @@ -137,7 +135,7 @@ func TestSSHCacheResolveFromCacheWithAmbiguousExpirationInfo(t *testing.T) { ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ - SSHMetadata: SSHMetadata{ + SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", @@ -158,7 +156,7 @@ func TestSSHCacheResolveWithoutError(t *testing.T) { ssh.responses["userandhost"] = sshAuthResponse{Href: "real"} e := Endpoint{ - SSHMetadata: SSHMetadata{ + SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", @@ -190,7 +188,7 @@ func TestSSHCacheResolveWithError(t *testing.T) { ssh.responses["userandhost"] = sshAuthResponse{Message: "resolve error", Href: "real"} e := Endpoint{ - SSHMetadata: SSHMetadata{ + SSHMetadata: sshp.SSHMetadata{ UserAndHost: "userandhost", Port: "1", Path: "path", @@ -227,433 +225,3 @@ func (r *fakeResolver) Resolve(e Endpoint, method string) (sshAuthResponse, erro return res, err } - -func TestSSHGetLFSExeAndArgs(t *testing.T) { - cli, err := NewClient(nil) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - endpoint.SSHMetadata.Path = "user/repo" - - exe, args := sshGetLFSExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata, endpoint.Operation, "GET") - assert.Equal(t, "ssh", exe) - assert.Equal(t, []string{ - "--", - "user@foo.com", - "git-lfs-authenticate user/repo download", - }, args) - - exe, args = sshGetLFSExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata, endpoint.Operation, "HEAD") - assert.Equal(t, "ssh", exe) - assert.Equal(t, []string{ - "--", - "user@foo.com", - "git-lfs-authenticate user/repo download", - }, args) - - // this is going by endpoint.Operation, implicitly set by Endpoint() on L15. - exe, args = sshGetLFSExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata, endpoint.Operation, "POST") - assert.Equal(t, "ssh", exe) - assert.Equal(t, []string{ - "--", - "user@foo.com", - "git-lfs-authenticate user/repo download", - }, args) - - endpoint.Operation = "upload" - exe, args = sshGetLFSExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata, endpoint.Operation, "POST") - assert.Equal(t, "ssh", exe) - assert.Equal(t, []string{ - "--", - "user@foo.com", - "git-lfs-authenticate user/repo upload", - }, args) -} - -func TestSSHGetExeAndArgsSsh(t *testing.T) { - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": "", - "GIT_SSH": "", - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "ssh", exe) - assert.Equal(t, []string{"--", "user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsSshCustomPort(t *testing.T) { - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": "", - "GIT_SSH": "", - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - endpoint.SSHMetadata.Port = "8888" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "ssh", exe) - assert.Equal(t, []string{"-p", "8888", "--", "user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsPlink(t *testing.T) { - plink := filepath.Join("Users", "joebloggs", "bin", "plink.exe") - - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": "", - "GIT_SSH": plink, - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, plink, exe) - assert.Equal(t, []string{"user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsPlinkCustomPort(t *testing.T) { - plink := filepath.Join("Users", "joebloggs", "bin", "plink") - - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": "", - "GIT_SSH": plink, - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - endpoint.SSHMetadata.Port = "8888" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, plink, exe) - assert.Equal(t, []string{"-P", "8888", "user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsTortoisePlink(t *testing.T) { - plink := filepath.Join("Users", "joebloggs", "bin", "tortoiseplink.exe") - - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": "", - "GIT_SSH": plink, - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, plink, exe) - assert.Equal(t, []string{"-batch", "user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsTortoisePlinkCustomPort(t *testing.T) { - plink := filepath.Join("Users", "joebloggs", "bin", "tortoiseplink") - - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": "", - "GIT_SSH": plink, - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - endpoint.SSHMetadata.Port = "8888" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, plink, exe) - assert.Equal(t, []string{"-batch", "-P", "8888", "user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsSshCommandPrecedence(t *testing.T) { - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": "sshcmd", - "GIT_SSH": "bad", - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "sh", exe) - assert.Equal(t, []string{"-c", "sshcmd user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsSshCommandArgs(t *testing.T) { - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": "sshcmd --args 1", - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "sh", exe) - assert.Equal(t, []string{"-c", "sshcmd --args 1 user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsSshCommandArgsWithMixedQuotes(t *testing.T) { - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": "sshcmd foo 'bar \"baz\"'", - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "sh", exe) - assert.Equal(t, []string{"-c", "sshcmd foo 'bar \"baz\"' user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsSshCommandCustomPort(t *testing.T) { - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": "sshcmd", - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - endpoint.SSHMetadata.Port = "8888" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "sh", exe) - assert.Equal(t, []string{"-c", "sshcmd -p 8888 user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsCoreSshCommand(t *testing.T) { - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": "sshcmd --args 2", - }, map[string]string{ - "core.sshcommand": "sshcmd --args 1", - })) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "sh", exe) - assert.Equal(t, []string{"-c", "sshcmd --args 2 user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsCoreSshCommandArgsWithMixedQuotes(t *testing.T) { - cli, err := NewClient(NewContext(nil, nil, map[string]string{ - "core.sshcommand": "sshcmd foo 'bar \"baz\"'", - })) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "sh", exe) - assert.Equal(t, []string{"-c", "sshcmd foo 'bar \"baz\"' user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsConfigVersusEnv(t *testing.T) { - cli, err := NewClient(NewContext(nil, nil, map[string]string{ - "core.sshcommand": "sshcmd --args 1", - })) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "sh", exe) - assert.Equal(t, []string{"-c", "sshcmd --args 1 user@foo.com"}, args) -} - -func TestSSHGetLFSExeAndArgsWithCustomSSH(t *testing.T) { - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH": "not-ssh", - }, nil)) - require.Nil(t, err) - - u, err := url.Parse("ssh://git@host.com:12345/repo") - require.Nil(t, err) - - e := EndpointFromSshUrl(u) - t.Logf("ENDPOINT: %+v", e) - assert.Equal(t, "12345", e.SSHMetadata.Port) - assert.Equal(t, "git@host.com", e.SSHMetadata.UserAndHost) - assert.Equal(t, "repo", e.SSHMetadata.Path) - - exe, args := sshGetLFSExeAndArgs(cli.OSEnv(), cli.GitEnv(), &e.SSHMetadata, "download", "GET") - assert.Equal(t, "not-ssh", exe) - assert.Equal(t, []string{"-p", "12345", "git@host.com", "git-lfs-authenticate repo download"}, args) -} - -func TestSSHGetLFSExeAndArgsInvalidOptionsAsHost(t *testing.T) { - cli, err := NewClient(nil) - require.Nil(t, err) - - u, err := url.Parse("ssh://-oProxyCommand=gnome-calculator/repo") - require.Nil(t, err) - assert.Equal(t, "-oProxyCommand=gnome-calculator", u.Host) - - e := EndpointFromSshUrl(u) - t.Logf("ENDPOINT: %+v", e) - assert.Equal(t, "-oProxyCommand=gnome-calculator", e.SSHMetadata.UserAndHost) - assert.Equal(t, "repo", e.SSHMetadata.Path) - - exe, args := sshGetLFSExeAndArgs(cli.OSEnv(), cli.GitEnv(), &e.SSHMetadata, "download", "GET") - assert.Equal(t, "ssh", exe) - assert.Equal(t, []string{"--", "-oProxyCommand=gnome-calculator", "git-lfs-authenticate repo download"}, args) -} - -func TestSSHGetLFSExeAndArgsInvalidOptionsAsHostWithCustomSSH(t *testing.T) { - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH": "not-ssh", - }, nil)) - require.Nil(t, err) - - u, err := url.Parse("ssh://--oProxyCommand=gnome-calculator/repo") - require.Nil(t, err) - assert.Equal(t, "--oProxyCommand=gnome-calculator", u.Host) - - e := EndpointFromSshUrl(u) - t.Logf("ENDPOINT: %+v", e) - assert.Equal(t, "--oProxyCommand=gnome-calculator", e.SSHMetadata.UserAndHost) - assert.Equal(t, "repo", e.SSHMetadata.Path) - - exe, args := sshGetLFSExeAndArgs(cli.OSEnv(), cli.GitEnv(), &e.SSHMetadata, "download", "GET") - assert.Equal(t, "not-ssh", exe) - assert.Equal(t, []string{"oProxyCommand=gnome-calculator", "git-lfs-authenticate repo download"}, args) -} - -func TestSSHGetExeAndArgsInvalidOptionsAsHost(t *testing.T) { - cli, err := NewClient(nil) - require.Nil(t, err) - - u, err := url.Parse("ssh://-oProxyCommand=gnome-calculator") - require.Nil(t, err) - assert.Equal(t, "-oProxyCommand=gnome-calculator", u.Host) - - e := EndpointFromSshUrl(u) - t.Logf("ENDPOINT: %+v", e) - assert.Equal(t, "-oProxyCommand=gnome-calculator", e.SSHMetadata.UserAndHost) - assert.Equal(t, "", e.SSHMetadata.Path) - - exe, args, needShell := sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &e.SSHMetadata) - assert.Equal(t, "ssh", exe) - assert.Equal(t, []string{"--", "-oProxyCommand=gnome-calculator"}, args) - assert.Equal(t, false, needShell) -} - -func TestSSHGetExeAndArgsInvalidOptionsAsPath(t *testing.T) { - cli, err := NewClient(nil) - require.Nil(t, err) - - u, err := url.Parse("ssh://git@git-host.com/-oProxyCommand=gnome-calculator") - require.Nil(t, err) - assert.Equal(t, "git-host.com", u.Host) - - e := EndpointFromSshUrl(u) - t.Logf("ENDPOINT: %+v", e) - assert.Equal(t, "git@git-host.com", e.SSHMetadata.UserAndHost) - assert.Equal(t, "-oProxyCommand=gnome-calculator", e.SSHMetadata.Path) - - exe, args, needShell := sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &e.SSHMetadata) - assert.Equal(t, "ssh", exe) - assert.Equal(t, []string{"--", "git@git-host.com"}, args) - assert.Equal(t, false, needShell) -} - -func TestParseBareSSHUrl(t *testing.T) { - e := EndpointFromBareSshUrl("git@git-host.com:repo.git") - t.Logf("endpoint: %+v", e) - assert.Equal(t, "git@git-host.com", e.SSHMetadata.UserAndHost) - assert.Equal(t, "repo.git", e.SSHMetadata.Path) - - e = EndpointFromBareSshUrl("git@git-host.com/should-be-a-colon.git") - t.Logf("endpoint: %+v", e) - assert.Equal(t, "", e.SSHMetadata.UserAndHost) - assert.Equal(t, "", e.SSHMetadata.Path) - - e = EndpointFromBareSshUrl("-oProxyCommand=gnome-calculator") - t.Logf("endpoint: %+v", e) - assert.Equal(t, "", e.SSHMetadata.UserAndHost) - assert.Equal(t, "", e.SSHMetadata.Path) - - e = EndpointFromBareSshUrl("git@git-host.com:-oProxyCommand=gnome-calculator") - t.Logf("endpoint: %+v", e) - assert.Equal(t, "git@git-host.com", e.SSHMetadata.UserAndHost) - assert.Equal(t, "-oProxyCommand=gnome-calculator", e.SSHMetadata.Path) -} - -func TestSSHGetExeAndArgsPlinkCommand(t *testing.T) { - plink := filepath.Join("Users", "joebloggs", "bin", "plink.exe") - - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": plink, - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "sh", exe) - assert.Equal(t, []string{"-c", plink + " user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsPlinkCommandCustomPort(t *testing.T) { - plink := filepath.Join("Users", "joebloggs", "bin", "plink") - - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": plink, - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - endpoint.SSHMetadata.Port = "8888" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "sh", exe) - assert.Equal(t, []string{"-c", plink + " -P 8888 user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsTortoisePlinkCommand(t *testing.T) { - plink := filepath.Join("Users", "joebloggs", "bin", "tortoiseplink.exe") - - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": plink, - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "sh", exe) - assert.Equal(t, []string{"-c", plink + " -batch user@foo.com"}, args) -} - -func TestSSHGetExeAndArgsTortoisePlinkCommandCustomPort(t *testing.T) { - plink := filepath.Join("Users", "joebloggs", "bin", "tortoiseplink") - - cli, err := NewClient(NewContext(nil, map[string]string{ - "GIT_SSH_COMMAND": plink, - }, nil)) - require.Nil(t, err) - - endpoint := Endpoint{Operation: "download"} - endpoint.SSHMetadata.UserAndHost = "user@foo.com" - endpoint.SSHMetadata.Port = "8888" - - exe, args := sshFormatArgs(sshGetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &endpoint.SSHMetadata)) - assert.Equal(t, "sh", exe) - assert.Equal(t, []string{"-c", plink + " -batch -P 8888 user@foo.com"}, args) -} diff --git a/ssh/ssh.go b/ssh/ssh.go new file mode 100644 index 00000000..b5d0eb15 --- /dev/null +++ b/ssh/ssh.go @@ -0,0 +1,123 @@ +package ssh + +import ( + "fmt" + "path/filepath" + "regexp" + "strings" + + "github.com/git-lfs/git-lfs/config" + "github.com/git-lfs/git-lfs/subprocess" + "github.com/git-lfs/git-lfs/tools" + "github.com/rubyist/tracerx" +) + +type SSHMetadata struct { + UserAndHost string + Port string + Path string +} + +func FormatArgs(cmd string, args []string, needShell bool) (string, []string) { + if !needShell { + return cmd, args + } + + return subprocess.FormatForShellQuotedArgs(cmd, args) +} + +func GetLFSExeAndArgs(osEnv config.Environment, gitEnv config.Environment, meta *SSHMetadata, operation, method string) (string, []string) { + exe, args, needShell := GetExeAndArgs(osEnv, gitEnv, meta) + args = append(args, fmt.Sprintf("git-lfs-authenticate %s %s", meta.Path, operation)) + exe, args = FormatArgs(exe, args, needShell) + tracerx.Printf("run_command: %s %s", exe, strings.Join(args, " ")) + return exe, args +} + +// Parse command, and if it looks like a valid command, return the ssh binary +// name, the command to run, and whether we need a shell. If not, return +// existing as the ssh binary name. +func parseShellCommand(command string, existing string) (ssh string, cmd string, needShell bool) { + ssh = existing + if cmdArgs := tools.QuotedFields(command); len(cmdArgs) > 0 { + needShell = true + ssh = cmdArgs[0] + cmd = command + } + return +} + +// Return the executable name for ssh on this machine and the base args +// Base args includes port settings, user/host, everything pre the command to execute +func GetExeAndArgs(osEnv config.Environment, gitEnv config.Environment, meta *SSHMetadata) (exe string, baseargs []string, needShell bool) { + var cmd string + + isPlink := false + isTortoise := false + + ssh, _ := osEnv.Get("GIT_SSH") + sshCmd, _ := osEnv.Get("GIT_SSH_COMMAND") + ssh, cmd, needShell = parseShellCommand(sshCmd, ssh) + + if ssh == "" { + sshCmd, _ := gitEnv.Get("core.sshcommand") + ssh, cmd, needShell = parseShellCommand(sshCmd, defaultSSHCmd) + } + + if cmd == "" { + cmd = ssh + } + + basessh := filepath.Base(ssh) + + if basessh != defaultSSHCmd { + // Strip extension for easier comparison + if ext := filepath.Ext(basessh); len(ext) > 0 { + basessh = basessh[:len(basessh)-len(ext)] + } + isPlink = strings.EqualFold(basessh, "plink") + isTortoise = strings.EqualFold(basessh, "tortoiseplink") + } + + args := make([]string, 0, 7) + + if isTortoise { + // TortoisePlink requires the -batch argument to behave like ssh/plink + args = append(args, "-batch") + } + + if len(meta.Port) > 0 { + if isPlink || isTortoise { + args = append(args, "-P") + } else { + args = append(args, "-p") + } + args = append(args, meta.Port) + } + + if sep, ok := sshSeparators[basessh]; ok { + // inserts a separator between cli -options and host/cmd commands + // example: $ ssh -p 12345 -- user@host.com git-lfs-authenticate ... + args = append(args, sep, meta.UserAndHost) + } else { + // no prefix supported, strip leading - off host to prevent cmd like: + // $ git config lfs.url ssh://-proxycmd=whatever + // $ plink -P 12345 -proxycmd=foo git-lfs-authenticate ... + // + // Instead, it'll attempt this, and eventually return an error + // $ plink -P 12345 proxycmd=foo git-lfs-authenticate ... + args = append(args, sshOptPrefixRE.ReplaceAllString(meta.UserAndHost, "")) + } + + return cmd, args, needShell +} + +const defaultSSHCmd = "ssh" + +var ( + sshOptPrefixRE = regexp.MustCompile(`\A\-+`) + sshSeparators = map[string]string{ + "ssh": "--", + "lfs-ssh-echo": "--", // used in lfs integration tests only + } +) diff --git a/ssh/ssh_test.go b/ssh/ssh_test.go new file mode 100644 index 00000000..2880d36d --- /dev/null +++ b/ssh/ssh_test.go @@ -0,0 +1,424 @@ +package ssh_test + +import ( + "net/url" + "path/filepath" + "testing" + + "github.com/git-lfs/git-lfs/lfshttp" + "github.com/git-lfs/git-lfs/ssh" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSSHGetLFSExeAndArgs(t *testing.T) { + cli, err := lfshttp.NewClient(nil) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + meta.Path = "user/repo" + + exe, args := ssh.GetLFSExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta, "download", "GET") + assert.Equal(t, "ssh", exe) + assert.Equal(t, []string{ + "--", + "user@foo.com", + "git-lfs-authenticate user/repo download", + }, args) + + exe, args = ssh.GetLFSExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta, "upload", "GET") + assert.Equal(t, "ssh", exe) + assert.Equal(t, []string{ + "--", + "user@foo.com", + "git-lfs-authenticate user/repo upload", + }, args) +} + +func TestSSHGetExeAndArgsSsh(t *testing.T) { + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": "", + "GIT_SSH": "", + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "ssh", exe) + assert.Equal(t, []string{"--", "user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsSshCustomPort(t *testing.T) { + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": "", + "GIT_SSH": "", + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + meta.Port = "8888" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "ssh", exe) + assert.Equal(t, []string{"-p", "8888", "--", "user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsPlink(t *testing.T) { + plink := filepath.Join("Users", "joebloggs", "bin", "plink.exe") + + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": "", + "GIT_SSH": plink, + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, plink, exe) + assert.Equal(t, []string{"user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsPlinkCustomPort(t *testing.T) { + plink := filepath.Join("Users", "joebloggs", "bin", "plink") + + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": "", + "GIT_SSH": plink, + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + meta.Port = "8888" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, plink, exe) + assert.Equal(t, []string{"-P", "8888", "user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsTortoisePlink(t *testing.T) { + plink := filepath.Join("Users", "joebloggs", "bin", "tortoiseplink.exe") + + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": "", + "GIT_SSH": plink, + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, plink, exe) + assert.Equal(t, []string{"-batch", "user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsTortoisePlinkCustomPort(t *testing.T) { + plink := filepath.Join("Users", "joebloggs", "bin", "tortoiseplink") + + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": "", + "GIT_SSH": plink, + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + meta.Port = "8888" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, plink, exe) + assert.Equal(t, []string{"-batch", "-P", "8888", "user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsSshCommandPrecedence(t *testing.T) { + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": "sshcmd", + "GIT_SSH": "bad", + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "sh", exe) + assert.Equal(t, []string{"-c", "sshcmd user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsSshCommandArgs(t *testing.T) { + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": "sshcmd --args 1", + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "sh", exe) + assert.Equal(t, []string{"-c", "sshcmd --args 1 user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsSshCommandArgsWithMixedQuotes(t *testing.T) { + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": "sshcmd foo 'bar \"baz\"'", + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "sh", exe) + assert.Equal(t, []string{"-c", "sshcmd foo 'bar \"baz\"' user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsSshCommandCustomPort(t *testing.T) { + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": "sshcmd", + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + meta.Port = "8888" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "sh", exe) + assert.Equal(t, []string{"-c", "sshcmd -p 8888 user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsCoreSshCommand(t *testing.T) { + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": "sshcmd --args 2", + }, map[string]string{ + "core.sshcommand": "sshcmd --args 1", + })) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "sh", exe) + assert.Equal(t, []string{"-c", "sshcmd --args 2 user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsCoreSshCommandArgsWithMixedQuotes(t *testing.T) { + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, nil, map[string]string{ + "core.sshcommand": "sshcmd foo 'bar \"baz\"'", + })) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "sh", exe) + assert.Equal(t, []string{"-c", "sshcmd foo 'bar \"baz\"' user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsConfigVersusEnv(t *testing.T) { + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, nil, map[string]string{ + "core.sshcommand": "sshcmd --args 1", + })) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "sh", exe) + assert.Equal(t, []string{"-c", "sshcmd --args 1 user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsPlinkCommand(t *testing.T) { + plink := filepath.Join("Users", "joebloggs", "bin", "plink.exe") + + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": plink, + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "sh", exe) + assert.Equal(t, []string{"-c", plink + " user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsPlinkCommandCustomPort(t *testing.T) { + plink := filepath.Join("Users", "joebloggs", "bin", "plink") + + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": plink, + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + meta.Port = "8888" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "sh", exe) + assert.Equal(t, []string{"-c", plink + " -P 8888 user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsTortoisePlinkCommand(t *testing.T) { + plink := filepath.Join("Users", "joebloggs", "bin", "tortoiseplink.exe") + + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": plink, + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "sh", exe) + assert.Equal(t, []string{"-c", plink + " -batch user@foo.com"}, args) +} + +func TestSSHGetExeAndArgsTortoisePlinkCommandCustomPort(t *testing.T) { + plink := filepath.Join("Users", "joebloggs", "bin", "tortoiseplink") + + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH_COMMAND": plink, + }, nil)) + require.Nil(t, err) + + meta := ssh.SSHMetadata{} + meta.UserAndHost = "user@foo.com" + meta.Port = "8888" + + exe, args := ssh.FormatArgs(ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &meta)) + assert.Equal(t, "sh", exe) + assert.Equal(t, []string{"-c", plink + " -batch -P 8888 user@foo.com"}, args) +} + +func TestSSHGetLFSExeAndArgsWithCustomSSH(t *testing.T) { + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH": "not-ssh", + }, nil)) + require.Nil(t, err) + + u, err := url.Parse("ssh://git@host.com:12345/repo") + require.Nil(t, err) + + e := lfshttp.EndpointFromSshUrl(u) + t.Logf("ENDPOINT: %+v", e) + assert.Equal(t, "12345", e.SSHMetadata.Port) + assert.Equal(t, "git@host.com", e.SSHMetadata.UserAndHost) + assert.Equal(t, "repo", e.SSHMetadata.Path) + + exe, args := ssh.GetLFSExeAndArgs(cli.OSEnv(), cli.GitEnv(), &e.SSHMetadata, "download", "GET") + assert.Equal(t, "not-ssh", exe) + assert.Equal(t, []string{"-p", "12345", "git@host.com", "git-lfs-authenticate repo download"}, args) +} + +func TestSSHGetLFSExeAndArgsInvalidOptionsAsHost(t *testing.T) { + cli, err := lfshttp.NewClient(nil) + require.Nil(t, err) + + u, err := url.Parse("ssh://-oProxyCommand=gnome-calculator/repo") + require.Nil(t, err) + assert.Equal(t, "-oProxyCommand=gnome-calculator", u.Host) + + e := lfshttp.EndpointFromSshUrl(u) + t.Logf("ENDPOINT: %+v", e) + assert.Equal(t, "-oProxyCommand=gnome-calculator", e.SSHMetadata.UserAndHost) + assert.Equal(t, "repo", e.SSHMetadata.Path) + + exe, args := ssh.GetLFSExeAndArgs(cli.OSEnv(), cli.GitEnv(), &e.SSHMetadata, "download", "GET") + assert.Equal(t, "ssh", exe) + assert.Equal(t, []string{"--", "-oProxyCommand=gnome-calculator", "git-lfs-authenticate repo download"}, args) +} + +func TestSSHGetLFSExeAndArgsInvalidOptionsAsHostWithCustomSSH(t *testing.T) { + cli, err := lfshttp.NewClient(lfshttp.NewContext(nil, map[string]string{ + "GIT_SSH": "not-ssh", + }, nil)) + require.Nil(t, err) + + u, err := url.Parse("ssh://--oProxyCommand=gnome-calculator/repo") + require.Nil(t, err) + assert.Equal(t, "--oProxyCommand=gnome-calculator", u.Host) + + e := lfshttp.EndpointFromSshUrl(u) + t.Logf("ENDPOINT: %+v", e) + assert.Equal(t, "--oProxyCommand=gnome-calculator", e.SSHMetadata.UserAndHost) + assert.Equal(t, "repo", e.SSHMetadata.Path) + + exe, args := ssh.GetLFSExeAndArgs(cli.OSEnv(), cli.GitEnv(), &e.SSHMetadata, "download", "GET") + assert.Equal(t, "not-ssh", exe) + assert.Equal(t, []string{"oProxyCommand=gnome-calculator", "git-lfs-authenticate repo download"}, args) +} + +func TestSSHGetExeAndArgsInvalidOptionsAsHost(t *testing.T) { + cli, err := lfshttp.NewClient(nil) + require.Nil(t, err) + + u, err := url.Parse("ssh://-oProxyCommand=gnome-calculator") + require.Nil(t, err) + assert.Equal(t, "-oProxyCommand=gnome-calculator", u.Host) + + e := lfshttp.EndpointFromSshUrl(u) + t.Logf("ENDPOINT: %+v", e) + assert.Equal(t, "-oProxyCommand=gnome-calculator", e.SSHMetadata.UserAndHost) + assert.Equal(t, "", e.SSHMetadata.Path) + + exe, args, needShell := ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &e.SSHMetadata) + assert.Equal(t, "ssh", exe) + assert.Equal(t, []string{"--", "-oProxyCommand=gnome-calculator"}, args) + assert.Equal(t, false, needShell) +} + +func TestSSHGetExeAndArgsInvalidOptionsAsPath(t *testing.T) { + cli, err := lfshttp.NewClient(nil) + require.Nil(t, err) + + u, err := url.Parse("ssh://git@git-host.com/-oProxyCommand=gnome-calculator") + require.Nil(t, err) + assert.Equal(t, "git-host.com", u.Host) + + e := lfshttp.EndpointFromSshUrl(u) + t.Logf("ENDPOINT: %+v", e) + assert.Equal(t, "git@git-host.com", e.SSHMetadata.UserAndHost) + assert.Equal(t, "-oProxyCommand=gnome-calculator", e.SSHMetadata.Path) + + exe, args, needShell := ssh.GetExeAndArgs(cli.OSEnv(), cli.GitEnv(), &e.SSHMetadata) + assert.Equal(t, "ssh", exe) + assert.Equal(t, []string{"--", "git@git-host.com"}, args) + assert.Equal(t, false, needShell) +} + +func TestParseBareSSHUrl(t *testing.T) { + e := lfshttp.EndpointFromBareSshUrl("git@git-host.com:repo.git") + t.Logf("endpoint: %+v", e) + assert.Equal(t, "git@git-host.com", e.SSHMetadata.UserAndHost) + assert.Equal(t, "repo.git", e.SSHMetadata.Path) + + e = lfshttp.EndpointFromBareSshUrl("git@git-host.com/should-be-a-colon.git") + t.Logf("endpoint: %+v", e) + assert.Equal(t, "", e.SSHMetadata.UserAndHost) + assert.Equal(t, "", e.SSHMetadata.Path) + + e = lfshttp.EndpointFromBareSshUrl("-oProxyCommand=gnome-calculator") + t.Logf("endpoint: %+v", e) + assert.Equal(t, "", e.SSHMetadata.UserAndHost) + assert.Equal(t, "", e.SSHMetadata.Path) + + e = lfshttp.EndpointFromBareSshUrl("git@git-host.com:-oProxyCommand=gnome-calculator") + t.Logf("endpoint: %+v", e) + assert.Equal(t, "git@git-host.com", e.SSHMetadata.UserAndHost) + assert.Equal(t, "-oProxyCommand=gnome-calculator", e.SSHMetadata.Path) +}