1// Copyright 2014 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 main
6
7import (
8	"bytes"
9	"encoding/json"
10	"fmt"
11	"io"
12	"io/ioutil"
13	"net/http"
14	"net/url"
15	"os/user"
16	"path/filepath"
17	"runtime"
18	"sort"
19	"strings"
20	"sync"
21)
22
23// auth holds cached data about authentication to Gerrit.
24var auth struct {
25	initialized bool
26
27	host    string // "go.googlesource.com"
28	url     string // "https://go-review.googlesource.com"
29	project string // "go", "tools", "crypto", etc
30
31	// Authentication information.
32	// Either cookie name + value from git cookie file
33	// or username and password from .netrc.
34	cookieName  string
35	cookieValue string
36	user        string
37	password    string
38}
39
40// loadGerritOriginMutex is used to control access when initializing auth
41// in loadGerritOrigin, which can be called in parallel by "pending".
42// We use a mutex rather than a sync.Once because the tests clear auth.
43var loadGerritOriginMutex sync.Mutex
44
45// loadGerritOrigin loads the Gerrit host name from the origin remote.
46// This sets auth.{initialized,host,url,project}.
47// If the origin remote does not appear to be a Gerrit server
48// (is missing, is GitHub, is not https, has too many path elements),
49// loadGerritOrigin dies.
50func loadGerritOrigin() {
51	loadGerritOriginMutex.Lock()
52	defer loadGerritOriginMutex.Unlock()
53
54	if auth.initialized {
55		return
56	}
57
58	// Gerrit must be set, either explicitly via the code review config or
59	// implicitly as Git's origin remote.
60	origin := config()["gerrit"]
61	originUrl := trim(cmdOutput("git", "config", "remote.origin.url"))
62
63	err := loadGerritOriginInternal(origin, originUrl)
64	if err != nil {
65		dief("failed to load Gerrit origin: %v", err)
66	}
67
68	auth.initialized = true
69}
70
71// loadGerritOriginInternal does the work of loadGerritOrigin, just extracted out
72// for easier testing.
73func loadGerritOriginInternal(origin, remoteOrigin string) error {
74	originUrl, err := url.Parse(remoteOrigin)
75	if err != nil {
76		return fmt.Errorf("failed to parse git's remote.origin.url %q as a URL: %v", remoteOrigin, err)
77	} else {
78		originUrl.User = nil
79		remoteOrigin = originUrl.String()
80	}
81	hasGerritConfig := true
82	if origin == "" {
83		hasGerritConfig = false
84		origin = remoteOrigin
85	}
86	if strings.Contains(origin, "github.com") {
87		return fmt.Errorf("git origin must be a Gerrit host, not GitHub: %s", origin)
88	}
89
90	if googlesourceIndex := strings.Index(origin, ".googlesource.com"); googlesourceIndex >= 0 {
91		if !strings.HasPrefix(origin, "https://") {
92			return fmt.Errorf("git origin must be an https:// URL: %s", origin)
93		}
94		// https:// prefix and then one slash between host and top-level name
95		if strings.Count(origin, "/") != 3 {
96			return fmt.Errorf("git origin is malformed: %s", origin)
97		}
98		host := origin[len("https://"):strings.LastIndex(origin, "/")]
99
100		// In the case of Google's Gerrit, host is go.googlesource.com
101		// and apiURL uses go-review.googlesource.com, but the Gerrit
102		// setup instructions do not write down a cookie explicitly for
103		// go-review.googlesource.com, so we look for the non-review
104		// host name instead.
105		url := origin
106		i := googlesourceIndex
107		url = url[:i] + "-review" + url[i:]
108
109		i = strings.LastIndex(url, "/")
110		url, project := url[:i], url[i+1:]
111
112		auth.host = host
113		auth.url = url
114		auth.project = project
115		return nil
116	}
117
118	// Origin is not *.googlesource.com.
119	//
120	// If the Gerrit origin is set from the codereview.cfg file than we handle it
121	// differently to allow for sub-path hosted Gerrit.
122	auth.host = originUrl.Host
123	if hasGerritConfig {
124		if !strings.HasPrefix(remoteOrigin, origin) {
125			return fmt.Errorf("Gerrit origin %q from %q different than git origin url %q", origin, configPath, originUrl)
126		}
127
128		auth.project = strings.Trim(strings.TrimPrefix(remoteOrigin, origin), "/")
129		auth.url = origin
130	} else {
131		auth.project = strings.Trim(originUrl.Path, "/")
132		auth.url = strings.TrimSuffix(remoteOrigin, originUrl.Path)
133	}
134
135	return nil
136}
137
138// testHomeDir is empty for normal use. During tests it may be set and used
139// in place of the actual home directory. Tests may still need to
140// set the HOME var for sub-processes such as git.
141var testHomeDir = ""
142
143func netrcName() string {
144	// Git on Windows will look in $HOME\_netrc.
145	if runtime.GOOS == "windows" {
146		return "_netrc"
147	}
148	return ".netrc"
149}
150
151// loadAuth loads the authentication tokens for making API calls to
152// the Gerrit origin host.
153func loadAuth() {
154	if auth.user != "" || auth.cookieName != "" {
155		return
156	}
157
158	loadGerritOrigin()
159
160	// First look in Git's http.cookiefile, which is where Gerrit
161	// now tells users to store this information.
162	if cookieFile, _ := trimErr(cmdOutputErr("git", "config", "--path", "--get-urlmatch", "http.cookiefile", auth.url)); cookieFile != "" {
163		data, _ := ioutil.ReadFile(cookieFile)
164		maxMatch := -1
165		for _, line := range lines(string(data)) {
166			f := strings.Split(line, "\t")
167			if len(f) >= 7 && (f[0] == auth.host || strings.HasPrefix(f[0], ".") && strings.HasSuffix(auth.host, f[0])) {
168				if len(f[0]) > maxMatch {
169					auth.cookieName = f[5]
170					auth.cookieValue = f[6]
171					maxMatch = len(f[0])
172				}
173			}
174		}
175		if maxMatch > 0 {
176			return
177		}
178	}
179
180	// If not there, then look in $HOME/.netrc, which is where Gerrit
181	// used to tell users to store the information, until the passwords
182	// got so long that old versions of curl couldn't handle them.
183	netrc := netrcName()
184	homeDir := testHomeDir
185	if homeDir == "" {
186		usr, err := user.Current()
187		if err != nil {
188			dief("failed to get current user home directory to look for %q: %v", netrc, err)
189		}
190		homeDir = usr.HomeDir
191	}
192	data, _ := ioutil.ReadFile(filepath.Join(homeDir, netrc))
193	for _, line := range lines(string(data)) {
194		if i := strings.Index(line, "#"); i >= 0 {
195			line = line[:i]
196		}
197		f := strings.Fields(line)
198		if len(f) >= 6 && f[0] == "machine" && f[1] == auth.host && f[2] == "login" && f[4] == "password" {
199			auth.user = f[3]
200			auth.password = f[5]
201			return
202		}
203	}
204
205	dief("cannot find authentication info for %s", auth.host)
206}
207
208// gerritError is an HTTP error response served by Gerrit.
209type gerritError struct {
210	url        string
211	statusCode int
212	status     string
213	body       string
214}
215
216func (e *gerritError) Error() string {
217	if e.statusCode == http.StatusNotFound {
218		return "change not found on Gerrit server"
219	}
220
221	extra := strings.TrimSpace(e.body)
222	if extra != "" {
223		extra = ": " + extra
224	}
225	return fmt.Sprintf("%s%s", e.status, extra)
226}
227
228// gerritAPI executes a GET or POST request to a Gerrit API endpoint.
229// It uses GET when requestBody is nil, otherwise POST. If target != nil,
230// gerritAPI expects to get a 200 response with a body consisting of an
231// anti-xss line (]})' or some such) followed by JSON.
232// If requestBody != nil, gerritAPI sets the Content-Type to application/json.
233func gerritAPI(path string, requestBody []byte, target interface{}) error {
234	// Strictly speaking, we might be able to use unauthenticated
235	// access, by removing the /a/ from the URL, but that assumes
236	// that all the information we care about is publicly visible.
237	// Using authentication makes it possible for this to work with
238	// non-public CLs or Gerrit hosts too.
239	loadAuth()
240
241	if !strings.HasPrefix(path, "/") {
242		dief("internal error: gerritAPI called with malformed path")
243	}
244
245	url := auth.url + path
246	method := "GET"
247	var reader io.Reader
248	if requestBody != nil {
249		method = "POST"
250		reader = bytes.NewReader(requestBody)
251	}
252	req, err := http.NewRequest(method, url, reader)
253	if err != nil {
254		return err
255	}
256	if requestBody != nil {
257		req.Header.Set("Content-Type", "application/json")
258	}
259	if auth.cookieName != "" {
260		req.AddCookie(&http.Cookie{
261			Name:  auth.cookieName,
262			Value: auth.cookieValue,
263		})
264	} else {
265		req.SetBasicAuth(auth.user, auth.password)
266	}
267
268	resp, err := http.DefaultClient.Do(req)
269	if err != nil {
270		return err
271	}
272	body, err := ioutil.ReadAll(resp.Body)
273	resp.Body.Close()
274
275	if err != nil {
276		return fmt.Errorf("reading response body: %v", err)
277	}
278	if resp.StatusCode != http.StatusOK {
279		return &gerritError{url, resp.StatusCode, resp.Status, string(body)}
280	}
281
282	if target != nil {
283		i := bytes.IndexByte(body, '\n')
284		if i < 0 {
285			return fmt.Errorf("%s: malformed json response - bad header", url)
286		}
287		body = body[i:]
288		if err := json.Unmarshal(body, target); err != nil {
289			return fmt.Errorf("%s: malformed json response", url)
290		}
291	}
292	return nil
293}
294
295// fullChangeID returns the unambigous Gerrit change ID for the commit c on branch b.
296// The returned ID has the form project~originbranch~Ihexhexhexhexhex.
297// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id for details.
298func fullChangeID(b *Branch, c *Commit) string {
299	loadGerritOrigin()
300	return auth.project + "~" + strings.TrimPrefix(b.OriginBranch(), "origin/") + "~" + c.ChangeID
301}
302
303// readGerritChange reads the metadata about a change from the Gerrit server.
304// The changeID should use the syntax project~originbranch~Ihexhexhexhexhex returned
305// by fullChangeID. Using only Ihexhexhexhexhex will work provided it uniquely identifies
306// a single change on the server.
307// The changeID can have additional query parameters appended to it, as in "normalid?o=LABELS".
308// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id for details.
309func readGerritChange(changeID string) (*GerritChange, error) {
310	var c GerritChange
311	err := gerritAPI("/a/changes/"+changeID, nil, &c)
312	if err != nil {
313		return nil, err
314	}
315	return &c, nil
316}
317
318// readGerritChanges is like readGerritChange but expects changeID
319// to be a query parameter list like q=change:XXX&q=change:YYY&o=OPTIONS,
320// and it expects to receive a JSON array of GerritChanges, not just one.
321func readGerritChanges(query string) ([][]*GerritChange, error) {
322	// The Gerrit server imposes a limit of at most 10 q= parameters.
323	v, err := url.ParseQuery(query)
324	if err != nil {
325		return nil, err
326	}
327	var results []chan gerritChangeResult
328	for len(v["q"]) > 0 {
329		n := len(v["q"])
330		if n > 10 {
331			n = 10
332		}
333		all := v["q"]
334		v["q"] = all[:n]
335		query := v.Encode()
336		v["q"] = all[n:]
337		ch := make(chan gerritChangeResult, 1)
338		go readGerritChangesBatch(query, n, ch)
339		results = append(results, ch)
340	}
341
342	var c [][]*GerritChange
343	for _, ch := range results {
344		res := <-ch
345		if res.err != nil {
346			return nil, res.err
347		}
348		c = append(c, res.c...)
349	}
350	return c, nil
351}
352
353type gerritChangeResult struct {
354	c   [][]*GerritChange
355	err error
356}
357
358func readGerritChangesBatch(query string, n int, ch chan gerritChangeResult) {
359	var c [][]*GerritChange
360	// If there are multiple q=, the server sends back an array of arrays of results.
361	// If there is a single q=, it only sends back an array of results; in that case
362	// we need to do the wrapping ourselves.
363	var arg interface{} = &c
364	if n == 1 {
365		c = append(c, nil)
366		arg = &c[0]
367	}
368	err := gerritAPI("/a/changes/?"+query, nil, arg)
369	if len(c) != n && err == nil {
370		err = fmt.Errorf("gerrit result count mismatch")
371	}
372	ch <- gerritChangeResult{c, err}
373}
374
375// GerritChange is the JSON struct returned by a Gerrit CL query.
376type GerritChange struct {
377	ID              string
378	Project         string
379	Branch          string
380	ChangeId        string `json:"change_id"`
381	Subject         string
382	Status          string
383	Created         string
384	Updated         string
385	Insertions      int
386	Deletions       int
387	Number          int `json:"_number"`
388	Owner           *GerritAccount
389	Labels          map[string]*GerritLabel
390	CurrentRevision string `json:"current_revision"`
391	Revisions       map[string]*GerritRevision
392	Messages        []*GerritMessage
393}
394
395// LabelNames returns the label names for the change, in lexicographic order.
396func (g *GerritChange) LabelNames() []string {
397	var names []string
398	for name := range g.Labels {
399		names = append(names, name)
400	}
401	sort.Strings(names)
402	return names
403}
404
405// GerritMessage is the JSON struct for a Gerrit MessageInfo.
406type GerritMessage struct {
407	Author struct {
408		Name string
409	}
410	Message string
411}
412
413// GerritLabel is the JSON struct for a Gerrit LabelInfo.
414type GerritLabel struct {
415	Optional bool
416	Blocking bool
417	Approved *GerritAccount
418	Rejected *GerritAccount
419	All      []*GerritApproval
420}
421
422// GerritAccount is the JSON struct for a Gerrit AccountInfo.
423type GerritAccount struct {
424	ID       int `json:"_account_id"`
425	Name     string
426	Email    string
427	Username string
428}
429
430// GerritApproval is the JSON struct for a Gerrit ApprovalInfo.
431type GerritApproval struct {
432	GerritAccount
433	Value int
434	Date  string
435}
436
437// GerritRevision is the JSON struct for a Gerrit RevisionInfo.
438type GerritRevision struct {
439	Number int `json:"_number"`
440	Ref    string
441	Fetch  map[string]*GerritFetch
442}
443
444// GerritFetch is the JSON struct for a Gerrit FetchInfo
445type GerritFetch struct {
446	URL string
447	Ref string
448}
449