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{
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",

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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