diff --git a/cmd/login/add.go b/cmd/login/add.go index ca53c9c..9beed16 100644 --- a/cmd/login/add.go +++ b/cmd/login/add.go @@ -52,7 +52,7 @@ var CmdLoginAdd = cli.Command{ &cli.StringFlag{ Name: "ssh-key", Aliases: []string{"s"}, - Usage: "Path to a SSH key to use for pull/push operations", + Usage: "Path to a SSH key to use, overrides auto-discovery", }, &cli.BoolFlag{ Name: "insecure", diff --git a/modules/config/login.go b/modules/config/login.go index c347533..4ed47bc 100644 --- a/modules/config/login.go +++ b/modules/config/login.go @@ -6,16 +6,21 @@ package config import ( "crypto/tls" + "encoding/base64" "errors" "fmt" + "io/ioutil" "log" "net/http" "net/http/cookiejar" "net/url" + "path/filepath" + "strings" "code.gitea.io/tea/modules/utils" "code.gitea.io/sdk/gitea" + "golang.org/x/crypto/ssh" ) // Login represents a login to a gitea server, you even could add multiple logins for one gitea server @@ -133,3 +138,65 @@ func (l *Login) GetSSHHost() string { return u.Hostname() } + +// FindSSHKey retrieves the ssh keys registered in gitea, and tries to find +// a matching private key in ~/.ssh/. If no match is found, path is empty. +func (l *Login) FindSSHKey() (string, error) { + // get keys registered on gitea instance + keys, _, err := l.Client().ListMyPublicKeys(gitea.ListPublicKeysOptions{}) + if err != nil || len(keys) == 0 { + return "", err + } + + // enumerate ~/.ssh/*.pub files + glob, err := utils.AbsPathWithExpansion("~/.ssh/*.pub") + if err != nil { + return "", err + } + localPubkeyPaths, err := filepath.Glob(glob) + if err != nil { + return "", err + } + + // parse each local key with present privkey & compare fingerprints to online keys + for _, pubkeyPath := range localPubkeyPaths { + var pubkeyFile []byte + pubkeyFile, err = ioutil.ReadFile(pubkeyPath) + if err != nil { + continue + } + fields := strings.Split(string(pubkeyFile), " ") + if len(fields) < 2 { // first word is key type, second word is key material + continue + } + + var keymaterial []byte + keymaterial, err = base64.StdEncoding.DecodeString(fields[1]) + if err != nil { + continue + } + + var pubkey ssh.PublicKey + pubkey, err = ssh.ParsePublicKey(keymaterial) + if err != nil { + continue + } + + privkeyPath := strings.TrimSuffix(pubkeyPath, ".pub") + var exists bool + exists, err = utils.FileExist(privkeyPath) + if err != nil || !exists { + continue + } + + // if pubkey fingerprints match, return path to corresponding privkey. + fingerprint := ssh.FingerprintSHA256(pubkey) + for _, key := range keys { + if fingerprint == key.Fingerprint { + return privkeyPath, nil + } + } + } + + return "", err +} diff --git a/modules/config/login_tasks.go b/modules/config/login_tasks.go index 3afce60..7a61565 100644 --- a/modules/config/login_tasks.go +++ b/modules/config/login_tasks.go @@ -89,6 +89,13 @@ func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool) // so we just use the hostname login.SSHHost = serverURL.Hostname() + if len(sshKey) == 0 { + login.SSHKey, err = login.FindSSHKey() + if err != nil { + fmt.Printf("Warning: problem while finding a SSH key: %s\n", err) + } + } + // save login to global var Config.Logins = append(Config.Logins, login) diff --git a/modules/git/auth.go b/modules/git/auth.go index 7b78bad..2334ab7 100644 --- a/modules/git/auth.go +++ b/modules/git/auth.go @@ -22,29 +22,26 @@ type pwCallback = func(string) (string, error) // GetAuthForURL returns the appropriate AuthMethod to be used in Push() / Pull() // operations depending on the protocol, and prompts the user for credentials if // necessary. -func GetAuthForURL(remoteURL *url.URL, authToken, keyFile string, passwordCallback pwCallback) (auth git_transport.AuthMethod, err error) { +func GetAuthForURL(remoteURL *url.URL, authToken, keyFile string, passwordCallback pwCallback) (git_transport.AuthMethod, error) { switch remoteURL.Scheme { case "http", "https": // gitea supports push/pull via app token as username. - auth = &gogit_http.BasicAuth{Password: "", Username: authToken} + return &gogit_http.BasicAuth{Password: "", Username: authToken}, nil case "ssh": // try to select right key via ssh-agent. if it fails, try to read a key manually user := remoteURL.User.Username() - auth, err = gogit_ssh.DefaultAuthBuilder(user) - if err != nil && passwordCallback != nil { + auth, err := gogit_ssh.DefaultAuthBuilder(user) + if err != nil { signer, err2 := readSSHPrivKey(keyFile, passwordCallback) if err2 != nil { return nil, err2 } auth = &gogit_ssh.PublicKeys{User: user, Signer: signer} } - - default: - return nil, fmt.Errorf("don't know how to handle url scheme %v", remoteURL.Scheme) + return auth, nil } - - return + return nil, fmt.Errorf("don't know how to handle url scheme %v", remoteURL.Scheme) } func readSSHPrivKey(keyFile string, passwordCallback pwCallback) (sig ssh.Signer, err error) { @@ -61,7 +58,7 @@ func readSSHPrivKey(keyFile string, passwordCallback pwCallback) (sig ssh.Signer return nil, err } sig, err = ssh.ParsePrivateKey(sshKey) - if _, ok := err.(*ssh.PassphraseMissingError); ok { + if _, ok := err.(*ssh.PassphraseMissingError); ok && passwordCallback != nil { // allow for up to 3 password attempts for i := 0; i < 3; i++ { var pass string diff --git a/modules/interact/login.go b/modules/interact/login.go index 27d6b26..2ada533 100644 --- a/modules/interact/login.go +++ b/modules/interact/login.go @@ -73,7 +73,7 @@ func CreateLogin() error { return err } if optSettings { - promptI = &survey.Input{Message: "SSH Key Path: "} + promptI = &survey.Input{Message: "SSH Key Path (leave empty for auto-discovery):"} if err := survey.AskOne(promptI, &sshKey); err != nil { return err } diff --git a/modules/task/pull_checkout.go b/modules/task/pull_checkout.go index 14e48e0..92262a6 100644 --- a/modules/task/pull_checkout.go +++ b/modules/task/pull_checkout.go @@ -7,7 +7,6 @@ package task import ( "fmt" - "code.gitea.io/sdk/gitea" "code.gitea.io/tea/modules/config" local_git "code.gitea.io/tea/modules/git" @@ -29,13 +28,11 @@ func PullCheckout(login *config.Login, repoOwner, repoName string, index int64, return err } - // test if we can pull via SSH, and configure git remote accordingly remoteURL := pr.Head.Repository.CloneURL - keys, _, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{}) - if err != nil { - return err - } - if len(keys) != 0 { + if len(login.SSHKey) != 0 { + // login.SSHKey is nonempty, if user specified a key manually or we automatically + // found a matching private key on this machine during login creation. + // this means, we are very likely to have a working ssh setup. remoteURL = pr.Head.Repository.SSHURL } @@ -54,9 +51,8 @@ func PullCheckout(login *config.Login, repoOwner, repoName string, index int64, } localRemoteName := localRemote.Config().Name - // get auth & fetch remote - fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n", index, remoteURL, pr.Head.Ref, localRemoteName) - url, err := local_git.ParseURL(remoteURL) + // get auth & fetch remote via its configured protocol + url, err := localRepo.TeaRemoteURL(localRemoteName) if err != nil { return err } @@ -64,6 +60,7 @@ func PullCheckout(login *config.Login, repoOwner, repoName string, index int64, if err != nil { return err } + fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n", index, url, pr.Head.Ref, localRemoteName) err = localRemote.Fetch(&git.FetchOptions{Auth: auth}) if err == git.NoErrAlreadyUpToDate { fmt.Println(err) @@ -72,9 +69,10 @@ func PullCheckout(login *config.Login, repoOwner, repoName string, index int64, } // checkout local branch - fmt.Printf("Creating branch '%s'\n", localBranchName) err = localRepo.TeaCreateBranch(localBranchName, pr.Head.Ref, localRemoteName) - if err == git.ErrBranchExists { + if err == nil { + fmt.Printf("Created branch '%s'\n", localBranchName) + } else if err == git.ErrBranchExists { fmt.Println("There may be changes since you last checked out, run `git pull` to get them.") } else if err != nil { return err