diff --git a/cmd/auth/cookieauth/cookieauth.go b/cmd/auth/cookieauth/cookieauth.go new file mode 100644 index 0000000000..37e8d6e182 --- /dev/null +++ b/cmd/auth/cookieauth/cookieauth.go @@ -0,0 +1,166 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// cookieauth uses a “Netscape cookie file” to implement the GOAUTH protocol +// described in https://golang.org/issue/26232. +// It expects the location of the file as the first command-line argument. +// +// Example GOAUTH usage: +// export GOAUTH="cookieauth $(git config --get http.cookieFile)" +// +// See http://www.cookiecentral.com/faq/#3.5 for a description of the Netscape +// cookie file format. +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "net/http" + "net/http/cookiejar" + "net/url" + "os" + "strconv" + "strings" + "time" + "unicode" +) + +func main() { + if len(os.Args) < 2 { + fmt.Fprintf(os.Stderr, "usage: %s COOKIEFILE [URL]\n", os.Args[0]) + os.Exit(2) + } + + log.SetPrefix("cookieauth: ") + + f, err := os.Open(os.Args[1]) + if err != nil { + log.Fatalf("failed to read cookie file: %v\n", os.Args[1]) + os.Exit(1) + } + defer f.Close() + + var ( + targetURL *url.URL + targetURLs = map[string]*url.URL{} + ) + if len(os.Args) == 3 { + targetURL, err = url.ParseRequestURI(os.Args[2]) + if err != nil { + log.Fatalf("invalid request URI (%v): %q\n", err, os.Args[2]) + } + targetURLs[targetURL.String()] = targetURL + } else if len(os.Args) > 3 { + // Extra arguments were passed: maybe the protocol was expanded? + // We don't know how to interpret the request, so ignore it. + return + } + + entries, err := parseCookieFile(f.Name(), f) + if err != nil { + log.Fatalf("error reading cookie file: %v\n", f.Name()) + } + + jar, err := cookiejar.New(nil) + if err != nil { + log.Fatalf("failed to initialize cookie jar: %v\n", err) + } + + for _, e := range entries { + u := &url.URL{ + Scheme: "https", + Host: e.Host, + Path: e.Cookie.Path, + } + + if targetURL == nil { + targetURLs[u.String()] = u + } + + jar.SetCookies(u, []*http.Cookie{&e.Cookie}) + } + + for _, u := range targetURLs { + req := &http.Request{URL: u, Header: make(http.Header)} + for _, c := range jar.Cookies(req.URL) { + req.AddCookie(c) + } + fmt.Printf("%s\n\n", u) + req.Header.Write(os.Stdout) + fmt.Println() + } +} + +type Entry struct { + Host string + Cookie http.Cookie +} + +// parseCookieFile parses a Netscape cookie file as described in +// http://www.cookiecentral.com/faq/#3.5. +func parseCookieFile(name string, r io.Reader) ([]*Entry, error) { + var entries []*Entry + s := bufio.NewScanner(r) + line := 0 + for s.Scan() { + line++ + text := strings.TrimSpace(s.Text()) + if len(text) < 2 || (text[0] == '#' && unicode.IsSpace(rune(text[1]))) { + continue + } + + e, err := parseCookieLine(text) + if err != nil { + log.Printf("%s:%d: %v\n", name, line, err) + continue + } + entries = append(entries, e) + } + return entries, s.Err() +} + +func parseCookieLine(line string) (*Entry, error) { + f := strings.Fields(line) + if len(f) < 7 { + return nil, fmt.Errorf("found %d columns; want 7", len(f)) + } + + e := new(Entry) + c := &e.Cookie + + if domain := f[0]; strings.HasPrefix(domain, "#HttpOnly_") { + c.HttpOnly = true + e.Host = strings.TrimPrefix(domain[10:], ".") + } else { + e.Host = strings.TrimPrefix(domain, ".") + } + + isDomain, err := strconv.ParseBool(f[1]) + if err != nil { + return nil, fmt.Errorf("non-boolean domain flag: %v", err) + } + if isDomain { + c.Domain = e.Host + } + + c.Path = f[2] + + c.Secure, err = strconv.ParseBool(f[3]) + if err != nil { + return nil, fmt.Errorf("non-boolean secure flag: %v", err) + } + + expiration, err := strconv.ParseInt(f[4], 10, 64) + if err != nil { + return nil, fmt.Errorf("malformed expiration: %v", err) + } + c.Expires = time.Unix(expiration, 0) + + c.Name = f[5] + c.Value = f[6] + + return e, nil +}