diff --git a/go.mod b/go.mod index 171d265..d50225b 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,16 @@ go 1.20 require ( github.com/adrg/xdg v0.4.0 github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v2 v2.25.7 ) require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/sys v0.11.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5093134..f43b109 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,9 @@ github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -11,8 +12,9 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= @@ -20,6 +22,8 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index c33d6f8..7219863 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ func main() { app.Usage = "git plugin" app.Action = run app.Version = version + app.Commands = []*cli.Command{netrcCommand} app.Flags = globalFlags if err := app.Run(os.Args); err != nil { diff --git a/netrc.go b/netrc.go new file mode 100644 index 0000000..3cb7bcf --- /dev/null +++ b/netrc.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + + "github.com/urfave/cli/v2" + + "github.com/woodpecker-ci/plugin-git/netrc" +) + +var netrcCommand = &cli.Command{ + Name: "netrc", + Usage: "built-in credentials helper to read netrc", + Flags: []cli.Flag{&cli.StringFlag{ + Name: "home", + Usage: "Change home directory", + EnvVars: []string{"PLUGIN_HOME"}, + }}, + Action: netrcGet, +} + +func netrcGet(c *cli.Context) error { + if c.Args().Len() == 0 { + curExec, err := os.Executable() + if err != nil { + return err + } + fmt.Printf("built-in credentials helper to read netrc\n"+ + "exec \"git config --global credential.helper '%s netrc'\" to use it\n", curExec) + return nil + } + + // set custom home + if c.IsSet("home") { + os.Setenv("HOME", c.String("home")) + } + + // implement custom git credentials helper + // https://git-scm.com/docs/gitcredentials + switch c.Args().First() { + case "get": + netRC, err := netrc.Read() + if err != nil { + return err + } + if netRC != nil { + fmt.Printf("username=%s\n", netRC.Login) + fmt.Printf("password=%s\n", netRC.Password) + fmt.Println("quit=true") + } + case "store": + // TODO: netrc.Save() + case "erase": + _, err := netrc.Delete() + if err != nil { + return err + } + default: + return fmt.Errorf("got unknown helper arg '%s'", c.Args().First()) + } + + return nil +} + +func setNetRCHelper() *exec.Cmd { + curExec, err := os.Executable() + if err != nil { + log.Fatal(err) + } + + credHelper := fmt.Sprintf("%s netrc", curExec) + + return appendEnv(exec.Command("git", "config", "--global", "credential.helper", credHelper), defaultEnvVars...) +} diff --git a/netrc/netrc.go b/netrc/netrc.go new file mode 100644 index 0000000..fb13913 --- /dev/null +++ b/netrc/netrc.go @@ -0,0 +1,131 @@ +package netrc + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" +) + +type NetRC struct { + Machine, + Login, + Password string +} + +func getFilePath() (string, error) { + // get home + homeDir := getHomeDir() + + // calc netrc file path + file := filepath.Join(homeDir, ".netrc") + if runtime.GOOS == "windows" { + file = filepath.Join(homeDir, "_netrc") + } + + stats, err := os.Stat(file) + if err != nil { + return "", nil + } + if !stats.Mode().IsRegular() { + return "", fmt.Errorf("'%s' exist but is a %s", file, stats.Mode().Type().String()) + } + return file, nil +} + +// Delete delete the netrc if file exist +func Delete() (bool, error) { + file, err := getFilePath() + if err != nil || file == "" { + return false, err + } + + return true, os.Remove(file) +} + +// Save save a netrc +func Save(n *NetRC) error { + if n == nil { + return nil + } + + // get home + homeDir := getHomeDir() + + // calc netrc file path + file := filepath.Join(homeDir, ".netrc") + if runtime.GOOS == "windows" { + file = filepath.Join(homeDir, "_netrc") + } + + content := fmt.Sprintf(` +machine %s +login %s +password %s +`, + n.Machine, + n.Login, + n.Password, + ) + + return os.WriteFile(file, []byte(content), 0o600) +} + +// Read return netrc if file or env var exist +func Read() (*NetRC, error) { + // try to read from env var + netRC := &NetRC{ + Machine: os.Getenv("CI_NETRC_MACHINE"), + Login: os.Getenv("CI_NETRC_USERNAME"), + Password: os.Getenv("CI_NETRC_PASSWORD"), + } + // if we get at least user and password from env we can return + if netRC.Login != "" && netRC.Password != "" { + return netRC, nil + } + + file, err := getFilePath() + if err != nil || file == "" { + return nil, err + } + raw, err := os.ReadFile(file) + if err != nil { + return nil, fmt.Errorf("error while reading file '%s': %w", file, err) + } + + return parseNetRC(string(raw)) +} + +func getHomeDir() string { + if homeDir := os.Getenv("HOME"); homeDir != "" { + return homeDir + } + if homeDir, _ := os.UserHomeDir(); homeDir != "" { + return homeDir + } + pwd, _ := os.Getwd() + return pwd +} + +func parseNetRC(raw string) (*NetRC, error) { + netRC := &NetRC{} + for _, v := range strings.Split(raw, "\n") { + v = strings.TrimSpace(v) + if strings.HasPrefix(v, "machine") { + netRC.Machine = strings.TrimSpace(strings.TrimPrefix(v, "machine")) + } + if strings.HasPrefix(v, "login") { + netRC.Login = strings.TrimSpace(strings.TrimPrefix(v, "login")) + } + if strings.HasPrefix(v, "password") { + netRC.Password = strings.TrimSpace(strings.TrimPrefix(v, "password")) + } + } + + if netRC.Login == "" && netRC.Password == "" { + return nil, fmt.Errorf("parsing netrc failed, got empty result") + } + + return netRC, nil +} diff --git a/netrc/netrc_test.go b/netrc/netrc_test.go new file mode 100644 index 0000000..22f298c --- /dev/null +++ b/netrc/netrc_test.go @@ -0,0 +1,26 @@ +package netrc + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseNetRC(t *testing.T) { + n, err := parseNetRC(``) + assert.Error(t, err) + assert.Nil(t, n) + + n, err = parseNetRC(`machine example.org`) + assert.Error(t, err) + assert.Nil(t, n) + + n, err = parseNetRC(` +machine test.com +password someTestPWD + +login awesomeUser +`) + assert.NoError(t, err) + assert.EqualValues(t, &NetRC{Machine: "test.com", Login: "awesomeUser", Password: "someTestPWD"}, n) +} diff --git a/plugin.go b/plugin.go index 0664c51..9076c4c 100644 --- a/plugin.go +++ b/plugin.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "time" ) @@ -51,6 +52,11 @@ func (p Plugin) Exec() error { var cmds []*exec.Cmd + // windows doesn't understand netrc, so we use our build-in helper + if runtime.GOOS == "windows" { + cmds = append(cmds, setNetRCHelper()) + } + if p.Config.SkipVerify { cmds = append(cmds, skipVerify()) } else if p.Config.CustomCert != "" {