From ead30c5b454faf2e79951e0967a0dc5afc8ecc7c Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 13 Apr 2024 15:01:34 +0200 Subject: fix ssh authenticate --- repository/gogit.go | 82 +++++++++++++++++++++++++++++++++++++++++++++--- repository/gogit_test.go | 46 +++++++++++++++++++++++++++ repository/mock_repo.go | 5 +++ repository/repo.go | 4 +++ 4 files changed, 133 insertions(+), 4 deletions(-) diff --git a/repository/gogit.go b/repository/gogit.go index 96d62665..a271ab8b 100644 --- a/repository/gogit.go +++ b/repository/gogit.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "os" "path/filepath" + "regexp" "sort" "strings" "sync" @@ -21,6 +22,7 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" "golang.org/x/sync/errgroup" "golang.org/x/sys/execabs" @@ -383,14 +385,31 @@ func (repo *GoGitRepo) FetchRefs(remote string, prefixes ...string) (string, err buf := bytes.NewBuffer(nil) - err := repo.r.Fetch(&gogit.FetchOptions{ + fetchOptions := &gogit.FetchOptions{ RemoteName: remote, RefSpecs: refSpecs, Progress: buf, - }) + } + + publicKeys, err := repo.SSHAuth(remote) + if err != nil { + return "", err + } + + err = repo.r.Fetch(fetchOptions) if err == gogit.NoErrAlreadyUpToDate { return "already up-to-date", nil } + // ssh-agent is required if ssh or scp-like url is configured in repository config + // we can not fetch if the ssh-agent has invalid keys or ssh-agent is not running + // retry to fetch again if we can retreive public keys from the users home directory + if err != nil && publicKeys != nil { + fetchOptions.Auth = publicKeys + err = repo.r.Fetch(fetchOptions) + if err == gogit.NoErrAlreadyUpToDate { + return "already up-to-date", nil + } + } if err != nil { return "", err } @@ -436,14 +455,31 @@ func (repo *GoGitRepo) PushRefs(remote string, prefixes ...string) (string, erro buf := bytes.NewBuffer(nil) - err = remo.Push(&gogit.PushOptions{ + pushOptions := &gogit.PushOptions{ RemoteName: remote, RefSpecs: refSpecs, Progress: buf, - }) + } + + publicKeys, err := repo.SSHAuth(remote) + if err != nil { + return "", err + } + + err = repo.r.Push(pushOptions) if err == gogit.NoErrAlreadyUpToDate { return "already up-to-date", nil } + // ssh-agent is required if ssh or scp-like url is configured in repository config + // we can not push if the ssh-agent has invalid keys or ssh-agent is not running + // retry to push again if we can retreive public keys from the users home directory + if err != nil && publicKeys != nil { + pushOptions.Auth = publicKeys + err = repo.r.Push(pushOptions) + if err == gogit.NoErrAlreadyUpToDate { + return "already up-to-date", nil + } + } if err != nil { return "", err } @@ -451,6 +487,44 @@ func (repo *GoGitRepo) PushRefs(remote string, prefixes ...string) (string, erro return buf.String(), nil } +// SSHAuth will attempt to read public keys for SSH auth +// if the repository remote contains a ssh or scp-like url +func (repo *GoGitRepo) SSHAuth(remote string) (*ssh.PublicKeys, error) { + // get the repository config + config, err := repo.r.Config() + if err != nil { + return nil, err + } + + // check if the repository config has at least one remote url + remotes, found := config.Remotes[remote] + if !found || len(remotes.URLs) < 1 { + return nil, fmt.Errorf("remote %s url not found in repository config", remote) + } + + schemeRegexp := regexp.MustCompile(`^[^:]+://`) + scpLikeRegexp := regexp.MustCompile(`^(?:(?P[^@]+)@)?(?P)`) + + // try to load public keys from the users home directory + // if the repository remote contains a ssh or scp-like url + if strings.HasPrefix(remotes.URLs[0], "ssh://") || (scpLikeRegexp.MatchString(remotes.URLs[0]) && !schemeRegexp.MatchString(remotes.URLs[0])) { + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + + // try to find and load valid public keys from the users home directory + attemptKeys := []string{"id_rsa", "id_ecdsa", "id_ecdsa_sk", "id_ed25519", "id_ed25519_sk", "id_xmss", "id_dsa"} + for _, key := range attemptKeys { + authMethod, err := ssh.NewPublicKeysFromFile("git", filepath.Join(home, ".ssh", key), "") + if err == nil { + return authMethod, nil + } + } + } + return nil, nil +} + // StoreData will store arbitrary data and return the corresponding hash func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) { obj := repo.r.Storer.NewEncodedObject() diff --git a/repository/gogit_test.go b/repository/gogit_test.go index 21acd5df..dff94605 100644 --- a/repository/gogit_test.go +++ b/repository/gogit_test.go @@ -2,6 +2,7 @@ package repository import ( "fmt" + "log" "os" "path" "path/filepath" @@ -96,3 +97,48 @@ func TestGoGit_DetectsSubmodules(t *testing.T) { assert.Empty(t, err) assert.Equal(t, expected, result) } + +func TestGoGitRepoSSH(t *testing.T) { + repo := CreateGoGitTestRepo(t, false) + + err := repo.AddRemote("ssh", "ssh://git@github.com:MichaelMure/git-bug.git") + if err != nil { + log.Fatal(err) + } + keys, err := repo.SSHAuth("ssh") + require.NotNil(t, keys) + require.Empty(t, err) + + err = repo.AddRemote("http", "http://github.com/MichaelMure/git-bug.git") + if err != nil { + log.Fatal(err) + } + keys, err = repo.SSHAuth("http") + require.Nil(t, keys) + require.Empty(t, err) + + err = repo.AddRemote("https", "https://github.com/MichaelMure/git-bug.git") + if err != nil { + log.Fatal(err) + } + keys, err = repo.SSHAuth("https") + require.Nil(t, keys) + require.Empty(t, err) + + err = repo.AddRemote("git", "git://github.com/MichaelMure/git-bug.git") + if err != nil { + log.Fatal(err) + } + keys, err = repo.SSHAuth("git") + require.Nil(t, keys) + require.Empty(t, err) + + err = repo.AddRemote("scp-like", "git@github.com:MichaelMure/git-bug.git") + if err != nil { + log.Fatal(err) + } + keys, err = repo.SSHAuth("scp-like") + require.NotNil(t, keys) + require.Empty(t, err) + +} diff --git a/repository/mock_repo.go b/repository/mock_repo.go index 6ea5c71e..a4095fec 100644 --- a/repository/mock_repo.go +++ b/repository/mock_repo.go @@ -10,6 +10,7 @@ import ( "github.com/99designs/keyring" "github.com/ProtonMail/go-crypto/openpgp" "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -252,6 +253,10 @@ func (r *mockRepoData) PushRefs(remote string, prefixes ...string) (string, erro panic("implement me") } +func (r *mockRepoData) SSHAuth(remote string) (*ssh.PublicKeys, error) { + panic("implement me") +} + func (r *mockRepoData) StoreData(data []byte) (Hash, error) { rawHash := sha1.Sum(data) hash := Hash(fmt.Sprintf("%x", rawHash)) diff --git a/repository/repo.go b/repository/repo.go index c39051d5..49f5a97b 100644 --- a/repository/repo.go +++ b/repository/repo.go @@ -7,6 +7,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp" "github.com/go-git/go-billy/v5" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" "github.com/MichaelMure/git-bug/util/lamport" ) @@ -142,6 +143,9 @@ type RepoData interface { // the remote state. PushRefs(remote string, prefixes ...string) (string, error) + // SSHAuth will attempt to read public keys for SSH auth + SSHAuth(remote string) (*ssh.PublicKeys, error) + // StoreData will store arbitrary data and return the corresponding hash StoreData(data []byte) (Hash, error) -- cgit