[Refactor] unexport config.Config var & move login tasks to task module (#288)

Unexport generateToken()

move CreateLogin into task

Create func config.SetDefaultLogin()

Unexport loadConfig() & saveConfig

unexport config var

make SetDefaultLogin() case insensitive

update func descriptions

move FindSSHKey to task module

Reviewed-on: https://gitea.com/gitea/tea/pulls/288
Reviewed-by: Norwin <noerw@noreply.gitea.io>
Reviewed-by: Andrew Thornton <art27@cantab.net>
Co-Authored-By: 6543 <6543@obermui.de>
Co-Committed-By: 6543 <6543@obermui.de>
This commit is contained in:
6543 2020-12-12 21:28:37 +08:00
parent eeb9cbafe7
commit c063329e9a
12 changed files with 226 additions and 175 deletions

View File

@ -39,10 +39,6 @@ func runLogins(ctx *cli.Context) error {
} }
func runLoginDetail(name string) error { func runLoginDetail(name string) error {
if err := config.LoadConfig(); err != nil {
return err
}
l := config.GetLoginByName(name) l := config.GetLoginByName(name)
if l == nil { if l == nil {
fmt.Printf("Login '%s' do not exist\n\n", name) fmt.Printf("Login '%s' do not exist\n\n", name)

View File

@ -5,8 +5,8 @@
package login package login
import ( import (
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/interact" "code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -70,7 +70,7 @@ func runLoginAdd(ctx *cli.Context) error {
} }
// else use args to add login // else use args to add login
return config.AddLogin( return task.CreateLogin(
ctx.String("name"), ctx.String("name"),
ctx.String("token"), ctx.String("token"),
ctx.String("user"), ctx.String("user"),

View File

@ -24,9 +24,6 @@ var CmdLoginSetDefault = cli.Command{
} }
func runLoginSetDefault(ctx *cli.Context) error { func runLoginSetDefault(ctx *cli.Context) error {
if err := config.LoadConfig(); err != nil {
return err
}
if ctx.Args().Len() == 0 { if ctx.Args().Len() == 0 {
l, err := config.GetDefaultLogin() l, err := config.GetDefaultLogin()
if err != nil { if err != nil {
@ -35,18 +32,7 @@ func runLoginSetDefault(ctx *cli.Context) error {
fmt.Printf("Default Login: %s\n", l.Name) fmt.Printf("Default Login: %s\n", l.Name)
return nil return nil
} }
loginExist := false
for i := range config.Config.Logins {
config.Config.Logins[i].Default = false
if config.Config.Logins[i].Name == ctx.Args().First() {
config.Config.Logins[i].Default = true
loginExist = true
}
}
if !loginExist { name := ctx.Args().First()
return fmt.Errorf("login '%s' not found", ctx.Args().First()) return config.SetDefaultLogin(name)
}
return config.SaveConfig()
} }

View File

@ -21,6 +21,6 @@ var CmdLoginEdit = cli.Command{
Flags: []cli.Flag{&flags.OutputFlag}, Flags: []cli.Flag{&flags.OutputFlag},
} }
func runLoginEdit(ctx *cli.Context) error { func runLoginEdit(_ *cli.Context) error {
return open.Start(config.GetConfigPath()) return open.Start(config.GetConfigPath())
} }

View File

@ -25,12 +25,12 @@ var CmdLoginList = cli.Command{
} }
// RunLoginList list all logins // RunLoginList list all logins
func RunLoginList(ctx *cli.Context) error { func RunLoginList(_ *cli.Context) error {
err := config.LoadConfig() logins, err := config.GetLogins()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
print.LoginsList(config.Config.Logins, flags.GlobalOutputValue) print.LoginsList(logins, flags.GlobalOutputValue)
return nil return nil
} }

View File

@ -29,7 +29,7 @@ var CmdLogout = cli.Command{
} }
func runLogout(ctx *cli.Context) error { func runLogout(ctx *cli.Context) error {
err := config.LoadConfig() logins, err := config.GetLogins()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -40,8 +40,8 @@ func runLogout(ctx *cli.Context) error {
name = ctx.String("name") name = ctx.String("name")
} else if len(ctx.Args().First()) != 0 { } else if len(ctx.Args().First()) != 0 {
name = ctx.Args().First() name = ctx.Args().First()
} else if len(config.Config.Logins) == 1 { } else if len(logins) == 1 {
name = config.Config.Logins[0].Name name = logins[0].Name
} else { } else {
return errors.New("Please specify a login name") return errors.New("Please specify a login name")
} }

View File

@ -21,7 +21,7 @@ import (
// the remotes of the .git repo specified in repoFlag or $PWD, and using overrides from // the remotes of the .git repo specified in repoFlag or $PWD, and using overrides from
// command flags. If a local git repo can't be found, repo slug values are unset. // command flags. If a local git repo can't be found, repo slug values are unset.
func InitCommand(repoFlag, loginFlag, remoteFlag string) (login *Login, owner string, reponame string) { func InitCommand(repoFlag, loginFlag, remoteFlag string) (login *Login, owner string, reponame string) {
err := LoadConfig() err := loadConfig()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -69,7 +69,7 @@ func InitCommand(repoFlag, loginFlag, remoteFlag string) (login *Login, owner st
return return
} }
// discovers login & repo slug from the default branch remote of the given local repo // contextFromLocalRepo discovers login & repo slug from the default branch remote of the given local repo
func contextFromLocalRepo(repoValue, remoteValue string) (*Login, string, error) { func contextFromLocalRepo(repoValue, remoteValue string) (*Login, string, error) {
repo, err := git.RepoFromPath(repoValue) repo, err := git.RepoFromPath(repoValue)
if err != nil { if err != nil {
@ -106,7 +106,7 @@ func contextFromLocalRepo(repoValue, remoteValue string) (*Login, string, error)
return nil, "", errors.New("Remote " + remoteValue + " not found in this Git repository") return nil, "", errors.New("Remote " + remoteValue + " not found in this Git repository")
} }
for _, l := range Config.Logins { for _, l := range config.Logins {
for _, u := range remoteConfig.URLs { for _, u := range remoteConfig.URLs {
p, err := git.ParseURL(strings.TrimSpace(u)) p, err := git.ParseURL(strings.TrimSpace(u))
if err != nil { if err != nil {

View File

@ -9,6 +9,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"path/filepath" "path/filepath"
"sync"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
@ -22,8 +23,9 @@ type LocalConfig struct {
} }
var ( var (
// Config contain if loaded local tea config // config contain if loaded local tea config
Config LocalConfig config LocalConfig
loadConfigOnce sync.Once
) )
// GetConfigPath return path to tea config file // GetConfigPath return path to tea config file
@ -53,29 +55,30 @@ func GetConfigPath() string {
return configFilePath return configFilePath
} }
// LoadConfig load config into global Config var // loadConfig load config from file
func LoadConfig() error { func loadConfig() (err error) {
ymlPath := GetConfigPath() loadConfigOnce.Do(func() {
exist, _ := utils.FileExist(ymlPath) ymlPath := GetConfigPath()
if exist { exist, _ := utils.FileExist(ymlPath)
bs, err := ioutil.ReadFile(ymlPath) if exist {
if err != nil { bs, err := ioutil.ReadFile(ymlPath)
return fmt.Errorf("Failed to read config file: %s", ymlPath) if err != nil {
} err = fmt.Errorf("Failed to read config file: %s", ymlPath)
}
err = yaml.Unmarshal(bs, &Config) err = yaml.Unmarshal(bs, &config)
if err != nil { if err != nil {
return fmt.Errorf("Failed to parse contents of config file: %s", ymlPath) err = fmt.Errorf("Failed to parse contents of config file: %s", ymlPath)
}
} }
} })
return
return nil
} }
// SaveConfig save config from global Config var into config file // saveConfig save config to file
func SaveConfig() error { func saveConfig() error {
ymlPath := GetConfigPath() ymlPath := GetConfigPath()
bs, err := yaml.Marshal(Config) bs, err := yaml.Marshal(config)
if err != nil { if err != nil {
return err return err
} }

View File

@ -6,21 +6,15 @@ 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" "strings"
"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
@ -39,55 +33,88 @@ type Login struct {
Created int64 `yaml:"created"` Created int64 `yaml:"created"`
} }
// GetLogins return all login available by config
func GetLogins() ([]Login, error) {
if err := loadConfig(); err != nil {
return nil, err
}
return config.Logins, nil
}
// GetDefaultLogin return the default login // GetDefaultLogin return the default login
func GetDefaultLogin() (*Login, error) { func GetDefaultLogin() (*Login, error) {
if len(Config.Logins) == 0 { if err := loadConfig(); err != nil {
return nil, err
}
if len(config.Logins) == 0 {
return nil, errors.New("No available login") return nil, errors.New("No available login")
} }
for _, l := range Config.Logins { for _, l := range config.Logins {
if l.Default { if l.Default {
return &l, nil return &l, nil
} }
} }
return &Config.Logins[0], nil return &config.Logins[0], nil
} }
// GetLoginByName get login by name // SetDefaultLogin set the default login by name (case insensitive)
func SetDefaultLogin(name string) error {
if err := loadConfig(); err != nil {
return err
}
loginExist := false
for i := range config.Logins {
config.Logins[i].Default = false
if strings.ToLower(config.Logins[i].Name) == strings.ToLower(name) {
config.Logins[i].Default = true
loginExist = true
}
}
if !loginExist {
return fmt.Errorf("login '%s' not found", name)
}
return saveConfig()
}
// GetLoginByName get login by name (case insensitive)
func GetLoginByName(name string) *Login { func GetLoginByName(name string) *Login {
for _, l := range Config.Logins { err := loadConfig()
if l.Name == name { if err != nil {
log.Fatal(err)
}
for _, l := range config.Logins {
if strings.ToLower(l.Name) == strings.ToLower(name) {
return &l return &l
} }
} }
return nil return nil
} }
// GenerateLoginName generates a name string based on instance URL & adds username if the result is not unique // GetLoginByToken get login by token
func GenerateLoginName(url, user string) (string, error) { func GetLoginByToken(token string) *Login {
parsedURL, err := utils.NormalizeURL(url) err := loadConfig()
if err != nil { if err != nil {
return "", err log.Fatal(err)
} }
name := parsedURL.Host
// append user name if login name already exists for _, l := range config.Logins {
if len(user) != 0 { if l.Token == token {
for _, l := range Config.Logins { return &l
if l.Name == name {
name += "_" + user
break
}
} }
} }
return nil
return name, nil
} }
// DeleteLogin delete a login by name // DeleteLogin delete a login by name from config
func DeleteLogin(name string) error { func DeleteLogin(name string) error {
var idx = -1 var idx = -1
for i, l := range Config.Logins { for i, l := range config.Logins {
if l.Name == name { if l.Name == name {
idx = i idx = i
break break
@ -97,9 +124,22 @@ func DeleteLogin(name string) error {
return fmt.Errorf("can not delete login '%s', does not exist", name) return fmt.Errorf("can not delete login '%s', does not exist", name)
} }
Config.Logins = append(Config.Logins[:idx], Config.Logins[idx+1:]...) config.Logins = append(config.Logins[:idx], config.Logins[idx+1:]...)
return SaveConfig() return saveConfig()
}
// AddLogin save a login to config
func AddLogin(login *Login) error {
if err := loadConfig(); err != nil {
return err
}
// save login to global var
config.Logins = append(config.Logins, *login)
// save login to config file
return saveConfig()
} }
// Client returns a client to operate Gitea API // Client returns a client to operate Gitea API
@ -138,65 +178,3 @@ 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

@ -8,7 +8,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/task"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
) )
@ -28,7 +28,7 @@ func CreateLogin() error {
return nil return nil
} }
name, err := config.GenerateLoginName(giteaURL, "") name, err := task.GenerateLoginName(giteaURL, "")
if err != nil { if err != nil {
return err return err
} }
@ -87,5 +87,5 @@ func CreateLogin() error {
} }
} }
return config.AddLogin(name, token, user, passwd, sshKey, giteaURL, insecure) return task.CreateLogin(name, token, user, passwd, sshKey, giteaURL, insecure)
} }

View File

@ -2,42 +2,35 @@
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package config package task
import ( import (
"fmt" "fmt"
"log" "log"
"os" "os"
"strings"
"time" "time"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
) )
// AddLogin add login to config ( global var & file) // CreateLogin create a login to be stored in config
func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool) error { func CreateLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool) error {
// checks ... // checks ...
// ... if we have a url // ... if we have a url
if len(giteaURL) == 0 { if len(giteaURL) == 0 {
log.Fatal("You have to input Gitea server URL") log.Fatal("You have to input Gitea server URL")
} }
err := LoadConfig() // ... if there already exist a login with same name
if err != nil { if login := config.GetLoginByName(name); login != nil {
log.Fatal(err) return fmt.Errorf("login name '%s' has already been used", login.Name)
} }
// ... if we already use this token
for _, l := range Config.Logins { if login := config.GetLoginByToken(token); login != nil {
// ... if there already exist a login with same name return fmt.Errorf("token already been used, delete login '%s' first", login.Name)
if strings.ToLower(l.Name) == strings.ToLower(name) {
return fmt.Errorf("login name '%s' has already been used", l.Name)
}
// ... if we already use this token
if l.Token == token {
return fmt.Errorf("token already been used, delete login '%s' first", l.Name)
}
} }
// .. if we have enough information to authenticate // .. if we have enough information to authenticate
@ -55,7 +48,7 @@ func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool)
log.Fatal("Unable to parse URL", err) log.Fatal("Unable to parse URL", err)
} }
login := Login{ login := config.Login{
Name: name, Name: name,
URL: serverURL.String(), URL: serverURL.String(),
Token: token, Token: token,
@ -64,15 +57,17 @@ func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool)
Created: time.Now().Unix(), Created: time.Now().Unix(),
} }
client := login.Client()
if len(token) == 0 { if len(token) == 0 {
login.Token, err = GenerateToken(login.Client(), user, passwd) login.Token, err = generateToken(client, user, passwd)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
// Verify if authentication works and get user info // Verify if authentication works and get user info
u, _, err := login.Client().GetMyUserInfo() u, _, err := client.GetMyUserInfo()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -90,17 +85,13 @@ func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool)
login.SSHHost = serverURL.Hostname() login.SSHHost = serverURL.Hostname()
if len(sshKey) == 0 { if len(sshKey) == 0 {
login.SSHKey, err = login.FindSSHKey() login.SSHKey, err = findSSHKey(client)
if err != nil { if err != nil {
fmt.Printf("Warning: problem while finding a SSH key: %s\n", err) fmt.Printf("Warning: problem while finding a SSH key: %s\n", err)
} }
} }
// save login to global var err = config.AddLogin(&login)
Config.Logins = append(Config.Logins, login)
// save login to config file
err = SaveConfig()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -110,8 +101,8 @@ func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool)
return nil return nil
} }
// GenerateToken creates a new token when given BasicAuth credentials // generateToken creates a new token when given BasicAuth credentials
func GenerateToken(client *gitea.Client, user, pass string) (string, error) { func generateToken(client *gitea.Client, user, pass string) (string, error) {
gitea.SetBasicAuth(user, pass)(client) gitea.SetBasicAuth(user, pass)(client)
host, _ := os.Hostname() host, _ := os.Hostname()
@ -131,3 +122,21 @@ func GenerateToken(client *gitea.Client, user, pass string) (string, error) {
t, _, err := client.CreateAccessToken(gitea.CreateAccessTokenOption{Name: tokenName}) t, _, err := client.CreateAccessToken(gitea.CreateAccessTokenOption{Name: tokenName})
return t.Token, err return t.Token, err
} }
// GenerateLoginName generates a name string based on instance URL & adds username if the result is not unique
func GenerateLoginName(url, user string) (string, error) {
parsedURL, err := utils.NormalizeURL(url)
if err != nil {
return "", err
}
name := parsedURL.Host
// append user name if login name already exists
if len(user) != 0 {
if login := config.GetLoginByName(name); login != nil {
return name + "_" + user, nil
}
}
return name, nil
}

79
modules/task/login_ssh.go Normal file
View File

@ -0,0 +1,79 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package task
import (
"encoding/base64"
"io/ioutil"
"path/filepath"
"strings"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"golang.org/x/crypto/ssh"
)
// 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 findSSHKey(client *gitea.Client) (string, error) {
// get keys registered on gitea instance
keys, _, err := 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
}