[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 {
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
}