golang.org/x/build@v0.0.0-20240506185731-218518f32b70/gerrit/auth.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package gerrit 6 7 import ( 8 "bytes" 9 "crypto/md5" 10 "encoding/hex" 11 "fmt" 12 "net/http" 13 "net/http/cookiejar" 14 "net/url" 15 "os" 16 "os/exec" 17 "path/filepath" 18 "runtime" 19 "strconv" 20 "strings" 21 "sync" 22 "time" 23 24 "golang.org/x/oauth2" 25 ) 26 27 // Auth is a Gerrit authentication mode. 28 // The most common ones are NoAuth or BasicAuth. 29 type Auth interface { 30 setAuth(*Client, *http.Request) error 31 } 32 33 // BasicAuth sends a username and password. 34 func BasicAuth(username, password string) Auth { 35 return basicAuth{username, password} 36 } 37 38 type basicAuth struct { 39 username, password string 40 } 41 42 func (ba basicAuth) setAuth(c *Client, r *http.Request) error { 43 r.SetBasicAuth(ba.username, ba.password) 44 return nil 45 } 46 47 // GitCookiesAuth derives the Gerrit authentication token from 48 // gitcookies based on the URL of the Gerrit request. 49 // The cookie file used is determined by running "git config 50 // http.cookiefile" in the current directory. 51 // To use a specific file, see GitCookieFileAuth. 52 func GitCookiesAuth() Auth { 53 return gitCookiesAuth{} 54 } 55 56 // GitCookieFileAuth derives the Gerrit authentication token from the 57 // provided gitcookies file. It is equivalent to GitCookiesAuth, 58 // except that "git config http.cookiefile" is not used to find which 59 // cookie file to use. 60 func GitCookieFileAuth(file string) Auth { 61 return &gitCookieFileAuth{file: file} 62 } 63 64 func netrcPath() string { 65 if runtime.GOOS == "windows" { 66 return filepath.Join(os.Getenv("USERPROFILE"), "_netrc") 67 } 68 return filepath.Join(os.Getenv("HOME"), ".netrc") 69 } 70 71 type gitCookiesAuth struct{} 72 73 func (gitCookiesAuth) setAuth(c *Client, r *http.Request) error { 74 // First look in Git's http.cookiefile, which is where Gerrit 75 // now tells users to store this information. 76 git := exec.Command("git", "config", "http.cookiefile") 77 git.Stderr = os.Stderr 78 79 // Ignore a failure here, git will exit(1) if no cookies are 80 // present and prevent the netrc from being read below. 81 gitOut, _ := git.Output() 82 83 cookieFile := strings.TrimSpace(string(gitOut)) 84 if len(cookieFile) != 0 { 85 auth := &gitCookieFileAuth{file: cookieFile} 86 if err := auth.setAuth(c, r); err != nil { 87 return err 88 } 89 if len(r.Header["Cookie"]) > 0 { 90 return nil 91 } 92 } 93 94 url, err := url.Parse(c.url) 95 if err != nil { 96 return err 97 } 98 99 // If not there, then look in $HOME/.netrc, which is where Gerrit 100 // used to tell users to store the information, until the passwords 101 // got so long that old versions of curl couldn't handle them. 102 host := url.Host 103 netrc := netrcPath() 104 data, _ := os.ReadFile(netrc) 105 for _, line := range strings.Split(string(data), "\n") { 106 if i := strings.Index(line, "#"); i >= 0 { 107 line = line[:i] 108 } 109 f := strings.Fields(line) 110 if len(f) >= 6 && f[0] == "machine" && f[1] == host && f[2] == "login" && f[4] == "password" { 111 r.SetBasicAuth(f[3], f[5]) 112 return nil 113 } 114 } 115 return fmt.Errorf("no authentication configured for Gerrit; tried both git config http.cookiefile and %s", netrc) 116 } 117 118 type gitCookieFileAuth struct { 119 file string 120 121 once sync.Once 122 jar *cookiejar.Jar 123 err error 124 } 125 126 func (a *gitCookieFileAuth) loadCookieFileOnce() { 127 data, err := os.ReadFile(a.file) 128 if err != nil { 129 a.err = fmt.Errorf("Error loading cookie file: %v", err) 130 return 131 } 132 a.jar = parseGitCookies(string(data)) 133 } 134 135 func (a *gitCookieFileAuth) setAuth(c *Client, r *http.Request) error { 136 a.once.Do(a.loadCookieFileOnce) 137 if a.err != nil { 138 return a.err 139 } 140 141 url, err := url.Parse(c.url) 142 if err != nil { 143 return err 144 } 145 146 for _, cookie := range a.jar.Cookies(url) { 147 r.AddCookie(cookie) 148 } 149 return nil 150 } 151 152 func parseGitCookies(data string) *cookiejar.Jar { 153 jar, _ := cookiejar.New(nil) 154 for _, line := range strings.Split(data, "\n") { 155 f := strings.Split(line, "\t") 156 if len(f) < 7 { 157 continue 158 } 159 expires, err := strconv.ParseInt(f[4], 10, 64) 160 if err != nil { 161 continue 162 } 163 c := http.Cookie{ 164 Domain: f[0], 165 Path: f[2], 166 Secure: f[3] == "TRUE", 167 Expires: time.Unix(expires, 0), 168 Name: f[5], 169 Value: f[6], 170 } 171 // Construct a fake URL to add c to the jar. 172 url := url.URL{ 173 Scheme: "http", 174 Host: c.Domain, 175 Path: c.Path, 176 } 177 jar.SetCookies(&url, []*http.Cookie{&c}) 178 } 179 return jar 180 } 181 182 // Scopes to use when creating a TokenSource. 183 var OAuth2Scopes = []string{ 184 "https://www.googleapis.com/auth/cloud-platform", 185 "https://www.googleapis.com/auth/gerritcodereview", 186 "https://www.googleapis.com/auth/source.full_control", 187 "https://www.googleapis.com/auth/source.read_write", 188 "https://www.googleapis.com/auth/source.read_only", 189 } 190 191 // OAuth2Auth uses the given TokenSource to authenticate requests. 192 func OAuth2Auth(src oauth2.TokenSource) Auth { 193 return oauth2Auth{src} 194 } 195 196 type oauth2Auth struct { 197 src oauth2.TokenSource 198 } 199 200 func (a oauth2Auth) setAuth(c *Client, r *http.Request) error { 201 token, err := a.src.Token() 202 if err != nil { 203 return err 204 } 205 token.SetAuthHeader(r) 206 return nil 207 } 208 209 // NoAuth makes requests unauthenticated. 210 var NoAuth = noAuth{} 211 212 type noAuth struct{} 213 214 func (noAuth) setAuth(c *Client, r *http.Request) error { 215 return nil 216 } 217 218 type digestAuth struct { 219 Username, Password, Realm, NONCE, QOP, Opaque, Algorithm string 220 } 221 222 func getDigestAuth(username, password string, resp *http.Response) *digestAuth { 223 header := resp.Header.Get("www-authenticate") 224 parts := strings.SplitN(header, " ", 2) 225 parts = strings.Split(parts[1], ", ") 226 opts := make(map[string]string) 227 228 for _, part := range parts { 229 vals := strings.SplitN(part, "=", 2) 230 key := vals[0] 231 val := strings.Trim(vals[1], "\",") 232 opts[key] = val 233 } 234 235 auth := digestAuth{ 236 username, password, 237 opts["realm"], opts["nonce"], opts["qop"], opts["opaque"], opts["algorithm"], 238 } 239 return &auth 240 } 241 242 func setDigestAuth(r *http.Request, username, password string, resp *http.Response, nc int) { 243 auth := getDigestAuth(username, password, resp) 244 authStr := getDigestAuthString(auth, r.URL, r.Method, nc) 245 r.Header.Add("Authorization", authStr) 246 } 247 248 func getDigestAuthString(auth *digestAuth, url *url.URL, method string, nc int) string { 249 var buf bytes.Buffer 250 h := md5.New() 251 fmt.Fprintf(&buf, "%s:%s:%s", auth.Username, auth.Realm, auth.Password) 252 buf.WriteTo(h) 253 ha1 := hex.EncodeToString(h.Sum(nil)) 254 255 h = md5.New() 256 fmt.Fprintf(&buf, "%s:%s", method, url.Path) 257 buf.WriteTo(h) 258 ha2 := hex.EncodeToString(h.Sum(nil)) 259 260 ncStr := fmt.Sprintf("%08x", nc) 261 hnc := "MTM3MDgw" 262 263 h = md5.New() 264 fmt.Fprintf(&buf, "%s:%s:%s:%s:%s:%s", ha1, auth.NONCE, ncStr, hnc, auth.QOP, ha2) 265 buf.WriteTo(h) 266 respdig := hex.EncodeToString(h.Sum(nil)) 267 268 buf.Write([]byte("Digest ")) 269 fmt.Fprintf(&buf, 270 `username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`, 271 auth.Username, auth.Realm, auth.NONCE, url.Path, respdig, 272 ) 273 274 if auth.Opaque != "" { 275 fmt.Fprintf(&buf, `, opaque="%s"`, auth.Opaque) 276 } 277 if auth.QOP != "" { 278 fmt.Fprintf(&buf, `, qop="%s", nc=%s, cnonce="%s"`, auth.QOP, ncStr, hnc) 279 } 280 if auth.Algorithm != "" { 281 fmt.Fprintf(&buf, `, algorithm="%s"`, auth.Algorithm) 282 } 283 284 return buf.String() 285 } 286 287 func (a digestAuth) setAuth(c *Client, r *http.Request) error { 288 resp, err := http.Get(r.URL.String()) 289 if err != nil { 290 return err 291 } 292 setDigestAuth(r, a.Username, a.Password, resp, 1) 293 return nil 294 } 295 296 // DigestAuth returns an Auth implementation which sends 297 // the provided username and password using HTTP Digest Authentication 298 // (RFC 2617) 299 func DigestAuth(username, password string) Auth { 300 return digestAuth{ 301 Username: username, 302 Password: password, 303 } 304 }