diff --git a/Nut.toml b/Nut.toml index ca74e3a1..b4bfe66e 100644 --- a/Nut.toml +++ b/Nut.toml @@ -9,6 +9,7 @@ authors = [ [dependencies] +"github.com/bgentry/go-netrc/netrc" = "9fd32a8b3d3d3f9d43c341bfe098430e07609480" "github.com/cheggaaa/pb" = "bd14546a551971ae7f460e6d6e527c5b56cd38d7" "github.com/kr/pretty" = "088c856450c08c03eb32f7a6c221e6eefaa10e6f" "github.com/kr/pty" = "5cf931ef8f76dccd0910001d74a58a7fca84a83d" @@ -18,6 +19,6 @@ authors = [ "github.com/rubyist/tracerx" = "d7bcc0bc315bed2a841841bee5dbecc8d7d7582f" "github.com/spf13/cobra" = "c55cdf33856a08e4822738728b41783292812889" "github.com/spf13/pflag" = "580b9be06c33d8ba9dcc8757ea56b7642472c2f5" +"github.com/ThomsonReutersEikon/go-ntlm/ntlm" = "52b7efa603f1b809167b528b8bbaa467e36fdc02" "github.com/technoweenie/assert" = "b25ea301d127043ffacf3b2545726e79b6632139" "github.com/technoweenie/go-contentaddressable" = "38171def3cd15e3b76eb156219b3d48704643899" -"github.com/ThomsonReutersEikon/go-ntlm/ntlm" = "52b7efa603f1b809167b528b8bbaa467e36fdc02" diff --git a/lfs/config.go b/lfs/config.go index 869cdc92..fd45abe0 100644 --- a/lfs/config.go +++ b/lfs/config.go @@ -11,6 +11,7 @@ import ( "github.com/github/git-lfs/git" "github.com/github/git-lfs/vendor/_nuts/github.com/ThomsonReutersEikon/go-ntlm/ntlm" + "github.com/github/git-lfs/vendor/_nuts/github.com/bgentry/go-netrc/netrc" "github.com/github/git-lfs/vendor/_nuts/github.com/rubyist/tracerx" ) @@ -61,6 +62,7 @@ type Configuration struct { fetchExcludePaths []string fetchPruneConfig *FetchPruneConfig manualEndpoint *Endpoint + parsedNetrc netrcfinder } func NewConfig() *Configuration { @@ -209,6 +211,20 @@ func (c *Configuration) SetAccess(operation string, authType string) { c.SetEndpointAccess(c.Endpoint(operation), authType) } +func (c *Configuration) FindNetrcHost(host string) (*netrc.Machine, error) { + c.loading.Lock() + defer c.loading.Unlock() + if c.parsedNetrc == nil { + n, err := c.parseNetrc() + if err != nil { + return nil, err + } + c.parsedNetrc = n + } + + return c.parsedNetrc.FindMachine(host), nil +} + func (c *Configuration) EndpointAccess(e Endpoint) string { key := fmt.Sprintf("lfs.%s.access", e.Url) if v, ok := c.GitConfig(key); ok && len(v) > 0 { diff --git a/lfs/config_netrc.go b/lfs/config_netrc.go new file mode 100644 index 00000000..3a0a1497 --- /dev/null +++ b/lfs/config_netrc.go @@ -0,0 +1,32 @@ +package lfs + +import ( + "os" + "path/filepath" + + "github.com/github/git-lfs/vendor/_nuts/github.com/bgentry/go-netrc/netrc" +) + +type netrcfinder interface { + FindMachine(string) *netrc.Machine +} + +type noNetrc struct{} + +func (n *noNetrc) FindMachine(host string) *netrc.Machine { + return nil +} + +func (c *Configuration) parseNetrc() (netrcfinder, error) { + home := c.Getenv("HOME") + if len(home) == 0 { + return &noNetrc{}, nil + } + + nrcfilename := filepath.Join(home, netrcBasename) + if _, err := os.Stat(nrcfilename); err != nil { + return &noNetrc{}, nil + } + + return netrc.ParseFile(nrcfilename) +} diff --git a/lfs/config_nix.go b/lfs/config_nix.go new file mode 100644 index 00000000..13233f05 --- /dev/null +++ b/lfs/config_nix.go @@ -0,0 +1,4 @@ +// +build !windows +package lfs + +var netrcBasename = ".netrc" diff --git a/lfs/config_windows.go b/lfs/config_windows.go new file mode 100644 index 00000000..a9dfbe70 --- /dev/null +++ b/lfs/config_windows.go @@ -0,0 +1,4 @@ +// +build windows +package lfs + +var netrcBasename = "_netrc" diff --git a/lfs/credentials.go b/lfs/credentials.go index e45b727d..9c6dfe4b 100644 --- a/lfs/credentials.go +++ b/lfs/credentials.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "net" "net/http" "net/url" "os/exec" @@ -26,9 +27,10 @@ func getCreds(req *http.Request) (Creds, error) { // getCredsForAPI gets the credentials for LFS API requests and sets the given // request's Authorization header with them using Basic Authentication. // 1. Check the LFS URL for authentication. Ex: http://user:pass@example.com -// 2. Check the Git remote URL for authentication IF it's the same scheme and +// 2. Check netrc for authentication. +// 3. Check the Git remote URL for authentication IF it's the same scheme and // host of the LFS URL. -// 3. Ask 'git credential' to fill in the password from one of the above URLs. +// 4. Ask 'git credential' to fill in the password from one of the above URLs. // // This prefers the Git remote URL for checking credentials so that users only // have to enter their passwords once for Git and Git LFS. It uses the same @@ -47,6 +49,10 @@ func getCredsForAPI(req *http.Request) (Creds, error) { return nil, nil } + if setCredURLFromNetrc(req) { + return nil, nil + } + return fillCredentials(req, credsUrl) } @@ -90,6 +96,21 @@ func getCredURLForAPI(req *http.Request) (*url.URL, error) { return credsUrl, nil } +func setCredURLFromNetrc(req *http.Request) bool { + host, _, err := net.SplitHostPort(req.URL.Host) + if err != nil { + return false + } + + machine, err := Config.FindNetrcHost(host) + if err != nil || machine == nil { + return false + } + + setRequestAuth(req, machine.Login, machine.Password) + return true +} + func skipCredsCheck(req *http.Request) bool { if Config.NtlmAccess(getOperationForHttpRequest(req)) { return false diff --git a/test/cmd/lfstest-gitserver.go b/test/cmd/lfstest-gitserver.go index 1ee1bf53..c8a39a41 100644 --- a/test/cmd/lfstest-gitserver.go +++ b/test/cmd/lfstest-gitserver.go @@ -250,6 +250,14 @@ func lfsBatchHandler(w http.ResponseWriter, r *http.Request, repo string) { return } + if repo == "netrctest" { + user, pass, err := extractAuth(r.Header.Get("Authorization")) + if err != nil || (user != "netrcuser" || pass != "netrcpass") { + w.WriteHeader(403) + return + } + } + type batchReq struct { Operation string `json:"operation"` Objects []lfsObject `json:"objects"` @@ -540,6 +548,25 @@ func newLfsStorage() *lfsStorage { } } +func extractAuth(auth string) (string, string, error) { + if strings.HasPrefix(auth, "Basic ") { + decodeBy, err := base64.StdEncoding.DecodeString(auth[6:len(auth)]) + decoded := string(decodeBy) + + if err != nil { + return "", "", err + } + + parts := strings.SplitN(decoded, ":", 2) + if len(parts) == 2 { + return parts[0], parts[1], nil + } + return "", "", nil + } + + return "", "", nil +} + func skipIfBadAuth(w http.ResponseWriter, r *http.Request) bool { auth := r.Header.Get("Authorization") if auth == "" { @@ -547,32 +574,25 @@ func skipIfBadAuth(w http.ResponseWriter, r *http.Request) bool { return true } - if strings.HasPrefix(auth, "Basic ") { - decodeBy, err := base64.StdEncoding.DecodeString(auth[6:len(auth)]) - decoded := string(decodeBy) + user, pass, err := extractAuth(auth) + if err != nil { + w.WriteHeader(403) + log.Printf("Error decoding auth: %s\n", err) + return true + } - if err != nil { - w.WriteHeader(403) - log.Printf("Error decoding auth: %s\n", err) - return true + switch user { + case "user": + if pass == "pass" { + return false } - - parts := strings.SplitN(decoded, ":", 2) - if len(parts) == 2 { - switch parts[0] { - case "user": - if parts[1] == "pass" { - return false - } - case "path": - if strings.HasPrefix(r.URL.Path, "/"+parts[1]) { - return false - } - log.Printf("auth attempt against: %q", r.URL.Path) - } + case "netrcuser": + return false + case "path": + if strings.HasPrefix(r.URL.Path, "/"+pass) { + return false } - - log.Printf("auth does not match: %q", decoded) + log.Printf("auth attempt against: %q", r.URL.Path) } w.WriteHeader(403) diff --git a/test/test-credentials.sh b/test/test-credentials.sh index 8736be12..325f4f9f 100755 --- a/test/test-credentials.sh +++ b/test/test-credentials.sh @@ -139,3 +139,61 @@ password=path" [ "$expected" = "$(cat cred.log)" ] ) end_test + +begin_test "credentials from netrc" +( + set -e + + printf "machine localhost\nlogin netrcuser\npassword netrcpass\n" >> "$HOME/.netrc" + echo $HOME + echo "GITSERVER $GITSERVER" + cat $HOME/.netrc + + + reponame="netrctest" + setup_remote_repo "$reponame" + + clone_repo "$reponame" repo + + # Need a remote named "localhost" or 127.0.0.1 in netrc will interfere with the other auth + git remote add "netrc" "$(echo $GITSERVER | sed s/127.0.0.1/localhost/)/netrctest" + git lfs env + + git lfs track "*.dat" + echo "push a" > a.dat + git add .gitattributes a.dat + git commit -m "add a.dat" + + git lfs push netrc master 2>&1 | tee push.log + grep "(1 of 1 files)" push.log +) +end_test + +begin_test "credentials from netrc with bad password" +( + set -e + + printf "machine localhost\nlogin netrcuser\npassword badpass\n" >> "$HOME/.netrc" + echo $HOME + echo "GITSERVER $GITSERVER" + cat $HOME/.netrc + + + reponame="netrctest" + setup_remote_repo "$reponame" + + clone_repo "$reponame" repo2 + + # Need a remote named "localhost" or 127.0.0.1 in netrc will interfere with the other auth + git remote add "netrc" "$(echo $GITSERVER | sed s/127.0.0.1/localhost/)/netrctest" + git lfs env + + git lfs track "*.dat" + echo "push a" > a.dat + git add .gitattributes a.dat + git commit -m "add a.dat" + + git push netrc master 2>&1 | tee push.log + [ "0" = "$(grep -c "(1 of 1 files)" push.log)" ] +) +end_test diff --git a/vendor/_nuts/github.com/bgentry/go-netrc/.hgignore b/vendor/_nuts/github.com/bgentry/go-netrc/.hgignore new file mode 100644 index 00000000..0871e011 --- /dev/null +++ b/vendor/_nuts/github.com/bgentry/go-netrc/.hgignore @@ -0,0 +1,3 @@ +syntax: glob +*.8 +*.a diff --git a/vendor/_nuts/github.com/bgentry/go-netrc/LICENSE b/vendor/_nuts/github.com/bgentry/go-netrc/LICENSE new file mode 100644 index 00000000..aade9a58 --- /dev/null +++ b/vendor/_nuts/github.com/bgentry/go-netrc/LICENSE @@ -0,0 +1,20 @@ +Original version Copyright © 2010 Fazlul Shahriar . Newer +portions Copyright © 2014 Blake Gentry . + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/_nuts/github.com/bgentry/go-netrc/README.md b/vendor/_nuts/github.com/bgentry/go-netrc/README.md new file mode 100644 index 00000000..6759f7ad --- /dev/null +++ b/vendor/_nuts/github.com/bgentry/go-netrc/README.md @@ -0,0 +1,9 @@ +# go-netrc + +A Golang package for reading and writing netrc files. This package can parse netrc +files, make changes to them, and then serialize them back to netrc format, while +preserving any whitespace that was present in the source file. + +[![GoDoc](https://godoc.org/github.com/bgentry/go-netrc?status.png)][godoc] + +[godoc]: https://godoc.org/github.com/bgentry/go-netrc "go-netrc on Godoc.org" diff --git a/vendor/_nuts/github.com/bgentry/go-netrc/netrc/examples/bad_default_order.netrc b/vendor/_nuts/github.com/bgentry/go-netrc/netrc/examples/bad_default_order.netrc new file mode 100644 index 00000000..6aeec07a --- /dev/null +++ b/vendor/_nuts/github.com/bgentry/go-netrc/netrc/examples/bad_default_order.netrc @@ -0,0 +1,13 @@ +# I am a comment +machine mail.google.com + login joe@gmail.com + account gmail + password somethingSecret +# I am another comment + +default + login anonymous + password joe@example.com + +machine ray login demo password mypassword + diff --git a/vendor/_nuts/github.com/bgentry/go-netrc/netrc/examples/good.netrc b/vendor/_nuts/github.com/bgentry/go-netrc/netrc/examples/good.netrc new file mode 100644 index 00000000..41a8e5ba --- /dev/null +++ b/vendor/_nuts/github.com/bgentry/go-netrc/netrc/examples/good.netrc @@ -0,0 +1,22 @@ +# I am a comment +machine mail.google.com + login joe@gmail.com + account justagmail #end of line comment with trailing space + password somethingSecret + # I am another comment + +macdef allput +put src/* + +macdef allput2 + put src/* +put src2/* + +machine ray login demo password mypassword + +machine weirdlogin login uname password pass#pass + +default + login anonymous + password joe@example.com + diff --git a/vendor/_nuts/github.com/bgentry/go-netrc/netrc/netrc.go b/vendor/_nuts/github.com/bgentry/go-netrc/netrc/netrc.go new file mode 100644 index 00000000..ea49987c --- /dev/null +++ b/vendor/_nuts/github.com/bgentry/go-netrc/netrc/netrc.go @@ -0,0 +1,510 @@ +package netrc + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "sync" + "unicode" + "unicode/utf8" +) + +type tkType int + +const ( + tkMachine tkType = iota + tkDefault + tkLogin + tkPassword + tkAccount + tkMacdef + tkComment + tkWhitespace +) + +var keywords = map[string]tkType{ + "machine": tkMachine, + "default": tkDefault, + "login": tkLogin, + "password": tkPassword, + "account": tkAccount, + "macdef": tkMacdef, + "#": tkComment, +} + +type Netrc struct { + tokens []*token + machines []*Machine + macros Macros + updateLock sync.Mutex +} + +// FindMachine returns the Machine in n named by name. If a machine named by +// name exists, it is returned. If no Machine with name name is found and there +// is a ``default'' machine, the ``default'' machine is returned. Otherwise, nil +// is returned. +func (n *Netrc) FindMachine(name string) (m *Machine) { + // TODO(bgentry): not safe for concurrency + var def *Machine + for _, m = range n.machines { + if m.Name == name { + return m + } + if m.IsDefault() { + def = m + } + } + if def == nil { + return nil + } + return def +} + +// MarshalText implements the encoding.TextMarshaler interface to encode a +// Netrc into text format. +func (n *Netrc) MarshalText() (text []byte, err error) { + // TODO(bgentry): not safe for concurrency + for i := range n.tokens { + switch n.tokens[i].kind { + case tkComment, tkDefault, tkWhitespace: // always append these types + text = append(text, n.tokens[i].rawkind...) + default: + if n.tokens[i].value != "" { // skip empty-value tokens + text = append(text, n.tokens[i].rawkind...) + } + } + if n.tokens[i].kind == tkMacdef { + text = append(text, ' ') + text = append(text, n.tokens[i].macroName...) + } + text = append(text, n.tokens[i].rawvalue...) + } + return +} + +func (n *Netrc) NewMachine(name, login, password, account string) *Machine { + n.updateLock.Lock() + defer n.updateLock.Unlock() + + prefix := "\n" + if len(n.tokens) == 0 { + prefix = "" + } + m := &Machine{ + Name: name, + Login: login, + Password: password, + Account: account, + + nametoken: &token{ + kind: tkMachine, + rawkind: []byte(prefix + "machine"), + value: name, + rawvalue: []byte(" " + name), + }, + logintoken: &token{ + kind: tkLogin, + rawkind: []byte("\n\tlogin"), + value: login, + rawvalue: []byte(" " + login), + }, + passtoken: &token{ + kind: tkPassword, + rawkind: []byte("\n\tpassword"), + value: password, + rawvalue: []byte(" " + password), + }, + accounttoken: &token{ + kind: tkAccount, + rawkind: []byte("\n\taccount"), + value: account, + rawvalue: []byte(" " + account), + }, + } + n.insertMachineTokensBeforeDefault(m) + for i := range n.machines { + if n.machines[i].IsDefault() { + n.machines = append(append(n.machines[:i], m), n.machines[i:]...) + return m + } + } + n.machines = append(n.machines, m) + return m +} + +func (n *Netrc) insertMachineTokensBeforeDefault(m *Machine) { + newtokens := []*token{m.nametoken} + if m.logintoken.value != "" { + newtokens = append(newtokens, m.logintoken) + } + if m.passtoken.value != "" { + newtokens = append(newtokens, m.passtoken) + } + if m.accounttoken.value != "" { + newtokens = append(newtokens, m.accounttoken) + } + for i := range n.tokens { + if n.tokens[i].kind == tkDefault { + // found the default, now insert tokens before it + n.tokens = append(n.tokens[:i], append(newtokens, n.tokens[i:]...)...) + return + } + } + // didn't find a default, just add the newtokens to the end + n.tokens = append(n.tokens, newtokens...) + return +} + +func (n *Netrc) RemoveMachine(name string) { + n.updateLock.Lock() + defer n.updateLock.Unlock() + + for i := range n.machines { + if n.machines[i] != nil && n.machines[i].Name == name { + m := n.machines[i] + for _, t := range []*token{ + m.nametoken, m.logintoken, m.passtoken, m.accounttoken, + } { + n.removeToken(t) + } + n.machines = append(n.machines[:i], n.machines[i+1:]...) + return + } + } +} + +func (n *Netrc) removeToken(t *token) { + if t != nil { + for i := range n.tokens { + if n.tokens[i] == t { + n.tokens = append(n.tokens[:i], n.tokens[i+1:]...) + return + } + } + } +} + +// Machine contains information about a remote machine. +type Machine struct { + Name string + Login string + Password string + Account string + + nametoken *token + logintoken *token + passtoken *token + accounttoken *token +} + +// IsDefault returns true if the machine is a "default" token, denoted by an +// empty name. +func (m *Machine) IsDefault() bool { + return m.Name == "" +} + +// UpdatePassword sets the password for the Machine m. +func (m *Machine) UpdatePassword(newpass string) { + m.Password = newpass + updateTokenValue(m.passtoken, newpass) +} + +// UpdateLogin sets the login for the Machine m. +func (m *Machine) UpdateLogin(newlogin string) { + m.Login = newlogin + updateTokenValue(m.logintoken, newlogin) +} + +// UpdateAccount sets the login for the Machine m. +func (m *Machine) UpdateAccount(newaccount string) { + m.Account = newaccount + updateTokenValue(m.accounttoken, newaccount) +} + +func updateTokenValue(t *token, value string) { + oldvalue := t.value + t.value = value + newraw := make([]byte, len(t.rawvalue)) + copy(newraw, t.rawvalue) + t.rawvalue = append( + bytes.TrimSuffix(newraw, []byte(oldvalue)), + []byte(value)..., + ) +} + +// Macros contains all the macro definitions in a netrc file. +type Macros map[string]string + +type token struct { + kind tkType + macroName string + value string + rawkind []byte + rawvalue []byte +} + +// Error represents a netrc file parse error. +type Error struct { + LineNum int // Line number + Msg string // Error message +} + +// Error returns a string representation of error e. +func (e *Error) Error() string { + return fmt.Sprintf("line %d: %s", e.LineNum, e.Msg) +} + +func (e *Error) BadDefaultOrder() bool { + return e.Msg == errBadDefaultOrder +} + +const errBadDefaultOrder = "default token must appear after all machine tokens" + +// scanLinesKeepPrefix is a split function for a Scanner that returns each line +// of text. The returned token may include newlines if they are before the +// first non-space character. The returned line may be empty. The end-of-line +// marker is one optional carriage return followed by one mandatory newline. In +// regular expression notation, it is `\r?\n`. The last non-empty line of +// input will be returned even if it has no newline. +func scanLinesKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + // Skip leading spaces. + start := 0 + for width := 0; start < len(data); start += width { + var r rune + r, width = utf8.DecodeRune(data[start:]) + if !unicode.IsSpace(r) { + break + } + } + if i := bytes.IndexByte(data[start:], '\n'); i >= 0 { + // We have a full newline-terminated line. + return start + i, data[0 : start+i], nil + } + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), data, nil + } + // Request more data. + return 0, nil, nil +} + +// scanWordsKeepPrefix is a split function for a Scanner that returns each +// space-separated word of text, with prefixing spaces included. It will never +// return an empty string. The definition of space is set by unicode.IsSpace. +// +// Adapted from bufio.ScanWords(). +func scanTokensKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) { + // Skip leading spaces. + start := 0 + for width := 0; start < len(data); start += width { + var r rune + r, width = utf8.DecodeRune(data[start:]) + if !unicode.IsSpace(r) { + break + } + } + if atEOF && len(data) == 0 || start == len(data) { + return len(data), data, nil + } + if len(data) > start && data[start] == '#' { + return scanLinesKeepPrefix(data, atEOF) + } + // Scan until space, marking end of word. + for width, i := 0, start; i < len(data); i += width { + var r rune + r, width = utf8.DecodeRune(data[i:]) + if unicode.IsSpace(r) { + return i, data[:i], nil + } + } + // If we're at EOF, we have a final, non-empty, non-terminated word. Return it. + if atEOF && len(data) > start { + return len(data), data, nil + } + // Request more data. + return 0, nil, nil +} + +func newToken(rawb []byte) (*token, error) { + _, tkind, err := bufio.ScanWords(rawb, true) + if err != nil { + return nil, err + } + var ok bool + t := token{rawkind: rawb} + t.kind, ok = keywords[string(tkind)] + if !ok { + trimmed := strings.TrimSpace(string(tkind)) + if trimmed == "" { + t.kind = tkWhitespace // whitespace-only, should happen only at EOF + return &t, nil + } + if strings.HasPrefix(trimmed, "#") { + t.kind = tkComment // this is a comment + return &t, nil + } + return &t, fmt.Errorf("keyword expected; got " + string(tkind)) + } + return &t, nil +} + +func scanValue(scanner *bufio.Scanner, pos int) ([]byte, string, int, error) { + if scanner.Scan() { + raw := scanner.Bytes() + pos += bytes.Count(raw, []byte{'\n'}) + return raw, strings.TrimSpace(string(raw)), pos, nil + } + if err := scanner.Err(); err != nil { + return nil, "", pos, &Error{pos, err.Error()} + } + return nil, "", pos, nil +} + +func parse(r io.Reader, pos int) (*Netrc, error) { + b, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + + nrc := Netrc{machines: make([]*Machine, 0, 20), macros: make(Macros, 10)} + + defaultSeen := false + var currentMacro *token + var m *Machine + var t *token + scanner := bufio.NewScanner(bytes.NewReader(b)) + scanner.Split(scanTokensKeepPrefix) + + for scanner.Scan() { + rawb := scanner.Bytes() + if len(rawb) == 0 { + break + } + pos += bytes.Count(rawb, []byte{'\n'}) + t, err = newToken(rawb) + if err != nil { + if currentMacro == nil { + return nil, &Error{pos, err.Error()} + } + currentMacro.rawvalue = append(currentMacro.rawvalue, rawb...) + continue + } + + if currentMacro != nil && bytes.Contains(rawb, []byte{'\n', '\n'}) { + // if macro rawvalue + rawb would contain \n\n, then macro def is over + currentMacro.value = strings.TrimLeft(string(currentMacro.rawvalue), "\r\n") + nrc.macros[currentMacro.macroName] = currentMacro.value + currentMacro = nil + } + + switch t.kind { + case tkMacdef: + if _, t.macroName, pos, err = scanValue(scanner, pos); err != nil { + return nil, &Error{pos, err.Error()} + } + currentMacro = t + case tkDefault: + if defaultSeen { + return nil, &Error{pos, "multiple default token"} + } + if m != nil { + nrc.machines, m = append(nrc.machines, m), nil + } + m = new(Machine) + m.Name = "" + defaultSeen = true + case tkMachine: + if defaultSeen { + return nil, &Error{pos, errBadDefaultOrder} + } + if m != nil { + nrc.machines, m = append(nrc.machines, m), nil + } + m = new(Machine) + if t.rawvalue, m.Name, pos, err = scanValue(scanner, pos); err != nil { + return nil, &Error{pos, err.Error()} + } + t.value = m.Name + m.nametoken = t + case tkLogin: + if m == nil || m.Login != "" { + return nil, &Error{pos, "unexpected token login "} + } + if t.rawvalue, m.Login, pos, err = scanValue(scanner, pos); err != nil { + return nil, &Error{pos, err.Error()} + } + t.value = m.Login + m.logintoken = t + case tkPassword: + if m == nil || m.Password != "" { + return nil, &Error{pos, "unexpected token password"} + } + if t.rawvalue, m.Password, pos, err = scanValue(scanner, pos); err != nil { + return nil, &Error{pos, err.Error()} + } + t.value = m.Password + m.passtoken = t + case tkAccount: + if m == nil || m.Account != "" { + return nil, &Error{pos, "unexpected token account"} + } + if t.rawvalue, m.Account, pos, err = scanValue(scanner, pos); err != nil { + return nil, &Error{pos, err.Error()} + } + t.value = m.Account + m.accounttoken = t + } + + nrc.tokens = append(nrc.tokens, t) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + if m != nil { + nrc.machines, m = append(nrc.machines, m), nil + } + return &nrc, nil +} + +// ParseFile opens the file at filename and then passes its io.Reader to +// Parse(). +func ParseFile(filename string) (*Netrc, error) { + fd, err := os.Open(filename) + if err != nil { + return nil, err + } + defer fd.Close() + return Parse(fd) +} + +// Parse parses from the the Reader r as a netrc file and returns the set of +// machine information and macros defined in it. The ``default'' machine, +// which is intended to be used when no machine name matches, is identified +// by an empty machine name. There can be only one ``default'' machine. +// +// If there is a parsing error, an Error is returned. +func Parse(r io.Reader) (*Netrc, error) { + return parse(r, 1) +} + +// FindMachine parses the netrc file identified by filename and returns the +// Machine named by name. If a problem occurs parsing the file at filename, an +// error is returned. If a machine named by name exists, it is returned. If no +// Machine with name name is found and there is a ``default'' machine, the +// ``default'' machine is returned. Otherwise, nil is returned. +func FindMachine(filename, name string) (m *Machine, err error) { + n, err := ParseFile(filename) + if err != nil { + return nil, err + } + return n.FindMachine(name), nil +} diff --git a/vendor/_nuts/github.com/bgentry/go-netrc/netrc/netrc_test.go b/vendor/_nuts/github.com/bgentry/go-netrc/netrc/netrc_test.go new file mode 100644 index 00000000..70ceacf6 --- /dev/null +++ b/vendor/_nuts/github.com/bgentry/go-netrc/netrc/netrc_test.go @@ -0,0 +1,559 @@ +// Copyright © 2010 Fazlul Shahriar and +// Copyright © 2014 Blake Gentry . +// See LICENSE file for license details. + +package netrc + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "testing" +) + +var expectedMachines = []*Machine{ + &Machine{Name: "mail.google.com", Login: "joe@gmail.com", Password: "somethingSecret", Account: "justagmail"}, + &Machine{Name: "ray", Login: "demo", Password: "mypassword", Account: ""}, + &Machine{Name: "weirdlogin", Login: "uname", Password: "pass#pass", Account: ""}, + &Machine{Name: "", Login: "anonymous", Password: "joe@example.com", Account: ""}, +} +var expectedMacros = Macros{ + "allput": "put src/*", + "allput2": " put src/*\nput src2/*", +} + +func eqMachine(a *Machine, b *Machine) bool { + return a.Name == b.Name && + a.Login == b.Login && + a.Password == b.Password && + a.Account == b.Account +} + +func testExpected(n *Netrc, t *testing.T) { + if len(expectedMachines) != len(n.machines) { + t.Errorf("expected %d machines, got %d", len(expectedMachines), len(n.machines)) + } else { + for i, e := range expectedMachines { + if !eqMachine(e, n.machines[i]) { + t.Errorf("bad machine; expected %v, got %v\n", e, n.machines[i]) + } + } + } + + if len(expectedMacros) != len(n.macros) { + t.Errorf("expected %d macros, got %d", len(expectedMacros), len(n.macros)) + } else { + for k, v := range expectedMacros { + if v != n.macros[k] { + t.Errorf("bad macro for %s; expected %q, got %q\n", k, v, n.macros[k]) + } + } + } +} + +var newTokenTests = []struct { + rawkind string + tkind tkType +}{ + {"machine", tkMachine}, + {"\n\n\tmachine", tkMachine}, + {"\n machine", tkMachine}, + {"default", tkDefault}, + {"login", tkLogin}, + {"password", tkPassword}, + {"account", tkAccount}, + {"macdef", tkMacdef}, + {"\n # comment stuff ", tkComment}, + {"\n # I am another comment", tkComment}, + {"\n\t\n ", tkWhitespace}, +} + +var newTokenInvalidTests = []string{ + " junk", + "sdfdsf", + "account#unspaced comment", +} + +func TestNewToken(t *testing.T) { + for _, tktest := range newTokenTests { + tok, err := newToken([]byte(tktest.rawkind)) + if err != nil { + t.Fatal(err) + } + if tok.kind != tktest.tkind { + t.Errorf("expected tok.kind %d, got %d", tktest.tkind, tok.kind) + } + if string(tok.rawkind) != tktest.rawkind { + t.Errorf("expected tok.rawkind %q, got %q", tktest.rawkind, string(tok.rawkind)) + } + } + + for _, tktest := range newTokenInvalidTests { + _, err := newToken([]byte(tktest)) + if err == nil { + t.Errorf("expected error with %q, got none", tktest) + } + } +} + +func TestParse(t *testing.T) { + r := netrcReader("examples/good.netrc", t) + n, err := Parse(r) + if err != nil { + t.Fatal(err) + } + testExpected(n, t) +} + +func TestParseFile(t *testing.T) { + n, err := ParseFile("examples/good.netrc") + if err != nil { + t.Fatal(err) + } + testExpected(n, t) + + _, err = ParseFile("examples/bad_default_order.netrc") + if err == nil { + t.Error("expected an error parsing bad_default_order.netrc, got none") + } else if !err.(*Error).BadDefaultOrder() { + t.Error("expected BadDefaultOrder() to be true, got false") + } + + _, err = ParseFile("examples/this_file_doesnt_exist.netrc") + if err == nil { + t.Error("expected an error loading this_file_doesnt_exist.netrc, got none") + } else if _, ok := err.(*os.PathError); !ok { + t.Errorf("expected *os.Error, got %v", err) + } +} + +func TestFindMachine(t *testing.T) { + m, err := FindMachine("examples/good.netrc", "ray") + if err != nil { + t.Fatal(err) + } + if !eqMachine(m, expectedMachines[1]) { + t.Errorf("bad machine; expected %v, got %v\n", expectedMachines[1], m) + } + if m.IsDefault() { + t.Errorf("expected m.IsDefault() to be false") + } + + m, err = FindMachine("examples/good.netrc", "non.existent") + if err != nil { + t.Fatal(err) + } + if !eqMachine(m, expectedMachines[3]) { + t.Errorf("bad machine; expected %v, got %v\n", expectedMachines[3], m) + } + if !m.IsDefault() { + t.Errorf("expected m.IsDefault() to be true") + } +} + +func TestNetrcFindMachine(t *testing.T) { + n, err := ParseFile("examples/good.netrc") + if err != nil { + t.Fatal(err) + } + + m := n.FindMachine("ray") + if !eqMachine(m, expectedMachines[1]) { + t.Errorf("bad machine; expected %v, got %v\n", expectedMachines[1], m) + } + if m.IsDefault() { + t.Errorf("expected def to be false") + } + + n = &Netrc{} + m = n.FindMachine("nonexistent") + if m != nil { + t.Errorf("expected nil, got %v", m) + } +} + +func TestMarshalText(t *testing.T) { + // load up expected netrc Marshal output + expected, err := ioutil.ReadAll(netrcReader("examples/good.netrc", t)) + if err != nil { + t.Fatal(err) + } + + n, err := ParseFile("examples/good.netrc") + if err != nil { + t.Fatal(err) + } + + result, err := n.MarshalText() + if err != nil { + t.Fatal(err) + } + if string(result) != string(expected) { + t.Errorf("expected:\n%q\ngot:\n%q", string(expected), string(result)) + } + + // make sure tokens w/ no value are not serialized + m := n.FindMachine("mail.google.com") + m.UpdatePassword("") + result, err = n.MarshalText() + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(result), "\tpassword \n") { + fmt.Println(string(result)) + t.Errorf("expected zero-value password token to not be serialzed") + } +} + +var newMachineTests = []struct { + name string + login string + password string + account string +}{ + {"heroku.com", "dodging-samurai-42@heroku.com", "octocatdodgeballchampions", "2011+2013"}, + {"bgentry.io", "special@test.com", "noacct", ""}, + {"github.io", "2@test.com", "", "acctwithnopass"}, + {"someotherapi.com", "", "passonly", ""}, +} + +func TestNewMachine(t *testing.T) { + n, err := ParseFile("examples/good.netrc") + if err != nil { + t.Fatal(err) + } + testNewMachine(t, n) + n = &Netrc{} + testNewMachine(t, n) + + // make sure that tokens without a value are not serialized at all + for _, test := range newMachineTests { + n = &Netrc{} + _ = n.NewMachine(test.name, test.login, test.password, test.account) + + bodyb, _ := n.MarshalText() + body := string(bodyb) + + // ensure desired values are present when they should be + if !strings.Contains(body, "machine") { + t.Errorf("NewMachine() %s missing keyword 'machine'", test.name) + } + if !strings.Contains(body, test.name) { + t.Errorf("NewMachine() %s missing value %q", test.name, test.name) + } + if test.login != "" && !strings.Contains(body, "login "+test.login) { + t.Errorf("NewMachine() %s missing value %q", test.name, "login "+test.login) + } + if test.password != "" && !strings.Contains(body, "password "+test.password) { + t.Errorf("NewMachine() %s missing value %q", test.name, "password "+test.password) + } + if test.account != "" && !strings.Contains(body, "account "+test.account) { + t.Errorf("NewMachine() %s missing value %q", test.name, "account "+test.account) + } + + // ensure undesired values are not present when they shouldn't be + if test.login == "" && strings.Contains(body, "login") { + t.Errorf("NewMachine() %s contains unexpected value %q", test.name, "login") + } + if test.password == "" && strings.Contains(body, "password") { + t.Errorf("NewMachine() %s contains unexpected value %q", test.name, "password") + } + if test.account == "" && strings.Contains(body, "account") { + t.Errorf("NewMachine() %s contains unexpected value %q", test.name, "account") + } + } +} + +func testNewMachine(t *testing.T, n *Netrc) { + for _, test := range newMachineTests { + mcount := len(n.machines) + // sanity check + bodyb, _ := n.MarshalText() + body := string(bodyb) + for _, value := range []string{test.name, test.login, test.password, test.account} { + if value != "" && strings.Contains(body, value) { + t.Errorf("MarshalText() before NewMachine() contained unexpected %q", value) + } + } + + // test prefix for machine token + prefix := "\n" + if len(n.tokens) == 0 { + prefix = "" + } + + m := n.NewMachine(test.name, test.login, test.password, test.account) + if m == nil { + t.Fatalf("NewMachine() returned nil") + } + + if len(n.machines) != mcount+1 { + t.Errorf("n.machines count expected %d, got %d", mcount+1, len(n.machines)) + } + // check values + if m.Name != test.name { + t.Errorf("m.Name expected %q, got %q", test.name, m.Name) + } + if m.Login != test.login { + t.Errorf("m.Login expected %q, got %q", test.login, m.Login) + } + if m.Password != test.password { + t.Errorf("m.Password expected %q, got %q", test.password, m.Password) + } + if m.Account != test.account { + t.Errorf("m.Account expected %q, got %q", test.account, m.Account) + } + // check tokens + checkToken(t, "nametoken", m.nametoken, tkMachine, prefix+"machine", test.name) + checkToken(t, "logintoken", m.logintoken, tkLogin, "\n\tlogin", test.login) + checkToken(t, "passtoken", m.passtoken, tkPassword, "\n\tpassword", test.password) + checkToken(t, "accounttoken", m.accounttoken, tkAccount, "\n\taccount", test.account) + // check marshal output + bodyb, _ = n.MarshalText() + body = string(bodyb) + for _, value := range []string{test.name, test.login, test.password, test.account} { + if !strings.Contains(body, value) { + t.Errorf("MarshalText() after NewMachine() did not include %q as expected", value) + } + } + } +} + +func checkToken(t *testing.T, name string, tok *token, kind tkType, rawkind, value string) { + if tok == nil { + t.Errorf("%s not defined", name) + return + } + if tok.kind != kind { + t.Errorf("%s expected kind %d, got %d", name, kind, tok.kind) + } + if string(tok.rawkind) != rawkind { + t.Errorf("%s expected rawkind %q, got %q", name, rawkind, string(tok.rawkind)) + } + if tok.value != value { + t.Errorf("%s expected value %q, got %q", name, value, tok.value) + } + if tok.value != value { + t.Errorf("%s expected value %q, got %q", name, value, tok.value) + } +} + +func TestNewMachineGoesBeforeDefault(t *testing.T) { + n, err := ParseFile("examples/good.netrc") + if err != nil { + t.Fatal(err) + } + m := n.NewMachine("mymachine", "mylogin", "mypassword", "myaccount") + if m2 := n.machines[len(n.machines)-2]; m2 != m { + t.Errorf("expected machine %v, got %v", m, m2) + } +} + +func TestRemoveMachine(t *testing.T) { + n, err := ParseFile("examples/good.netrc") + if err != nil { + t.Fatal(err) + } + + tests := []string{"mail.google.com", "weirdlogin"} + + for _, name := range tests { + mcount := len(n.machines) + // sanity check + m := n.FindMachine(name) + if m == nil { + t.Fatalf("machine %q not found", name) + } + if m.IsDefault() { + t.Fatalf("expected machine %q, got default instead", name) + } + n.RemoveMachine(name) + + if len(n.machines) != mcount-1 { + t.Errorf("n.machines count expected %d, got %d", mcount-1, len(n.machines)) + } + + // make sure Machine is no longer returned by FindMachine() + if m2 := n.FindMachine(name); m2 != nil && !m2.IsDefault() { + t.Errorf("Machine %q not removed from Machines list", name) + } + + // make sure tokens are not present in tokens list + for _, token := range []*token{m.nametoken, m.logintoken, m.passtoken, m.accounttoken} { + if token != nil { + for _, tok2 := range n.tokens { + if tok2 == token { + t.Errorf("token not removed from tokens list: %v", token) + break + } + } + } + } + + bodyb, _ := n.MarshalText() + body := string(bodyb) + for _, value := range []string{m.Name, m.Login, m.Password, m.Account} { + if value != "" && strings.Contains(body, value) { + t.Errorf("MarshalText() after RemoveMachine() contained unexpected %q", value) + } + } + } +} + +func TestUpdateLogin(t *testing.T) { + n, err := ParseFile("examples/good.netrc") + if err != nil { + t.Fatal(err) + } + + tests := []struct { + exists bool + name string + oldlogin string + newlogin string + }{ + {true, "mail.google.com", "joe@gmail.com", "joe2@gmail.com"}, + {false, "heroku.com", "", "dodging-samurai-42@heroku.com"}, + } + + bodyb, _ := n.MarshalText() + body := string(bodyb) + for _, test := range tests { + if strings.Contains(body, test.newlogin) { + t.Errorf("MarshalText() before UpdateLogin() contained unexpected %q", test.newlogin) + } + } + + for _, test := range tests { + m := n.FindMachine(test.name) + if m.IsDefault() == test.exists { + t.Errorf("expected machine %s to not exist, but it did", test.name) + } else { + if !test.exists { + m = n.NewMachine(test.name, test.newlogin, "", "") + } + if m == nil { + t.Errorf("machine %s was nil", test.name) + continue + } + m.UpdateLogin(test.newlogin) + m := n.FindMachine(test.name) + if m.Login != test.newlogin { + t.Errorf("expected new login %q, got %q", test.newlogin, m.Login) + } + if m.logintoken.value != test.newlogin { + t.Errorf("expected m.logintoken %q, got %q", test.newlogin, m.logintoken.value) + } + } + } + + bodyb, _ = n.MarshalText() + body = string(bodyb) + for _, test := range tests { + if test.exists && strings.Contains(body, test.oldlogin) { + t.Errorf("MarshalText() after UpdateLogin() contained unexpected %q", test.oldlogin) + } + if !strings.Contains(body, test.newlogin) { + t.Errorf("MarshalText after UpdatePassword did not contain %q as expected", test.newlogin) + } + } +} + +func TestUpdatePassword(t *testing.T) { + n, err := ParseFile("examples/good.netrc") + if err != nil { + t.Fatal(err) + } + + tests := []struct { + exists bool + name string + oldpassword string + newpassword string + }{ + {true, "ray", "mypassword", "supernewpass"}, + {false, "heroku.com", "", "octocatdodgeballchampions"}, + } + + bodyb, _ := n.MarshalText() + body := string(bodyb) + for _, test := range tests { + if test.exists && !strings.Contains(body, test.oldpassword) { + t.Errorf("MarshalText() before UpdatePassword() did not include %q as expected", test.oldpassword) + } + if strings.Contains(body, test.newpassword) { + t.Errorf("MarshalText() before UpdatePassword() contained unexpected %q", test.newpassword) + } + } + + for _, test := range tests { + m := n.FindMachine(test.name) + if m.IsDefault() == test.exists { + t.Errorf("expected machine %s to not exist, but it did", test.name) + } else { + if !test.exists { + m = n.NewMachine(test.name, "", test.newpassword, "") + } + if m == nil { + t.Errorf("machine %s was nil", test.name) + continue + } + m.UpdatePassword(test.newpassword) + m = n.FindMachine(test.name) + if m.Password != test.newpassword { + t.Errorf("expected new password %q, got %q", test.newpassword, m.Password) + } + if m.passtoken.value != test.newpassword { + t.Errorf("expected m.passtoken %q, got %q", test.newpassword, m.passtoken.value) + } + } + } + + bodyb, _ = n.MarshalText() + body = string(bodyb) + for _, test := range tests { + if test.exists && strings.Contains(body, test.oldpassword) { + t.Errorf("MarshalText() after UpdatePassword() contained unexpected %q", test.oldpassword) + } + if !strings.Contains(body, test.newpassword) { + t.Errorf("MarshalText() after UpdatePassword() did not contain %q as expected", test.newpassword) + } + } +} + +func TestNewFile(t *testing.T) { + var n Netrc + + result, err := n.MarshalText() + if err != nil { + t.Fatal(err) + } + if string(result) != "" { + t.Errorf("expected empty result=\"\", got %q", string(result)) + } + + n.NewMachine("netrctest.heroku.com", "auser", "apassword", "") + + result, err = n.MarshalText() + if err != nil { + t.Fatal(err) + } + expected := `machine netrctest.heroku.com + login auser + password apassword` + + if string(result) != expected { + t.Errorf("expected result:\n%q\ngot:\n%q", expected, string(result)) + } +} + +func netrcReader(filename string, t *testing.T) io.Reader { + b, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + return bytes.NewReader(b) +}