Improve ssh handling (#277)

checkout: use configured protocol for PR checkout

instead of defaulting to ssh if that is enabled
this might fix #262

login add: try to find a matching ssh key & store it in config

possibly expensive operation should be done once

pr checkout: don't fetch ssh keys

As a result, we don't try to pull via ssh, if no privkey was configured.
This increases chances of a using ssh only on a working ssh setup.

fix import order

remove debug print statement

improve ssh-key value docs

rm named return & fix pwCallback nil check

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/277
Reviewed-by: khmarbaise <khmarbaise@noreply.gitea.io>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Norwin <noerw@noreply.gitea.io>
Co-Committed-By: Norwin <noerw@noreply.gitea.io>
This commit is contained in:
Norwin 2020-12-11 21:42:41 +08:00 committed by 6543
parent 7e191eb18b
commit 0f38da068c
6 changed files with 93 additions and 24 deletions

View File

@ -52,7 +52,7 @@ var CmdLoginAdd = cli.Command{
&cli.StringFlag{ &cli.StringFlag{
Name: "ssh-key", Name: "ssh-key",
Aliases: []string{"s"}, 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{ &cli.BoolFlag{
Name: "insecure", Name: "insecure",

View File

@ -6,16 +6,21 @@ package config
import ( import (
"crypto/tls" "crypto/tls"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
"path/filepath"
"strings"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea" "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 // 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() 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
}

View File

@ -89,6 +89,13 @@ func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool)
// so we just use the hostname // so we just use the hostname
login.SSHHost = serverURL.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 // save login to global var
Config.Logins = append(Config.Logins, login) Config.Logins = append(Config.Logins, login)

View File

@ -22,29 +22,26 @@ type pwCallback = func(string) (string, error)
// GetAuthForURL returns the appropriate AuthMethod to be used in Push() / Pull() // GetAuthForURL returns the appropriate AuthMethod to be used in Push() / Pull()
// operations depending on the protocol, and prompts the user for credentials if // operations depending on the protocol, and prompts the user for credentials if
// necessary. // 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 { switch remoteURL.Scheme {
case "http", "https": case "http", "https":
// gitea supports push/pull via app token as username. // 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": case "ssh":
// try to select right key via ssh-agent. if it fails, try to read a key manually // try to select right key via ssh-agent. if it fails, try to read a key manually
user := remoteURL.User.Username() user := remoteURL.User.Username()
auth, err = gogit_ssh.DefaultAuthBuilder(user) auth, err := gogit_ssh.DefaultAuthBuilder(user)
if err != nil && passwordCallback != nil { if err != nil {
signer, err2 := readSSHPrivKey(keyFile, passwordCallback) signer, err2 := readSSHPrivKey(keyFile, passwordCallback)
if err2 != nil { if err2 != nil {
return nil, err2 return nil, err2
} }
auth = &gogit_ssh.PublicKeys{User: user, Signer: signer} auth = &gogit_ssh.PublicKeys{User: user, Signer: signer}
} }
return auth, nil
default:
return nil, fmt.Errorf("don't know how to handle url scheme %v", remoteURL.Scheme)
} }
return nil, fmt.Errorf("don't know how to handle url scheme %v", remoteURL.Scheme)
return
} }
func readSSHPrivKey(keyFile string, passwordCallback pwCallback) (sig ssh.Signer, err error) { 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 return nil, err
} }
sig, err = ssh.ParsePrivateKey(sshKey) 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 // allow for up to 3 password attempts
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
var pass string var pass string

View File

@ -73,7 +73,7 @@ func CreateLogin() error {
return err return err
} }
if optSettings { 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 { if err := survey.AskOne(promptI, &sshKey); err != nil {
return err return err
} }

View File

@ -7,7 +7,6 @@ package task
import ( import (
"fmt" "fmt"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/config"
local_git "code.gitea.io/tea/modules/git" local_git "code.gitea.io/tea/modules/git"
@ -29,13 +28,11 @@ func PullCheckout(login *config.Login, repoOwner, repoName string, index int64,
return err return err
} }
// test if we can pull via SSH, and configure git remote accordingly
remoteURL := pr.Head.Repository.CloneURL remoteURL := pr.Head.Repository.CloneURL
keys, _, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{}) if len(login.SSHKey) != 0 {
if err != nil { // login.SSHKey is nonempty, if user specified a key manually or we automatically
return err // found a matching private key on this machine during login creation.
} // this means, we are very likely to have a working ssh setup.
if len(keys) != 0 {
remoteURL = pr.Head.Repository.SSHURL remoteURL = pr.Head.Repository.SSHURL
} }
@ -54,9 +51,8 @@ func PullCheckout(login *config.Login, repoOwner, repoName string, index int64,
} }
localRemoteName := localRemote.Config().Name localRemoteName := localRemote.Config().Name
// get auth & fetch remote // get auth & fetch remote via its configured protocol
fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n", index, remoteURL, pr.Head.Ref, localRemoteName) url, err := localRepo.TeaRemoteURL(localRemoteName)
url, err := local_git.ParseURL(remoteURL)
if err != nil { if err != nil {
return err return err
} }
@ -64,6 +60,7 @@ func PullCheckout(login *config.Login, repoOwner, repoName string, index int64,
if err != nil { if err != nil {
return err 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}) err = localRemote.Fetch(&git.FetchOptions{Auth: auth})
if err == git.NoErrAlreadyUpToDate { if err == git.NoErrAlreadyUpToDate {
fmt.Println(err) fmt.Println(err)
@ -72,9 +69,10 @@ func PullCheckout(login *config.Login, repoOwner, repoName string, index int64,
} }
// checkout local branch // checkout local branch
fmt.Printf("Creating branch '%s'\n", localBranchName)
err = localRepo.TeaCreateBranch(localBranchName, pr.Head.Ref, localRemoteName) 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.") fmt.Println("There may be changes since you last checked out, run `git pull` to get them.")
} else if err != nil { } else if err != nil {
return err return err