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:
parent
7e191eb18b
commit
0f38da068c
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue