blob: 428f9adc5ebd4f62710fe311fd7acbc91eb36c93 [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 "io/ioutil"
13 "log"
14 "net/http"
15 "net/http/cookiejar"
16 "net/url"
17 "os"
18 "os/exec"
19 "path/filepath"
Daniel Theophanesc1b72a712018-08-03 10:51:18 -070020 "runtime"
Austin Clements16adb8c2015-10-21 21:35:17 -040021 "strconv"
22 "strings"
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000023 "sync"
Austin Clements16adb8c2015-10-21 21:35:17 -040024 "time"
25)
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 {
30 setAuth(*Client, *http.Request)
31}
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
42func (ba basicAuth) setAuth(c *Client, r *http.Request) {
43 r.SetBasicAuth(ba.username, ba.password)
44}
45
Austin Clements16adb8c2015-10-21 21:35:17 -040046// GitCookiesAuth derives the Gerrit authentication token from
47// gitcookies based on the URL of the Gerrit request.
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000048// The cookie file used is determined by running "git config
49// http.cookiefile" in the current directory.
50// To use a specific file, see GitCookieFileAuth.
Austin Clements16adb8c2015-10-21 21:35:17 -040051func GitCookiesAuth() Auth {
52 return gitCookiesAuth{}
53}
54
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000055// GitCookieFileAuth derives the Gerrit authentication token from the
56// provided gitcookies file. It is equivalent to GitCookiesAuth,
57// except that "git config http.cookiefile" is not used to find which
58// cookie file to use.
59func GitCookieFileAuth(file string) Auth {
60 return &gitCookieFileAuth{file: file}
61}
62
Daniel Theophanesc1b72a712018-08-03 10:51:18 -070063func netrcPath() string {
64 if runtime.GOOS == "windows" {
65 return filepath.Join(os.Getenv("USERPROFILE"), "_netrc")
66 }
67 return filepath.Join(os.Getenv("HOME"), ".netrc")
68}
69
Austin Clements16adb8c2015-10-21 21:35:17 -040070type gitCookiesAuth struct{}
71
72func (gitCookiesAuth) setAuth(c *Client, r *http.Request) {
Austin Clements16adb8c2015-10-21 21:35:17 -040073 // First look in Git's http.cookiefile, which is where Gerrit
74 // now tells users to store this information.
75 git := exec.Command("git", "config", "http.cookiefile")
76 git.Stderr = os.Stderr
Daniel Theophanesc1b72a712018-08-03 10:51:18 -070077
78 // Ignore a failure here, git will exit(1) if no cookies are
79 // present and prevent the netrc from being read below.
80 gitOut, _ := git.Output()
81
Austin Clements16adb8c2015-10-21 21:35:17 -040082 cookieFile := strings.TrimSpace(string(gitOut))
83 if len(cookieFile) != 0 {
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000084 auth := &gitCookieFileAuth{file: cookieFile}
85 auth.setAuth(c, r)
86 if len(r.Header["Cookie"]) > 0 {
Austin Clements16adb8c2015-10-21 21:35:17 -040087 return
88 }
89 }
90
Brad Fitzpatrick5612add2016-02-08 22:14:13 +000091 url, err := url.Parse(c.url)
92 if err != nil {
93 // Something else will complain about this.
94 return
95 }
96
Austin Clements16adb8c2015-10-21 21:35:17 -040097 // If not there, then look in $HOME/.netrc, which is where Gerrit
98 // used to tell users to store the information, until the passwords
99 // got so long that old versions of curl couldn't handle them.
Brad Fitzpatrick5612add2016-02-08 22:14:13 +0000100 host := url.Host
Daniel Theophanesc1b72a712018-08-03 10:51:18 -0700101 netrc := netrcPath()
102 data, _ := ioutil.ReadFile(netrc)
Austin Clements16adb8c2015-10-21 21:35:17 -0400103 for _, line := range strings.Split(string(data), "\n") {
104 if i := strings.Index(line, "#"); i >= 0 {
105 line = line[:i]
106 }
107 f := strings.Fields(line)
108 if len(f) >= 6 && f[0] == "machine" && f[1] == host && f[2] == "login" && f[4] == "password" {
109 r.SetBasicAuth(f[3], f[5])
110 return
111 }
112 }
Daniel Theophanesc1b72a712018-08-03 10:51:18 -0700113 log.Printf("no authentication configured for Gerrit; tried both git config http.cookiefile and %s", netrc)
Austin Clements16adb8c2015-10-21 21:35:17 -0400114}
115
Brad Fitzpatrick5612add2016-02-08 22:14:13 +0000116type gitCookieFileAuth struct {
117 file string
118
119 once sync.Once
120 jar *cookiejar.Jar
121 err error
122}
123
124func (a *gitCookieFileAuth) loadCookieFileOnce() {
125 data, err := ioutil.ReadFile(a.file)
126 if err != nil {
127 a.err = fmt.Errorf("Error loading cookie file: %v", err)
128 return
129 }
130 a.jar = parseGitCookies(string(data))
131}
132
133func (a *gitCookieFileAuth) setAuth(c *Client, r *http.Request) {
134 a.once.Do(a.loadCookieFileOnce)
135 if a.err != nil {
136 log.Print(a.err)
137 return
138 }
139
140 url, err := url.Parse(c.url)
141 if err != nil {
142 // Something else will complain about this.
143 return
144 }
145
146 for _, cookie := range a.jar.Cookies(url) {
147 r.AddCookie(cookie)
148 }
149}
150
Austin Clements16adb8c2015-10-21 21:35:17 -0400151func parseGitCookies(data string) *cookiejar.Jar {
152 jar, _ := cookiejar.New(nil)
153 for _, line := range strings.Split(data, "\n") {
154 f := strings.Split(line, "\t")
155 if len(f) < 7 {
156 continue
157 }
158 expires, err := strconv.ParseInt(f[4], 10, 64)
159 if err != nil {
160 continue
161 }
162 c := http.Cookie{
163 Domain: f[0],
164 Path: f[2],
165 Secure: f[3] == "TRUE",
166 Expires: time.Unix(expires, 0),
167 Name: f[5],
168 Value: f[6],
169 }
170 // Construct a fake URL to add c to the jar.
171 url := url.URL{
172 Scheme: "http",
173 Host: c.Domain,
174 Path: c.Path,
175 }
176 jar.SetCookies(&url, []*http.Cookie{&c})
177 }
178 return jar
179}
180
Brad Fitzpatrickc7891252015-02-10 20:25:37 -0800181// NoAuth makes requests unauthenticated.
182var NoAuth = noAuth{}
183
184type noAuth struct{}
185
186func (noAuth) setAuth(c *Client, r *http.Request) {}
Pavlo Sumkinff20af92016-10-19 16:34:28 +0300187
188type digestAuth struct {
189 Username, Password, Realm, NONCE, QOP, Opaque, Algorithm string
190}
191
192func getDigestAuth(username, password string, resp *http.Response) *digestAuth {
193 header := resp.Header.Get("www-authenticate")
194 parts := strings.SplitN(header, " ", 2)
195 parts = strings.Split(parts[1], ", ")
196 opts := make(map[string]string)
197
198 for _, part := range parts {
199 vals := strings.SplitN(part, "=", 2)
200 key := vals[0]
201 val := strings.Trim(vals[1], "\",")
202 opts[key] = val
203 }
204
205 auth := digestAuth{
206 username, password,
207 opts["realm"], opts["nonce"], opts["qop"], opts["opaque"], opts["algorithm"],
208 }
209 return &auth
210}
211
212func setDigestAuth(r *http.Request, username, password string, resp *http.Response, nc int) {
213 auth := getDigestAuth(username, password, resp)
214 authStr := getDigestAuthString(auth, r.URL, r.Method, nc)
215 r.Header.Add("Authorization", authStr)
216}
217
218func getDigestAuthString(auth *digestAuth, url *url.URL, method string, nc int) string {
219 var buf bytes.Buffer
220 h := md5.New()
221 fmt.Fprintf(&buf, "%s:%s:%s", auth.Username, auth.Realm, auth.Password)
222 buf.WriteTo(h)
223 ha1 := hex.EncodeToString(h.Sum(nil))
224
225 h = md5.New()
226 fmt.Fprintf(&buf, "%s:%s", method, url.Path)
227 buf.WriteTo(h)
228 ha2 := hex.EncodeToString(h.Sum(nil))
229
230 ncStr := fmt.Sprintf("%08x", nc)
231 hnc := "MTM3MDgw"
232
233 h = md5.New()
234 fmt.Fprintf(&buf, "%s:%s:%s:%s:%s:%s", ha1, auth.NONCE, ncStr, hnc, auth.QOP, ha2)
235 buf.WriteTo(h)
236 respdig := hex.EncodeToString(h.Sum(nil))
237
238 buf.Write([]byte("Digest "))
239 fmt.Fprintf(&buf,
240 `username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
241 auth.Username, auth.Realm, auth.NONCE, url.Path, respdig,
242 )
243
244 if auth.Opaque != "" {
245 fmt.Fprintf(&buf, `, opaque="%s"`, auth.Opaque)
246 }
247 if auth.QOP != "" {
248 fmt.Fprintf(&buf, `, qop="%s", nc=%s, cnonce="%s"`, auth.QOP, ncStr, hnc)
249 }
250 if auth.Algorithm != "" {
251 fmt.Fprintf(&buf, `, algorithm="%s"`, auth.Algorithm)
252 }
253
254 return buf.String()
255}
256
257func (a digestAuth) setAuth(c *Client, r *http.Request) {
258 resp, err := http.Get(r.URL.String())
259 if err != nil {
260 return
261 }
262 setDigestAuth(r, a.Username, a.Password, resp, 1)
263}
264
265// DigestAuth returns an Auth implementation which sends
266// the provided username and password using HTTP Digest Authentication
267// (RFC 2617)
268func DigestAuth(username, password string) Auth {
269 return digestAuth{
270 Username: username,
271 Password: password,
272 }
273}