diff options
author | Paulo Gomes <pjbgf@linux.com> | 2023-08-05 10:20:38 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-05 10:20:38 +0100 |
commit | e6f68d2e4cd1bc4447126816c7c27e1fc2098e30 (patch) | |
tree | 15c5e333b93641f9eadcb4bf4b34c338135f7a23 /plumbing/transport/ssh | |
parent | 5882d60fb7ccd4cfc0fe69286aa96e198c9d1eb0 (diff) | |
parent | 4ec6b3f4fa9cdfe8f10d0953ac7d398d01a90f17 (diff) | |
download | go-git-e6f68d2e4cd1bc4447126816c7c27e1fc2098e30.tar.gz |
Merge branch 'master' into jc/commit-ammend
Diffstat (limited to 'plumbing/transport/ssh')
-rw-r--r-- | plumbing/transport/ssh/auth_method.go | 21 | ||||
-rw-r--r-- | plumbing/transport/ssh/common.go | 49 | ||||
-rw-r--r-- | plumbing/transport/ssh/common_test.go | 79 | ||||
-rw-r--r-- | plumbing/transport/ssh/internal/test/proxy_test.go | 112 | ||||
-rw-r--r-- | plumbing/transport/ssh/internal/test/test_utils.go | 83 | ||||
-rw-r--r-- | plumbing/transport/ssh/proxy_test.go | 71 | ||||
-rw-r--r-- | plumbing/transport/ssh/upload_pack_test.go | 10 |
7 files changed, 389 insertions, 36 deletions
diff --git a/plumbing/transport/ssh/auth_method.go b/plumbing/transport/ssh/auth_method.go index 3514669..ac4e358 100644 --- a/plumbing/transport/ssh/auth_method.go +++ b/plumbing/transport/ssh/auth_method.go @@ -3,17 +3,15 @@ package ssh import ( "errors" "fmt" - "io/ioutil" "os" "os/user" "path/filepath" "github.com/go-git/go-git/v5/plumbing/transport" - "github.com/mitchellh/go-homedir" + "github.com/skeema/knownhosts" sshagent "github.com/xanzy/ssh-agent" "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/knownhosts" ) const DefaultUsername = "git" @@ -135,7 +133,7 @@ func NewPublicKeys(user string, pemBytes []byte, password string) (*PublicKeys, // encoded private key. An encryption password should be given if the pemBytes // contains a password encrypted PEM block otherwise password should be empty. func NewPublicKeysFromFile(user, pemFile, password string) (*PublicKeys, error) { - bytes, err := ioutil.ReadFile(pemFile) + bytes, err := os.ReadFile(pemFile) if err != nil { return nil, err } @@ -224,12 +222,19 @@ func (a *PublicKeysCallback) ClientConfig() (*ssh.ClientConfig, error) { // // If list of files is empty, then it will be read from the SSH_KNOWN_HOSTS // environment variable, example: -// /home/foo/custom_known_hosts_file:/etc/custom_known/hosts_file +// +// /home/foo/custom_known_hosts_file:/etc/custom_known/hosts_file // // If SSH_KNOWN_HOSTS is not set the following file locations will be used: -// ~/.ssh/known_hosts -// /etc/ssh/ssh_known_hosts +// +// ~/.ssh/known_hosts +// /etc/ssh/ssh_known_hosts func NewKnownHostsCallback(files ...string) (ssh.HostKeyCallback, error) { + kh, err := newKnownHosts(files...) + return ssh.HostKeyCallback(kh), err +} + +func newKnownHosts(files ...string) (knownhosts.HostKeyCallback, error) { var err error if len(files) == 0 { @@ -251,7 +256,7 @@ func getDefaultKnownHostsFiles() ([]string, error) { return files, nil } - homeDirPath, err := homedir.Dir() + homeDirPath, err := os.UserHomeDir() if err != nil { return nil, err } diff --git a/plumbing/transport/ssh/common.go b/plumbing/transport/ssh/common.go index 46e7913..1531603 100644 --- a/plumbing/transport/ssh/common.go +++ b/plumbing/transport/ssh/common.go @@ -4,12 +4,14 @@ package ssh import ( "context" "fmt" + "net" "reflect" "strconv" "strings" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/internal/common" + "github.com/skeema/knownhosts" "github.com/kevinburke/ssh_config" "golang.org/x/crypto/ssh" @@ -121,10 +123,24 @@ func (c *command) connect() error { if err != nil { return err } + hostWithPort := c.getHostWithPort() + if config.HostKeyCallback == nil { + kh, err := newKnownHosts() + if err != nil { + return err + } + config.HostKeyCallback = kh.HostKeyCallback() + config.HostKeyAlgorithms = kh.HostKeyAlgorithms(hostWithPort) + } else if len(config.HostKeyAlgorithms) == 0 { + // Set the HostKeyAlgorithms based on HostKeyCallback. + // For background see https://github.com/go-git/go-git/issues/411 as well as + // https://github.com/golang/go/issues/29286 for root cause. + config.HostKeyAlgorithms = knownhosts.HostKeyAlgorithms(config.HostKeyCallback, hostWithPort) + } overrideConfig(c.config, config) - c.client, err = dial("tcp", c.getHostWithPort(), config) + c.client, err = dial("tcp", hostWithPort, c.endpoint.Proxy, config) if err != nil { return err } @@ -139,7 +155,7 @@ func (c *command) connect() error { return nil } -func dial(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { +func dial(network, addr string, proxyOpts transport.ProxyOptions, config *ssh.ClientConfig) (*ssh.Client, error) { var ( ctx = context.Background() cancel context.CancelFunc @@ -151,10 +167,33 @@ func dial(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { } defer cancel() - conn, err := proxy.Dial(ctx, network, addr) + var conn net.Conn + var err error + + if proxyOpts.URL != "" { + proxyUrl, err := proxyOpts.FullURL() + if err != nil { + return nil, err + } + dialer, err := proxy.FromURL(proxyUrl, proxy.Direct) + if err != nil { + return nil, err + } + + // Try to use a ContextDialer, but fall back to a Dialer if that goes south. + ctxDialer, ok := dialer.(proxy.ContextDialer) + if !ok { + return nil, fmt.Errorf("expected ssh proxy dialer to be of type %s; got %s", + reflect.TypeOf(ctxDialer), reflect.TypeOf(dialer)) + } + conn, err = ctxDialer.DialContext(ctx, "tcp", addr) + } else { + conn, err = proxy.Dial(ctx, network, addr) + } if err != nil { return nil, err } + c, chans, reqs, err := ssh.NewClientConn(conn, addr, config) if err != nil { return nil, err @@ -173,7 +212,7 @@ func (c *command) getHostWithPort() string { port = DefaultPort } - return fmt.Sprintf("%s:%d", host, port) + return net.JoinHostPort(host, strconv.Itoa(port)) } func (c *command) doGetHostWithPortFromSSHConfig() (addr string, found bool) { @@ -201,7 +240,7 @@ func (c *command) doGetHostWithPortFromSSHConfig() (addr string, found bool) { } } - addr = fmt.Sprintf("%s:%d", host, port) + addr = net.JoinHostPort(host, strconv.Itoa(port)) return } diff --git a/plumbing/transport/ssh/common_test.go b/plumbing/transport/ssh/common_test.go index 6d634d5..496e82d 100644 --- a/plumbing/transport/ssh/common_test.go +++ b/plumbing/transport/ssh/common_test.go @@ -5,23 +5,25 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/gliderlabs/ssh" "github.com/kevinburke/ssh_config" - "golang.org/x/crypto/ssh" + stdssh "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/testdata" . "gopkg.in/check.v1" ) func Test(t *testing.T) { TestingT(t) } func (s *SuiteCommon) TestOverrideConfig(c *C) { - config := &ssh.ClientConfig{ + config := &stdssh.ClientConfig{ User: "foo", - Auth: []ssh.AuthMethod{ - ssh.Password("yourpassword"), + Auth: []stdssh.AuthMethod{ + stdssh.Password("yourpassword"), }, - HostKeyCallback: ssh.FixedHostKey(nil), + HostKeyCallback: stdssh.FixedHostKey(nil), } - target := &ssh.ClientConfig{} + target := &stdssh.ClientConfig{} overrideConfig(config, target) c.Assert(target.User, Equals, "foo") @@ -30,11 +32,11 @@ func (s *SuiteCommon) TestOverrideConfig(c *C) { } func (s *SuiteCommon) TestOverrideConfigKeep(c *C) { - config := &ssh.ClientConfig{ + config := &stdssh.ClientConfig{ User: "foo", } - target := &ssh.ClientConfig{ + target := &stdssh.ClientConfig{ User: "bar", } @@ -93,12 +95,69 @@ func (s *SuiteCommon) TestDefaultSSHConfigWildcard(c *C) { c.Assert(cmd.getHostWithPort(), Equals, "github.com:22") } +func (s *SuiteCommon) TestIgnoreHostKeyCallback(c *C) { + uploadPack := &UploadPackSuite{ + opts: []ssh.Option{ + ssh.HostKeyPEM(testdata.PEMBytes["ed25519"]), + }, + } + uploadPack.SetUpSuite(c) + // Use the default client, which does not have a host key callback + uploadPack.Client = DefaultClient + auth, err := NewPublicKeys("foo", testdata.PEMBytes["rsa"], "") + c.Assert(err, IsNil) + c.Assert(auth, NotNil) + auth.HostKeyCallback = stdssh.InsecureIgnoreHostKey() + ep := uploadPack.newEndpoint(c, "bar.git") + ps, err := uploadPack.Client.NewUploadPackSession(ep, auth) + c.Assert(err, IsNil) + c.Assert(ps, NotNil) +} + +func (s *SuiteCommon) TestFixedHostKeyCallback(c *C) { + hostKey, err := stdssh.ParsePrivateKey(testdata.PEMBytes["ed25519"]) + c.Assert(err, IsNil) + uploadPack := &UploadPackSuite{ + opts: []ssh.Option{ + ssh.HostKeyPEM(testdata.PEMBytes["ed25519"]), + }, + } + uploadPack.SetUpSuite(c) + // Use the default client, which does not have a host key callback + uploadPack.Client = DefaultClient + auth, err := NewPublicKeys("foo", testdata.PEMBytes["rsa"], "") + c.Assert(err, IsNil) + c.Assert(auth, NotNil) + auth.HostKeyCallback = stdssh.FixedHostKey(hostKey.PublicKey()) + ep := uploadPack.newEndpoint(c, "bar.git") + ps, err := uploadPack.Client.NewUploadPackSession(ep, auth) + c.Assert(err, IsNil) + c.Assert(ps, NotNil) +} + +func (s *SuiteCommon) TestFailHostKeyCallback(c *C) { + uploadPack := &UploadPackSuite{ + opts: []ssh.Option{ + ssh.HostKeyPEM(testdata.PEMBytes["ed25519"]), + }, + } + uploadPack.SetUpSuite(c) + // Use the default client, which does not have a host key callback + uploadPack.Client = DefaultClient + auth, err := NewPublicKeys("foo", testdata.PEMBytes["rsa"], "") + c.Assert(err, IsNil) + c.Assert(auth, NotNil) + ep := uploadPack.newEndpoint(c, "bar.git") + _, err = uploadPack.Client.NewUploadPackSession(ep, auth) + c.Assert(err, NotNil) +} + func (s *SuiteCommon) TestIssue70(c *C) { uploadPack := &UploadPackSuite{} uploadPack.SetUpSuite(c) - config := &ssh.ClientConfig{ - HostKeyCallback: ssh.InsecureIgnoreHostKey(), + config := &stdssh.ClientConfig{ + HostKeyCallback: stdssh.InsecureIgnoreHostKey(), } r := &runner{ config: config, diff --git a/plumbing/transport/ssh/internal/test/proxy_test.go b/plumbing/transport/ssh/internal/test/proxy_test.go new file mode 100644 index 0000000..8e775f8 --- /dev/null +++ b/plumbing/transport/ssh/internal/test/proxy_test.go @@ -0,0 +1,112 @@ +package test + +import ( + "context" + "fmt" + "log" + "net" + "os" + "path/filepath" + "sync/atomic" + "testing" + + "github.com/armon/go-socks5" + "github.com/gliderlabs/ssh" + "github.com/go-git/go-git/v5/plumbing/transport" + ggssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" + + fixtures "github.com/go-git/go-git-fixtures/v4" + stdssh "golang.org/x/crypto/ssh" + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type ProxyEnvSuite struct { + fixtures.Suite + port int + base string +} + +var _ = Suite(&ProxyEnvSuite{}) + +var socksProxiedRequests int32 + +// This test tests proxy support via an env var, i.e. `ALL_PROXY`. +// Its located in a separate package because golang caches the value +// of proxy env vars leading to misleading/unexpected test results. +func (s *ProxyEnvSuite) TestCommand(c *C) { + socksListener, err := net.Listen("tcp", "localhost:0") + c.Assert(err, IsNil) + + socksServer, err := socks5.New(&socks5.Config{ + Rules: TestProxyRule{}, + }) + c.Assert(err, IsNil) + go func() { + socksServer.Serve(socksListener) + }() + socksProxyAddr := fmt.Sprintf("socks5://localhost:%d", socksListener.Addr().(*net.TCPAddr).Port) + os.Setenv("ALL_PROXY", socksProxyAddr) + defer os.Unsetenv("ALL_PROXY") + + sshListener, err := net.Listen("tcp", "localhost:0") + c.Assert(err, IsNil) + sshServer := &ssh.Server{Handler: HandlerSSH} + go func() { + log.Fatal(sshServer.Serve(sshListener)) + }() + + s.port = sshListener.Addr().(*net.TCPAddr).Port + s.base, err = os.MkdirTemp(os.TempDir(), fmt.Sprintf("go-git-ssh-%d", s.port)) + c.Assert(err, IsNil) + + ggssh.DefaultAuthBuilder = func(user string) (ggssh.AuthMethod, error) { + return &ggssh.Password{User: user}, nil + } + + ep := s.prepareRepository(c, fixtures.Basic().One(), "basic.git") + c.Assert(err, IsNil) + + client := ggssh.NewClient(&stdssh.ClientConfig{ + HostKeyCallback: stdssh.InsecureIgnoreHostKey(), + }) + r, err := client.NewUploadPackSession(ep, nil) + c.Assert(err, IsNil) + defer func() { c.Assert(r.Close(), IsNil) }() + + info, err := r.AdvertisedReferences() + c.Assert(err, IsNil) + c.Assert(info, NotNil) + proxyUsed := atomic.LoadInt32(&socksProxiedRequests) > 0 + c.Assert(proxyUsed, Equals, true) +} + +func (s *ProxyEnvSuite) prepareRepository(c *C, f *fixtures.Fixture, name string) *transport.Endpoint { + fs := f.DotGit() + + err := fixtures.EnsureIsBare(fs) + c.Assert(err, IsNil) + + path := filepath.Join(s.base, name) + err = os.Rename(fs.Root(), path) + c.Assert(err, IsNil) + + return s.newEndpoint(c, name) +} + +func (s *ProxyEnvSuite) newEndpoint(c *C, name string) *transport.Endpoint { + ep, err := transport.NewEndpoint(fmt.Sprintf( + "ssh://git@localhost:%d/%s/%s", s.port, filepath.ToSlash(s.base), name, + )) + + c.Assert(err, IsNil) + return ep +} + +type TestProxyRule struct{} + +func (dr TestProxyRule) Allow(ctx context.Context, req *socks5.Request) (context.Context, bool) { + atomic.AddInt32(&socksProxiedRequests, 1) + return ctx, true +} diff --git a/plumbing/transport/ssh/internal/test/test_utils.go b/plumbing/transport/ssh/internal/test/test_utils.go new file mode 100644 index 0000000..c3797b1 --- /dev/null +++ b/plumbing/transport/ssh/internal/test/test_utils.go @@ -0,0 +1,83 @@ +package test + +import ( + "fmt" + "io" + "os/exec" + "runtime" + "strings" + "sync" + + "github.com/gliderlabs/ssh" +) + +func HandlerSSH(s ssh.Session) { + cmd, stdin, stderr, stdout, err := buildCommand(s.Command()) + if err != nil { + fmt.Println(err) + return + } + + if err := cmd.Start(); err != nil { + fmt.Println(err) + return + } + + go func() { + defer stdin.Close() + io.Copy(stdin, s) + }() + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + io.Copy(s.Stderr(), stderr) + }() + + go func() { + defer wg.Done() + io.Copy(s, stdout) + }() + + wg.Wait() + + if err := cmd.Wait(); err != nil { + return + } + +} + +func buildCommand(c []string) (cmd *exec.Cmd, stdin io.WriteCloser, stderr, stdout io.ReadCloser, err error) { + if len(c) != 2 { + err = fmt.Errorf("invalid command") + return + } + + // fix for Windows environments + var path string + if runtime.GOOS == "windows" { + path = strings.Replace(c[1], "/C:/", "C:/", 1) + } else { + path = c[1] + } + + cmd = exec.Command(c[0], path) + stdout, err = cmd.StdoutPipe() + if err != nil { + return + } + + stdin, err = cmd.StdinPipe() + if err != nil { + return + } + + stderr, err = cmd.StderrPipe() + if err != nil { + return + } + + return +} diff --git a/plumbing/transport/ssh/proxy_test.go b/plumbing/transport/ssh/proxy_test.go index 3caf1ff..2ba98e8 100644 --- a/plumbing/transport/ssh/proxy_test.go +++ b/plumbing/transport/ssh/proxy_test.go @@ -1,36 +1,87 @@ package ssh import ( + "context" "fmt" "log" "net" "os" + "sync/atomic" "github.com/armon/go-socks5" + "github.com/gliderlabs/ssh" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/plumbing/transport/ssh/internal/test" + + fixtures "github.com/go-git/go-git-fixtures/v4" + stdssh "golang.org/x/crypto/ssh" . "gopkg.in/check.v1" ) type ProxySuite struct { - UploadPackSuite + u UploadPackSuite + fixtures.Suite } var _ = Suite(&ProxySuite{}) -func (s *ProxySuite) SetUpSuite(c *C) { - s.UploadPackSuite.SetUpSuite(c) +var socksProxiedRequests int32 - l, err := net.Listen("tcp", "localhost:0") +func (s *ProxySuite) TestCommand(c *C) { + socksListener, err := net.Listen("tcp", "localhost:0") c.Assert(err, IsNil) - server, err := socks5.New(&socks5.Config{}) + socksServer, err := socks5.New(&socks5.Config{ + AuthMethods: []socks5.Authenticator{socks5.UserPassAuthenticator{ + Credentials: socks5.StaticCredentials{ + "user": "pass", + }, + }}, + Rules: TestProxyRule{}, + }) c.Assert(err, IsNil) + go func() { + socksServer.Serve(socksListener) + }() + socksProxyAddr := fmt.Sprintf("socks5://localhost:%d", socksListener.Addr().(*net.TCPAddr).Port) - port := l.Addr().(*net.TCPAddr).Port - - err = os.Setenv("ALL_PROXY", fmt.Sprintf("socks5://localhost:%d", port)) + sshListener, err := net.Listen("tcp", "localhost:0") c.Assert(err, IsNil) - + sshServer := &ssh.Server{Handler: test.HandlerSSH} go func() { - log.Fatal(server.Serve(l)) + log.Fatal(sshServer.Serve(sshListener)) }() + + s.u.port = sshListener.Addr().(*net.TCPAddr).Port + s.u.base, err = os.MkdirTemp(os.TempDir(), fmt.Sprintf("go-git-ssh-%d", s.u.port)) + c.Assert(err, IsNil) + + DefaultAuthBuilder = func(user string) (AuthMethod, error) { + return &Password{User: user}, nil + } + + ep := s.u.prepareRepository(c, fixtures.Basic().One(), "basic.git") + c.Assert(err, IsNil) + ep.Proxy = transport.ProxyOptions{ + URL: socksProxyAddr, + Username: "user", + Password: "pass", + } + + runner := runner{ + config: &stdssh.ClientConfig{ + HostKeyCallback: stdssh.InsecureIgnoreHostKey(), + }, + } + _, err = runner.Command(transport.UploadPackServiceName, ep, nil) + c.Assert(err, IsNil) + proxyUsed := atomic.LoadInt32(&socksProxiedRequests) > 0 + c.Assert(proxyUsed, Equals, true) +} + +type TestProxyRule struct{} + +func (dr TestProxyRule) Allow(ctx context.Context, req *socks5.Request) (context.Context, bool) { + atomic.AddInt32(&socksProxiedRequests, 1) + return ctx, true } diff --git a/plumbing/transport/ssh/upload_pack_test.go b/plumbing/transport/ssh/upload_pack_test.go index e65e04a..67af566 100644 --- a/plumbing/transport/ssh/upload_pack_test.go +++ b/plumbing/transport/ssh/upload_pack_test.go @@ -3,7 +3,6 @@ package ssh import ( "fmt" "io" - "io/ioutil" "log" "net" "os" @@ -14,6 +13,7 @@ import ( "sync" "github.com/go-git/go-git/v5/plumbing/transport" + testutils "github.com/go-git/go-git/v5/plumbing/transport/ssh/internal/test" "github.com/go-git/go-git/v5/plumbing/transport/test" "github.com/gliderlabs/ssh" @@ -25,6 +25,7 @@ import ( type UploadPackSuite struct { test.UploadPackSuite fixtures.Suite + opts []ssh.Option port int base string @@ -41,7 +42,7 @@ func (s *UploadPackSuite) SetUpSuite(c *C) { c.Assert(err, IsNil) s.port = l.Addr().(*net.TCPAddr).Port - s.base, err = ioutil.TempDir(os.TempDir(), fmt.Sprintf("go-git-ssh-%d", s.port)) + s.base, err = os.MkdirTemp(os.TempDir(), fmt.Sprintf("go-git-ssh-%d", s.port)) c.Assert(err, IsNil) DefaultAuthBuilder = func(user string) (AuthMethod, error) { @@ -56,7 +57,10 @@ func (s *UploadPackSuite) SetUpSuite(c *C) { s.UploadPackSuite.EmptyEndpoint = s.prepareRepository(c, fixtures.ByTag("empty").One(), "empty.git") s.UploadPackSuite.NonExistentEndpoint = s.newEndpoint(c, "non-existent.git") - server := &ssh.Server{Handler: handlerSSH} + server := &ssh.Server{Handler: testutils.HandlerSSH} + for _, opt := range s.opts { + opt(server) + } go func() { log.Fatal(server.Serve(l)) }() |