From c063329e9a122962a0d3d2eba155b5b19f3a78b6 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Sat, 12 Dec 2020 21:28:37 +0800 Subject: [PATCH] [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 Reviewed-by: Andrew Thornton Co-Authored-By: 6543 <6543@obermui.de> Co-Committed-By: 6543 <6543@obermui.de> --- cmd/login.go | 4 - cmd/login/add.go | 4 +- cmd/login/default.go | 18 +- cmd/login/edit.go | 2 +- cmd/login/list.go | 6 +- cmd/logout.go | 6 +- modules/config/command.go | 6 +- modules/config/config.go | 43 ++--- modules/config/login.go | 162 ++++++++---------- modules/interact/login.go | 6 +- .../login_tasks.go => task/login_create.go} | 65 ++++--- modules/task/login_ssh.go | 79 +++++++++ 12 files changed, 226 insertions(+), 175 deletions(-) rename modules/{config/login_tasks.go => task/login_create.go} (61%) create mode 100644 modules/task/login_ssh.go diff --git a/cmd/login.go b/cmd/login.go index 2461b39..c5d5fd8 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -39,10 +39,6 @@ func runLogins(ctx *cli.Context) error { } func runLoginDetail(name string) error { - if err := config.LoadConfig(); err != nil { - return err - } - l := config.GetLoginByName(name) if l == nil { fmt.Printf("Login '%s' do not exist\n\n", name) diff --git a/cmd/login/add.go b/cmd/login/add.go index 9beed16..56799c3 100644 --- a/cmd/login/add.go +++ b/cmd/login/add.go @@ -5,8 +5,8 @@ package login import ( - "code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/interact" + "code.gitea.io/tea/modules/task" "github.com/urfave/cli/v2" ) @@ -70,7 +70,7 @@ func runLoginAdd(ctx *cli.Context) error { } // else use args to add login - return config.AddLogin( + return task.CreateLogin( ctx.String("name"), ctx.String("token"), ctx.String("user"), diff --git a/cmd/login/default.go b/cmd/login/default.go index 7e8e60f..fb1dfe5 100644 --- a/cmd/login/default.go +++ b/cmd/login/default.go @@ -24,9 +24,6 @@ var CmdLoginSetDefault = cli.Command{ } func runLoginSetDefault(ctx *cli.Context) error { - if err := config.LoadConfig(); err != nil { - return err - } if ctx.Args().Len() == 0 { l, err := config.GetDefaultLogin() if err != nil { @@ -35,18 +32,7 @@ func runLoginSetDefault(ctx *cli.Context) error { fmt.Printf("Default Login: %s\n", l.Name) 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 { - return fmt.Errorf("login '%s' not found", ctx.Args().First()) - } - - return config.SaveConfig() + name := ctx.Args().First() + return config.SetDefaultLogin(name) } diff --git a/cmd/login/edit.go b/cmd/login/edit.go index c8ffd1e..0f06a5d 100644 --- a/cmd/login/edit.go +++ b/cmd/login/edit.go @@ -21,6 +21,6 @@ var CmdLoginEdit = cli.Command{ Flags: []cli.Flag{&flags.OutputFlag}, } -func runLoginEdit(ctx *cli.Context) error { +func runLoginEdit(_ *cli.Context) error { return open.Start(config.GetConfigPath()) } diff --git a/cmd/login/list.go b/cmd/login/list.go index 2660f90..ca03c4c 100644 --- a/cmd/login/list.go +++ b/cmd/login/list.go @@ -25,12 +25,12 @@ var CmdLoginList = cli.Command{ } // RunLoginList list all logins -func RunLoginList(ctx *cli.Context) error { - err := config.LoadConfig() +func RunLoginList(_ *cli.Context) error { + logins, err := config.GetLogins() if err != nil { log.Fatal(err) } - print.LoginsList(config.Config.Logins, flags.GlobalOutputValue) + print.LoginsList(logins, flags.GlobalOutputValue) return nil } diff --git a/cmd/logout.go b/cmd/logout.go index 8880480..4aeae80 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -29,7 +29,7 @@ var CmdLogout = cli.Command{ } func runLogout(ctx *cli.Context) error { - err := config.LoadConfig() + logins, err := config.GetLogins() if err != nil { log.Fatal(err) } @@ -40,8 +40,8 @@ func runLogout(ctx *cli.Context) error { name = ctx.String("name") } else if len(ctx.Args().First()) != 0 { name = ctx.Args().First() - } else if len(config.Config.Logins) == 1 { - name = config.Config.Logins[0].Name + } else if len(logins) == 1 { + name = logins[0].Name } else { return errors.New("Please specify a login name") } diff --git a/modules/config/command.go b/modules/config/command.go index 46a5ce8..5accb64 100644 --- a/modules/config/command.go +++ b/modules/config/command.go @@ -21,7 +21,7 @@ import ( // 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. func InitCommand(repoFlag, loginFlag, remoteFlag string) (login *Login, owner string, reponame string) { - err := LoadConfig() + err := loadConfig() if err != nil { log.Fatal(err) } @@ -69,7 +69,7 @@ func InitCommand(repoFlag, loginFlag, remoteFlag string) (login *Login, owner st 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) { repo, err := git.RepoFromPath(repoValue) 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") } - for _, l := range Config.Logins { + for _, l := range config.Logins { for _, u := range remoteConfig.URLs { p, err := git.ParseURL(strings.TrimSpace(u)) if err != nil { diff --git a/modules/config/config.go b/modules/config/config.go index 407ead6..74c7b5a 100644 --- a/modules/config/config.go +++ b/modules/config/config.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "log" "path/filepath" + "sync" "code.gitea.io/tea/modules/utils" @@ -22,8 +23,9 @@ type LocalConfig struct { } var ( - // Config contain if loaded local tea config - Config LocalConfig + // config contain if loaded local tea config + config LocalConfig + loadConfigOnce sync.Once ) // GetConfigPath return path to tea config file @@ -53,29 +55,30 @@ func GetConfigPath() string { return configFilePath } -// LoadConfig load config into global Config var -func LoadConfig() error { - ymlPath := GetConfigPath() - exist, _ := utils.FileExist(ymlPath) - if exist { - bs, err := ioutil.ReadFile(ymlPath) - if err != nil { - return fmt.Errorf("Failed to read config file: %s", ymlPath) - } +// loadConfig load config from file +func loadConfig() (err error) { + loadConfigOnce.Do(func() { + ymlPath := GetConfigPath() + exist, _ := utils.FileExist(ymlPath) + if exist { + bs, err := ioutil.ReadFile(ymlPath) + if err != nil { + err = fmt.Errorf("Failed to read config file: %s", ymlPath) + } - err = yaml.Unmarshal(bs, &Config) - if err != nil { - return fmt.Errorf("Failed to parse contents of config file: %s", ymlPath) + err = yaml.Unmarshal(bs, &config) + if err != nil { + err = fmt.Errorf("Failed to parse contents of config file: %s", ymlPath) + } } - } - - return nil + }) + return } -// SaveConfig save config from global Config var into config file -func SaveConfig() error { +// saveConfig save config to file +func saveConfig() error { ymlPath := GetConfigPath() - bs, err := yaml.Marshal(Config) + bs, err := yaml.Marshal(config) if err != nil { return err } diff --git a/modules/config/login.go b/modules/config/login.go index 4ed47bc..0c70edb 100644 --- a/modules/config/login.go +++ b/modules/config/login.go @@ -6,21 +6,15 @@ 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 @@ -39,55 +33,88 @@ type Login struct { 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 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") } - for _, l := range Config.Logins { + for _, l := range config.Logins { if l.Default { 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 { - for _, l := range Config.Logins { - if l.Name == name { + err := loadConfig() + if err != nil { + log.Fatal(err) + } + + for _, l := range config.Logins { + if strings.ToLower(l.Name) == strings.ToLower(name) { return &l } } return nil } -// 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) +// GetLoginByToken get login by token +func GetLoginByToken(token string) *Login { + err := loadConfig() if err != nil { - return "", err + log.Fatal(err) } - name := parsedURL.Host - // append user name if login name already exists - if len(user) != 0 { - for _, l := range Config.Logins { - if l.Name == name { - name += "_" + user - break - } + for _, l := range config.Logins { + if l.Token == token { + return &l } } - - return name, nil + return nil } -// DeleteLogin delete a login by name +// DeleteLogin delete a login by name from config func DeleteLogin(name string) error { var idx = -1 - for i, l := range Config.Logins { + for i, l := range config.Logins { if l.Name == name { idx = i break @@ -97,9 +124,22 @@ func DeleteLogin(name string) error { 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 @@ -138,65 +178,3 @@ 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/interact/login.go b/modules/interact/login.go index 2ada533..1520114 100644 --- a/modules/interact/login.go +++ b/modules/interact/login.go @@ -8,7 +8,7 @@ import ( "fmt" "strings" - "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/task" "github.com/AlecAivazis/survey/v2" ) @@ -28,7 +28,7 @@ func CreateLogin() error { return nil } - name, err := config.GenerateLoginName(giteaURL, "") + name, err := task.GenerateLoginName(giteaURL, "") if err != nil { 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) } diff --git a/modules/config/login_tasks.go b/modules/task/login_create.go similarity index 61% rename from modules/config/login_tasks.go rename to modules/task/login_create.go index 7a61565..bb3ccaa 100644 --- a/modules/config/login_tasks.go +++ b/modules/task/login_create.go @@ -2,42 +2,35 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package config +package task import ( "fmt" "log" "os" - "strings" "time" + "code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/utils" "code.gitea.io/sdk/gitea" ) -// AddLogin add login to config ( global var & file) -func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool) error { +// CreateLogin create a login to be stored in config +func CreateLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool) error { // checks ... // ... if we have a url if len(giteaURL) == 0 { log.Fatal("You have to input Gitea server URL") } - err := LoadConfig() - if err != nil { - log.Fatal(err) + // ... if there already exist a login with same name + if login := config.GetLoginByName(name); login != nil { + return fmt.Errorf("login name '%s' has already been used", login.Name) } - - for _, l := range Config.Logins { - // ... if there already exist a login with same 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 already use this token + if login := config.GetLoginByToken(token); login != nil { + return fmt.Errorf("token already been used, delete login '%s' first", login.Name) } // .. 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) } - login := Login{ + login := config.Login{ Name: name, URL: serverURL.String(), Token: token, @@ -64,15 +57,17 @@ func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool) Created: time.Now().Unix(), } + client := login.Client() + if len(token) == 0 { - login.Token, err = GenerateToken(login.Client(), user, passwd) + login.Token, err = generateToken(client, user, passwd) if err != nil { log.Fatal(err) } } // Verify if authentication works and get user info - u, _, err := login.Client().GetMyUserInfo() + u, _, err := client.GetMyUserInfo() if err != nil { log.Fatal(err) } @@ -90,17 +85,13 @@ func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool) login.SSHHost = serverURL.Hostname() if len(sshKey) == 0 { - login.SSHKey, err = login.FindSSHKey() + login.SSHKey, err = findSSHKey(client) 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) - - // save login to config file - err = SaveConfig() + err = config.AddLogin(&login) if err != nil { log.Fatal(err) } @@ -110,8 +101,8 @@ func AddLogin(name, token, user, passwd, sshKey, giteaURL string, insecure bool) return nil } -// GenerateToken creates a new token when given BasicAuth credentials -func GenerateToken(client *gitea.Client, user, pass string) (string, error) { +// generateToken creates a new token when given BasicAuth credentials +func generateToken(client *gitea.Client, user, pass string) (string, error) { gitea.SetBasicAuth(user, pass)(client) 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}) 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 +} diff --git a/modules/task/login_ssh.go b/modules/task/login_ssh.go new file mode 100644 index 0000000..852bb92 --- /dev/null +++ b/modules/task/login_ssh.go @@ -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 +}