blob: 16bc97f425f14ccb50a490a50acf98d9eb03d04a [file] [log] [blame]
Brad Fitzpatrickc7891252015-02-10 20:25:37 -08001// 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
5package gerrit
6
Austin Clements16adb8c2015-10-21 21:35:17 -04007import (
Pavlo Sumkinff20af92016-10-19 16:34:28 +03008 "bytes"
9 "crypto/md5"
10 "encoding/hex"
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000011 "fmt"
Austin Clements16adb8c2015-10-21 21:35:17 -040012 "net/http"
13 "net/http/cookiejar"
14 "net/url"
15 "os"
16 "os/exec"
17 "path/filepath"
Daniel Theophanesc1b72a712018-08-03 10:51:18 -070018 "runtime"
Austin Clements16adb8c2015-10-21 21:35:17 -040019 "strconv"
20 "strings"
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000021 "sync"
Austin Clements16adb8c2015-10-21 21:35:17 -040022 "time"
Heschi Kreinick644dfce2022-07-12 13:41:14 -040023
24 "golang.org/x/oauth2"
Austin Clements16adb8c2015-10-21 21:35:17 -040025)
Brad Fitzpatrickc7891252015-02-10 20:25:37 -080026
27// Auth is a Gerrit authentication mode.
28// The most common ones are NoAuth or BasicAuth.
29type Auth interface {
Heschi Kreinick644dfce2022-07-12 13:41:14 -040030 setAuth(*Client, *http.Request) error
Brad Fitzpatrickc7891252015-02-10 20:25:37 -080031}
32
33// BasicAuth sends a username and password.
34func BasicAuth(username, password string) Auth {
35 return basicAuth{username, password}
36}
37
Brad Fitzpatrickc7891252015-02-10 20:25:37 -080038type basicAuth struct {
39 username, password string
40}
41
Heschi Kreinick644dfce2022-07-12 13:41:14 -040042func (ba basicAuth) setAuth(c *Client, r *http.Request) error {
Brad Fitzpatrickc7891252015-02-10 20:25:37 -080043 r.SetBasicAuth(ba.username, ba.password)
Heschi Kreinick644dfce2022-07-12 13:41:14 -040044 return nil
Brad Fitzpatrickc7891252015-02-10 20:25:37 -080045}
46
Austin Clements16adb8c2015-10-21 21:35:17 -040047// GitCookiesAuth derives the Gerrit authentication token from
48// gitcookies based on the URL of the Gerrit request.
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000049// 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.
Austin Clements16adb8c2015-10-21 21:35:17 -040052func GitCookiesAuth() Auth {
53 return gitCookiesAuth{}
54}
55
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000056// 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.
60func GitCookieFileAuth(file string) Auth {
61 return &gitCookieFileAuth{file: file}
62}
63
Daniel Theophanesc1b72a712018-08-03 10:51:18 -070064func 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
Austin Clements16adb8c2015-10-21 21:35:17 -040071type gitCookiesAuth struct{}
72
Heschi Kreinick644dfce2022-07-12 13:41:14 -040073func (gitCookiesAuth) setAuth(c *Client, r *http.Request) error {
Austin Clements16adb8c2015-10-21 21:35:17 -040074 // 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
Daniel Theophanesc1b72a712018-08-03 10:51:18 -070078
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
Austin Clements16adb8c2015-10-21 21:35:17 -040083 cookieFile := strings.TrimSpace(string(gitOut))
84 if len(cookieFile) != 0 {
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000085 auth := &gitCookieFileAuth{file: cookieFile}
Heschi Kreinick644dfce2022-07-12 13:41:14 -040086 if err := auth.setAuth(c, r); err != nil {
87 return err
88 }
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000089 if len(r.Header["Cookie"]) > 0 {
Heschi Kreinick644dfce2022-07-12 13:41:14 -040090 return nil
Austin Clements16adb8c2015-10-21 21:35:17 -040091 }
92 }
93
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000094 url, err := url.Parse(c.url)
95 if err != nil {
Heschi Kreinick644dfce2022-07-12 13:41:14 -040096 return err
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000097 }
98
Austin Clements16adb8c2015-10-21 21:35:17 -040099 // 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.
Brad Fitzpatrick5612add2016-02-08 22:14:13 +0000102 host := url.Host
Daniel Theophanesc1b72a712018-08-03 10:51:18 -0700103 netrc := netrcPath()
Dmitri Shuralyov1ee051b2023-09-08 12:42:54 -0400104 data, _ := os.ReadFile(netrc)
Austin Clements16adb8c2015-10-21 21:35:17 -0400105 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])
Heschi Kreinick644dfce2022-07-12 13:41:14 -0400112 return nil
Austin Clements16adb8c2015-10-21 21:35:17 -0400113 }
114 }
Heschi Kreinick644dfce2022-07-12 13:41:14 -0400115 return fmt.Errorf("no authentication configured for Gerrit; tried both git config http.cookiefile and %s", netrc)
Austin Clements16adb8c2015-10-21 21:35:17 -0400116}
117
Brad Fitzpatrick5612add2016-02-08 22:14:13 +0000118type gitCookieFileAuth struct {
119 file string
120
121 once sync.Once
122 jar *cookiejar.Jar
123 err error
124}
125
126func (a *gitCookieFileAuth) loadCookieFileOnce() {
Dmitri Shuralyov1ee051b2023-09-08 12:42:54 -0400127 data, err := os.ReadFile(a.file)
Brad Fitzpatrick5612add2016-02-08 22:14:13 +0000128 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
Heschi Kreinick644dfce2022-07-12 13:41:14 -0400135func (a *gitCookieFileAuth) setAuth(c *Client, r *http.Request) error {
Brad Fitzpatrick5612add2016-02-08 22:14:13 +0000136 a.once.Do(a.loadCookieFileOnce)
137 if a.err != nil {
Heschi Kreinick644dfce2022-07-12 13:41:14 -0400138 return a.err
Brad Fitzpatrick5612add2016-02-08 22:14:13 +0000139 }
140
141 url, err := url.Parse(c.url)
142 if err != nil {
Heschi Kreinick644dfce2022-07-12 13:41:14 -0400143 return err
Brad Fitzpatrick5612add2016-02-08 22:14:13 +0000144 }
145
146 for _, cookie := range a.jar.Cookies(url) {
147 r.AddCookie(cookie)
148 }
Heschi Kreinick644dfce2022-07-12 13:41:14 -0400149 return nil
Brad Fitzpatrick5612add2016-02-08 22:14:13 +0000150}
151
Austin Clements16adb8c2015-10-21 21:35:17 -0400152func 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
Heschi Kreinick644dfce2022-07-12 13:41:14 -0400182// Scopes to use when creating a TokenSource.
183var 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.
192func OAuth2Auth(src oauth2.TokenSource) Auth {
193 return oauth2Auth{src}
194}
195
196type oauth2Auth struct {
197 src oauth2.TokenSource
198}
199
200func (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
Brad Fitzpatrickc7891252015-02-10 20:25:37 -0800209// NoAuth makes requests unauthenticated.
210var NoAuth = noAuth{}
211
212type noAuth struct{}
213
Heschi Kreinick644dfce2022-07-12 13:41:14 -0400214func (noAuth) setAuth(c *Client, r *http.Request) error {
215 return nil
216}
Pavlo Sumkinff20af92016-10-19 16:34:28 +0300217
218type digestAuth struct {
219 Username, Password, Realm, NONCE, QOP, Opaque, Algorithm string
220}
221
222func 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
242func 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
248func 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
Heschi Kreinick644dfce2022-07-12 13:41:14 -0400287func (a digestAuth) setAuth(c *Client, r *http.Request) error {
Pavlo Sumkinff20af92016-10-19 16:34:28 +0300288 resp, err := http.Get(r.URL.String())
289 if err != nil {
Heschi Kreinick644dfce2022-07-12 13:41:14 -0400290 return err
Pavlo Sumkinff20af92016-10-19 16:34:28 +0300291 }
292 setDigestAuth(r, a.Username, a.Password, resp, 1)
Heschi Kreinick644dfce2022-07-12 13:41:14 -0400293 return nil
Pavlo Sumkinff20af92016-10-19 16:34:28 +0300294}
295
296// DigestAuth returns an Auth implementation which sends
297// the provided username and password using HTTP Digest Authentication
298// (RFC 2617)
299func DigestAuth(username, password string) Auth {
300 return digestAuth{
301 Username: username,
302 Password: password,
303 }
304}