|  | // Copyright 2015 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. | 
|  |  | 
|  | package gerrit | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "crypto/md5" | 
|  | "encoding/hex" | 
|  | "fmt" | 
|  | "io/ioutil" | 
|  | "net/http" | 
|  | "net/http/cookiejar" | 
|  | "net/url" | 
|  | "os" | 
|  | "os/exec" | 
|  | "path/filepath" | 
|  | "runtime" | 
|  | "strconv" | 
|  | "strings" | 
|  | "sync" | 
|  | "time" | 
|  |  | 
|  | "golang.org/x/oauth2" | 
|  | ) | 
|  |  | 
|  | // Auth is a Gerrit authentication mode. | 
|  | // The most common ones are NoAuth or BasicAuth. | 
|  | type Auth interface { | 
|  | setAuth(*Client, *http.Request) error | 
|  | } | 
|  |  | 
|  | // BasicAuth sends a username and password. | 
|  | func BasicAuth(username, password string) Auth { | 
|  | return basicAuth{username, password} | 
|  | } | 
|  |  | 
|  | type basicAuth struct { | 
|  | username, password string | 
|  | } | 
|  |  | 
|  | func (ba basicAuth) setAuth(c *Client, r *http.Request) error { | 
|  | r.SetBasicAuth(ba.username, ba.password) | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // GitCookiesAuth derives the Gerrit authentication token from | 
|  | // gitcookies based on the URL of the Gerrit request. | 
|  | // The cookie file used is determined by running "git config | 
|  | // http.cookiefile" in the current directory. | 
|  | // To use a specific file, see GitCookieFileAuth. | 
|  | func GitCookiesAuth() Auth { | 
|  | return gitCookiesAuth{} | 
|  | } | 
|  |  | 
|  | // GitCookieFileAuth derives the Gerrit authentication token from the | 
|  | // provided gitcookies file. It is equivalent to GitCookiesAuth, | 
|  | // except that "git config http.cookiefile" is not used to find which | 
|  | // cookie file to use. | 
|  | func GitCookieFileAuth(file string) Auth { | 
|  | return &gitCookieFileAuth{file: file} | 
|  | } | 
|  |  | 
|  | func netrcPath() string { | 
|  | if runtime.GOOS == "windows" { | 
|  | return filepath.Join(os.Getenv("USERPROFILE"), "_netrc") | 
|  | } | 
|  | return filepath.Join(os.Getenv("HOME"), ".netrc") | 
|  | } | 
|  |  | 
|  | type gitCookiesAuth struct{} | 
|  |  | 
|  | func (gitCookiesAuth) setAuth(c *Client, r *http.Request) error { | 
|  | // First look in Git's http.cookiefile, which is where Gerrit | 
|  | // now tells users to store this information. | 
|  | git := exec.Command("git", "config", "http.cookiefile") | 
|  | git.Stderr = os.Stderr | 
|  |  | 
|  | // Ignore a failure here, git will exit(1) if no cookies are | 
|  | // present and prevent the netrc from being read below. | 
|  | gitOut, _ := git.Output() | 
|  |  | 
|  | cookieFile := strings.TrimSpace(string(gitOut)) | 
|  | if len(cookieFile) != 0 { | 
|  | auth := &gitCookieFileAuth{file: cookieFile} | 
|  | if err := auth.setAuth(c, r); err != nil { | 
|  | return err | 
|  | } | 
|  | if len(r.Header["Cookie"]) > 0 { | 
|  | return nil | 
|  | } | 
|  | } | 
|  |  | 
|  | url, err := url.Parse(c.url) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | // If not there, then look in $HOME/.netrc, which is where Gerrit | 
|  | // used to tell users to store the information, until the passwords | 
|  | // got so long that old versions of curl couldn't handle them. | 
|  | host := url.Host | 
|  | netrc := netrcPath() | 
|  | data, _ := ioutil.ReadFile(netrc) | 
|  | for _, line := range strings.Split(string(data), "\n") { | 
|  | if i := strings.Index(line, "#"); i >= 0 { | 
|  | line = line[:i] | 
|  | } | 
|  | f := strings.Fields(line) | 
|  | if len(f) >= 6 && f[0] == "machine" && f[1] == host && f[2] == "login" && f[4] == "password" { | 
|  | r.SetBasicAuth(f[3], f[5]) | 
|  | return nil | 
|  | } | 
|  | } | 
|  | return fmt.Errorf("no authentication configured for Gerrit; tried both git config http.cookiefile and %s", netrc) | 
|  | } | 
|  |  | 
|  | type gitCookieFileAuth struct { | 
|  | file string | 
|  |  | 
|  | once sync.Once | 
|  | jar  *cookiejar.Jar | 
|  | err  error | 
|  | } | 
|  |  | 
|  | func (a *gitCookieFileAuth) loadCookieFileOnce() { | 
|  | data, err := ioutil.ReadFile(a.file) | 
|  | if err != nil { | 
|  | a.err = fmt.Errorf("Error loading cookie file: %v", err) | 
|  | return | 
|  | } | 
|  | a.jar = parseGitCookies(string(data)) | 
|  | } | 
|  |  | 
|  | func (a *gitCookieFileAuth) setAuth(c *Client, r *http.Request) error { | 
|  | a.once.Do(a.loadCookieFileOnce) | 
|  | if a.err != nil { | 
|  | return a.err | 
|  | } | 
|  |  | 
|  | url, err := url.Parse(c.url) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | for _, cookie := range a.jar.Cookies(url) { | 
|  | r.AddCookie(cookie) | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func parseGitCookies(data string) *cookiejar.Jar { | 
|  | jar, _ := cookiejar.New(nil) | 
|  | for _, line := range strings.Split(data, "\n") { | 
|  | f := strings.Split(line, "\t") | 
|  | if len(f) < 7 { | 
|  | continue | 
|  | } | 
|  | expires, err := strconv.ParseInt(f[4], 10, 64) | 
|  | if err != nil { | 
|  | continue | 
|  | } | 
|  | c := http.Cookie{ | 
|  | Domain:  f[0], | 
|  | Path:    f[2], | 
|  | Secure:  f[3] == "TRUE", | 
|  | Expires: time.Unix(expires, 0), | 
|  | Name:    f[5], | 
|  | Value:   f[6], | 
|  | } | 
|  | // Construct a fake URL to add c to the jar. | 
|  | url := url.URL{ | 
|  | Scheme: "http", | 
|  | Host:   c.Domain, | 
|  | Path:   c.Path, | 
|  | } | 
|  | jar.SetCookies(&url, []*http.Cookie{&c}) | 
|  | } | 
|  | return jar | 
|  | } | 
|  |  | 
|  | // Scopes to use when creating a TokenSource. | 
|  | var OAuth2Scopes = []string{ | 
|  | "https://www.googleapis.com/auth/cloud-platform", | 
|  | "https://www.googleapis.com/auth/gerritcodereview", | 
|  | "https://www.googleapis.com/auth/source.full_control", | 
|  | "https://www.googleapis.com/auth/source.read_write", | 
|  | "https://www.googleapis.com/auth/source.read_only", | 
|  | } | 
|  |  | 
|  | // OAuth2Auth uses the given TokenSource to authenticate requests. | 
|  | func OAuth2Auth(src oauth2.TokenSource) Auth { | 
|  | return oauth2Auth{src} | 
|  | } | 
|  |  | 
|  | type oauth2Auth struct { | 
|  | src oauth2.TokenSource | 
|  | } | 
|  |  | 
|  | func (a oauth2Auth) setAuth(c *Client, r *http.Request) error { | 
|  | token, err := a.src.Token() | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | token.SetAuthHeader(r) | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // NoAuth makes requests unauthenticated. | 
|  | var NoAuth = noAuth{} | 
|  |  | 
|  | type noAuth struct{} | 
|  |  | 
|  | func (noAuth) setAuth(c *Client, r *http.Request) error { | 
|  | return nil | 
|  | } | 
|  |  | 
|  | type digestAuth struct { | 
|  | Username, Password, Realm, NONCE, QOP, Opaque, Algorithm string | 
|  | } | 
|  |  | 
|  | func getDigestAuth(username, password string, resp *http.Response) *digestAuth { | 
|  | header := resp.Header.Get("www-authenticate") | 
|  | parts := strings.SplitN(header, " ", 2) | 
|  | parts = strings.Split(parts[1], ", ") | 
|  | opts := make(map[string]string) | 
|  |  | 
|  | for _, part := range parts { | 
|  | vals := strings.SplitN(part, "=", 2) | 
|  | key := vals[0] | 
|  | val := strings.Trim(vals[1], "\",") | 
|  | opts[key] = val | 
|  | } | 
|  |  | 
|  | auth := digestAuth{ | 
|  | username, password, | 
|  | opts["realm"], opts["nonce"], opts["qop"], opts["opaque"], opts["algorithm"], | 
|  | } | 
|  | return &auth | 
|  | } | 
|  |  | 
|  | func setDigestAuth(r *http.Request, username, password string, resp *http.Response, nc int) { | 
|  | auth := getDigestAuth(username, password, resp) | 
|  | authStr := getDigestAuthString(auth, r.URL, r.Method, nc) | 
|  | r.Header.Add("Authorization", authStr) | 
|  | } | 
|  |  | 
|  | func getDigestAuthString(auth *digestAuth, url *url.URL, method string, nc int) string { | 
|  | var buf bytes.Buffer | 
|  | h := md5.New() | 
|  | fmt.Fprintf(&buf, "%s:%s:%s", auth.Username, auth.Realm, auth.Password) | 
|  | buf.WriteTo(h) | 
|  | ha1 := hex.EncodeToString(h.Sum(nil)) | 
|  |  | 
|  | h = md5.New() | 
|  | fmt.Fprintf(&buf, "%s:%s", method, url.Path) | 
|  | buf.WriteTo(h) | 
|  | ha2 := hex.EncodeToString(h.Sum(nil)) | 
|  |  | 
|  | ncStr := fmt.Sprintf("%08x", nc) | 
|  | hnc := "MTM3MDgw" | 
|  |  | 
|  | h = md5.New() | 
|  | fmt.Fprintf(&buf, "%s:%s:%s:%s:%s:%s", ha1, auth.NONCE, ncStr, hnc, auth.QOP, ha2) | 
|  | buf.WriteTo(h) | 
|  | respdig := hex.EncodeToString(h.Sum(nil)) | 
|  |  | 
|  | buf.Write([]byte("Digest ")) | 
|  | fmt.Fprintf(&buf, | 
|  | `username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`, | 
|  | auth.Username, auth.Realm, auth.NONCE, url.Path, respdig, | 
|  | ) | 
|  |  | 
|  | if auth.Opaque != "" { | 
|  | fmt.Fprintf(&buf, `, opaque="%s"`, auth.Opaque) | 
|  | } | 
|  | if auth.QOP != "" { | 
|  | fmt.Fprintf(&buf, `, qop="%s", nc=%s, cnonce="%s"`, auth.QOP, ncStr, hnc) | 
|  | } | 
|  | if auth.Algorithm != "" { | 
|  | fmt.Fprintf(&buf, `, algorithm="%s"`, auth.Algorithm) | 
|  | } | 
|  |  | 
|  | return buf.String() | 
|  | } | 
|  |  | 
|  | func (a digestAuth) setAuth(c *Client, r *http.Request) error { | 
|  | resp, err := http.Get(r.URL.String()) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | setDigestAuth(r, a.Username, a.Password, resp, 1) | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // DigestAuth returns an Auth implementation which sends | 
|  | // the provided username and password using HTTP Digest Authentication | 
|  | // (RFC 2617) | 
|  | func DigestAuth(username, password string) Auth { | 
|  | return digestAuth{ | 
|  | Username: username, | 
|  | Password: password, | 
|  | } | 
|  | } |