1package gerrit
2
3import (
4	"bytes"
5	"encoding/json"
6	"errors"
7	"fmt"
8	"io"
9	"io/ioutil"
10	"net/http"
11	"net/url"
12	"reflect"
13	"regexp"
14	"strings"
15
16	"github.com/google/go-querystring/query"
17)
18
19// TODO Try to reduce the code duplications of a std API req
20// Maybe with http://play.golang.org/p/j-667shCCB
21// and https://groups.google.com/forum/#!topic/golang-nuts/D-gIr24k5uY
22
23// A Client manages communication with the Gerrit API.
24type Client struct {
25	// client is the HTTP client used to communicate with the API.
26	client *http.Client
27
28	// baseURL is the base URL of the Gerrit instance for API requests.
29	// It must have a trailing slash.
30	baseURL *url.URL
31
32	// Gerrit service for authentication.
33	Authentication *AuthenticationService
34
35	// Services used for talking to different parts of the standard Gerrit API.
36	Access   *AccessService
37	Accounts *AccountsService
38	Changes  *ChangesService
39	Config   *ConfigService
40	Groups   *GroupsService
41	Plugins  *PluginsService
42	Projects *ProjectsService
43
44	// Additional services used for talking to non-standard Gerrit APIs.
45	EventsLog *EventsLogService
46}
47
48// Response is a Gerrit API response.
49// This wraps the standard http.Response returned from Gerrit.
50type Response struct {
51	*http.Response
52}
53
54var (
55	// ErrNoInstanceGiven is returned by NewClient in the event the
56	// gerritURL argument was blank.
57	ErrNoInstanceGiven = errors.New("no Gerrit instance given")
58
59	// ErrUserProvidedWithoutPassword is returned by NewClient
60	// if a user name is provided without a password.
61	ErrUserProvidedWithoutPassword = errors.New("a username was provided without a password")
62
63	// ErrAuthenticationFailed is returned by NewClient in the event the provided
64	// credentials didn't allow us to query account information using digest, basic or cookie
65	// auth.
66	ErrAuthenticationFailed = errors.New("failed to authenticate using the provided credentials")
67
68	// ReParseURL is used to parse the url provided to NewClient(). This
69	// regular expression contains five groups which capture the scheme,
70	// username, password, hostname and port. If we parse the url with this
71	// regular expression
72	ReParseURL = regexp.MustCompile(`^(http|https)://(.+):(.+)@(.+):(\d+)(.*)$`)
73)
74
75// NewClient returns a new Gerrit API client. gerritURL specifies the
76// HTTP endpoint of the Gerrit instance. For example, "http://localhost:8080/".
77// If gerritURL does not have a trailing slash, one is added automatically.
78// If a nil httpClient is provided, http.DefaultClient will be used.
79//
80// The url may contain credentials, http://admin:secret@localhost:8081/ for
81// example. These credentials may either be a user name and password or
82// name and value as in the case of cookie based authentication. If the url contains
83// credentials then this function will attempt to validate the credentials before
84// returning the client. ErrAuthenticationFailed will be returned if the credentials
85// cannot be validated. The process of validating the credentials is relatively simple and
86// only requires that the provided user have permission to GET /a/accounts/self.
87func NewClient(gerritURL string, httpClient *http.Client) (*Client, error) {
88	if httpClient == nil {
89		httpClient = http.DefaultClient
90	}
91
92	endpoint := gerritURL
93	if endpoint == "" {
94		return nil, ErrNoInstanceGiven
95	}
96
97	hasAuth := false
98	username := ""
99	password := ""
100
101	// Depending on the contents of the username and password the default
102	// url.Parse may not work. The below is an example URL that
103	// would end up being parsed incorrectly with url.Parse:
104	//   http://admin:ZOSOKjgV/kgEkN0bzPJp+oGeJLqpXykqWFJpon/Ckg@localhost:38607
105	// So instead of depending on url.Parse we'll try using a regular expression
106	// first to match a specific pattern. If that ends up working we modify
107	// the incoming endpoint to remove the username and password so the rest
108	// of this function will run as expected.
109	submatches := ReParseURL.FindAllStringSubmatch(endpoint, -1)
110	if len(submatches) > 0 && len(submatches[0]) > 5 {
111		submatch := submatches[0]
112		username = submatch[2]
113		password = submatch[3]
114		endpoint = fmt.Sprintf(
115			"%s://%s:%s%s", submatch[1], submatch[4], submatch[5], submatch[6])
116		hasAuth = true
117	}
118
119	baseURL, err := url.Parse(endpoint)
120	if err != nil {
121		return nil, err
122	}
123	if !strings.HasSuffix(baseURL.Path, "/") {
124		baseURL.Path += "/"
125	}
126
127	// Note, if we retrieved the URL and password using the regular
128	// expression above then the below code will do nothing.
129	if baseURL.User != nil {
130		username = baseURL.User.Username()
131		parsedPassword, haspassword := baseURL.User.Password()
132
133		// Catches cases like http://user@localhost:8081/ where no password
134		// was at all. If a blank password is required
135		if !haspassword {
136			return nil, ErrUserProvidedWithoutPassword
137		}
138
139		password = parsedPassword
140
141		// Reconstruct the url but without the username and password.
142		baseURL, err = url.Parse(
143			fmt.Sprintf("%s://%s%s", baseURL.Scheme, baseURL.Host, baseURL.RequestURI()))
144		if err != nil {
145			return nil, err
146		}
147		hasAuth = true
148	}
149
150	c := &Client{
151		client:  httpClient,
152		baseURL: baseURL,
153	}
154	c.Authentication = &AuthenticationService{client: c}
155	c.Access = &AccessService{client: c}
156	c.Accounts = &AccountsService{client: c}
157	c.Changes = &ChangesService{client: c}
158	c.Config = &ConfigService{client: c}
159	c.Groups = &GroupsService{client: c}
160	c.Plugins = &PluginsService{client: c}
161	c.Projects = &ProjectsService{client: c}
162	c.EventsLog = &EventsLogService{client: c}
163
164	if hasAuth {
165		// Digest auth (first since that's the default auth type)
166		c.Authentication.SetDigestAuth(username, password)
167		if success, err := checkAuth(c); success || err != nil {
168			return c, err
169		}
170
171		// Basic auth
172		c.Authentication.SetBasicAuth(username, password)
173		if success, err := checkAuth(c); success || err != nil {
174			return c, err
175		}
176
177		// Cookie auth
178		c.Authentication.SetCookieAuth(username, password)
179		if success, err := checkAuth(c); success || err != nil {
180			return c, err
181		}
182
183		// Reset auth in case the consumer needs to do something special.
184		c.Authentication.ResetAuth()
185		return c, ErrAuthenticationFailed
186	}
187
188	return c, nil
189}
190
191// checkAuth is used by NewClient to check if the current credentials are
192// valid. If the response is 401 Unauthorized then the error will be discarded.
193func checkAuth(client *Client) (bool, error) {
194	_, response, err := client.Accounts.GetAccount("self")
195	switch err {
196	case ErrWWWAuthenticateHeaderMissing:
197		return false, nil
198	case ErrWWWAuthenticateHeaderNotDigest:
199		return false, nil
200	default:
201		// Response could be nil if the connection outright failed
202		// or some other error occurred before we got a response.
203		if response == nil && err != nil {
204			return false, err
205		}
206
207		if err != nil && response.StatusCode == http.StatusUnauthorized {
208			err = nil
209		}
210		return response.StatusCode == http.StatusOK, err
211	}
212}
213
214// NewRequest creates an API request.
215// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
216// Relative URLs should always be specified without a preceding slash.
217// If specified, the value pointed to by body is JSON encoded and included as the request body.
218func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
219	// Build URL for request
220	u, err := c.buildURLForRequest(urlStr)
221	if err != nil {
222		return nil, err
223	}
224
225	var buf io.ReadWriter
226	if body != nil {
227		buf = new(bytes.Buffer)
228		err = json.NewEncoder(buf).Encode(body)
229		if err != nil {
230			return nil, err
231		}
232	}
233
234	req, err := http.NewRequest(method, u, buf)
235	if err != nil {
236		return nil, err
237	}
238
239	// Apply Authentication
240	if err := c.addAuthentication(req); err != nil {
241		return nil, err
242	}
243
244	// Request compact JSON
245	// See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output
246	req.Header.Add("Accept", "application/json")
247	req.Header.Add("Content-Type", "application/json")
248
249	// TODO: Add gzip encoding
250	// Accept-Encoding request header is set to gzip
251	// See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output
252
253	return req, nil
254}
255
256// NewRawPutRequest creates a raw PUT request and makes no attempt to encode
257// or marshal the body. Just passes it straight through.
258func (c *Client) NewRawPutRequest(urlStr string, body string) (*http.Request, error) {
259	// Build URL for request
260	u, err := c.buildURLForRequest(urlStr)
261	if err != nil {
262		return nil, err
263	}
264
265	buf := bytes.NewBuffer([]byte(body))
266	req, err := http.NewRequest("PUT", u, buf)
267	if err != nil {
268		return nil, err
269	}
270
271	// Apply Authentication
272	if err := c.addAuthentication(req); err != nil {
273		return nil, err
274	}
275
276	// Request compact JSON
277	// See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output
278	req.Header.Add("Accept", "application/json")
279	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
280
281	// TODO: Add gzip encoding
282	// Accept-Encoding request header is set to gzip
283	// See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output
284
285	return req, nil
286}
287
288// Call is a combine function for Client.NewRequest and Client.Do.
289//
290// Most API methods are quite the same.
291// Get the URL, apply options, make a request, and get the response.
292// Without adding special headers or something.
293// To avoid a big amount of code duplication you can Client.Call.
294//
295// method is the HTTP method you want to call.
296// u is the URL you want to call.
297// body is the HTTP body.
298// v is the HTTP response.
299//
300// For more information read https://github.com/google/go-github/issues/234
301func (c *Client) Call(method, u string, body interface{}, v interface{}) (*Response, error) {
302	req, err := c.NewRequest(method, u, body)
303	if err != nil {
304		return nil, err
305	}
306
307	resp, err := c.Do(req, v)
308	if err != nil {
309		return resp, err
310	}
311
312	return resp, err
313}
314
315// buildURLForRequest will build the URL (as string) that will be called.
316// We need such a utility method, because the URL.Path needs to be escaped (partly).
317//
318// E.g. if a project is called via "projects/%s" and the project is named "plugin/delete-project"
319// there has to be "projects/plugin%25Fdelete-project" instead of "projects/plugin/delete-project".
320// The second url will return nothing.
321func (c *Client) buildURLForRequest(urlStr string) (string, error) {
322	// If there is a "/" at the start, remove it.
323	// TODO: It can be arranged for all callers of buildURLForRequest to never have a "/" prefix,
324	//       which can be ensured via tests. This is how it's done in go-github.
325	//       Then, this run-time check becomes unnecessary and can be removed.
326	urlStr = strings.TrimPrefix(urlStr, "/")
327
328	// If we are authenticated, let's apply the "a/" prefix,
329	// but only if it has not already been applied.
330	if c.Authentication.HasAuth() && !strings.HasPrefix(urlStr, "a/") {
331		urlStr = "a/" + urlStr
332	}
333
334	rel, err := url.Parse(urlStr)
335	if err != nil {
336		return "", err
337	}
338
339	return c.baseURL.String() + rel.String(), nil
340}
341
342// Do sends an API request and returns the API response.
343// The API response is JSON decoded and stored in the value pointed to by v,
344// or returned as an error if an API error has occurred.
345// If v implements the io.Writer interface, the raw response body will be written to v,
346// without attempting to first decode it.
347func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
348	resp, err := c.client.Do(req)
349	if err != nil {
350		return nil, err
351	}
352
353	// Wrap response
354	response := &Response{Response: resp}
355
356	err = CheckResponse(resp)
357	if err != nil {
358		// even though there was an error, we still return the response
359		// in case the caller wants to inspect it further
360		return response, err
361	}
362
363	if v != nil {
364		defer resp.Body.Close() // nolint: errcheck
365		if w, ok := v.(io.Writer); ok {
366			if _, err := io.Copy(w, resp.Body); err != nil { // nolint: vetshadow
367				return nil, err
368			}
369		} else {
370			var body []byte
371			body, err = ioutil.ReadAll(resp.Body)
372			if err != nil {
373				// even though there was an error, we still return the response
374				// in case the caller wants to inspect it further
375				return response, err
376			}
377
378			body = RemoveMagicPrefixLine(body)
379			err = json.Unmarshal(body, v)
380		}
381	}
382	return response, err
383}
384
385func (c *Client) addAuthentication(req *http.Request) error {
386	// Apply HTTP Basic Authentication
387	if c.Authentication.HasBasicAuth() {
388		req.SetBasicAuth(c.Authentication.name, c.Authentication.secret)
389		return nil
390	}
391
392	// Apply HTTP Cookie
393	if c.Authentication.HasCookieAuth() {
394		req.AddCookie(&http.Cookie{
395			Name:  c.Authentication.name,
396			Value: c.Authentication.secret,
397		})
398		return nil
399	}
400
401	// Apply Digest Authentication.  If we're using digest based
402	// authentication we need to make a request, process the
403	// WWW-Authenticate header, then set the Authorization header on the
404	// incoming request.  We do not need to send a body along because
405	// the request itself should fail first.
406	if c.Authentication.HasDigestAuth() {
407		uri, err := c.buildURLForRequest(req.URL.RequestURI())
408		if err != nil {
409			return err
410		}
411
412		// WARNING: Don't use c.NewRequest here unless you like
413		// infinite recursion.
414		digestRequest, err := http.NewRequest(req.Method, uri, nil)
415		digestRequest.Header.Set("Accept", "*/*")
416		digestRequest.Header.Set("Content-Type", "application/json")
417		if err != nil {
418			return err
419		}
420
421		response, err := c.client.Do(digestRequest)
422		if err != nil {
423			return err
424		}
425
426		// When the function exits discard the rest of the
427		// body and close it.  This should cause go to
428		// reuse the connection.
429		defer io.Copy(ioutil.Discard, response.Body) // nolint: errcheck
430		defer response.Body.Close()                  // nolint: errcheck
431
432		if response.StatusCode == http.StatusUnauthorized {
433			authorization, err := c.Authentication.digestAuthHeader(response)
434			if err != nil {
435				return err
436			}
437			req.Header.Set("Authorization", authorization)
438		}
439	}
440
441	return nil
442}
443
444// DeleteRequest sends an DELETE API Request to urlStr with optional body.
445// It is a shorthand combination for Client.NewRequest with Client.Do.
446//
447// Relative URLs should always be specified without a preceding slash.
448// If specified, the value pointed to by body is JSON encoded and included as the request body.
449func (c *Client) DeleteRequest(urlStr string, body interface{}) (*Response, error) {
450	req, err := c.NewRequest("DELETE", urlStr, body)
451	if err != nil {
452		return nil, err
453	}
454
455	return c.Do(req, nil)
456}
457
458// BaseURL returns the client's Gerrit instance HTTP endpoint.
459func (c *Client) BaseURL() url.URL {
460	return *c.baseURL
461}
462
463// RemoveMagicPrefixLine removes the "magic prefix line" of Gerris JSON
464// response if present. The JSON response body starts with a magic prefix line
465// that must be stripped before feeding the rest of the response body to a JSON
466// parser. The reason for this is to prevent against Cross Site Script
467// Inclusion (XSSI) attacks.  By default all standard Gerrit APIs include this
468// prefix line though some plugins may not.
469//
470// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api.html#output
471func RemoveMagicPrefixLine(body []byte) []byte {
472	if bytes.HasPrefix(body, magicPrefix) {
473		return body[5:]
474	}
475	return body
476}
477
478var magicPrefix = []byte(")]}'\n")
479
480// CheckResponse checks the API response for errors, and returns them if present.
481// A response is considered an error if it has a status code outside the 200 range.
482// API error responses are expected to have no response body.
483//
484// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api.html#response-codes
485func CheckResponse(r *http.Response) error {
486	if c := r.StatusCode; 200 <= c && c <= 299 {
487		return nil
488	}
489
490	// Some calls require an authentification
491	// In such cases errors like:
492	// 		API call to https://review.typo3.org/accounts/self failed: 403 Forbidden
493	// will be thrown.
494
495	err := fmt.Errorf("API call to %s failed: %s", r.Request.URL.String(), r.Status)
496	return err
497}
498
499// queryParameterReplacements are values in a url, specifically the query
500// portion of the url, which should not be escaped before being sent to
501// Gerrit. Note, Gerrit itself does not escape these values when using the
502// search box so we shouldn't escape them either.
503var queryParameterReplacements = map[string]string{
504	"+": "GOGERRIT_URL_PLACEHOLDER_PLUS",
505	":": "GOGERRIT_URL_PLACEHOLDER_COLON"}
506
507// addOptions adds the parameters in opt as URL query parameters to s.
508// opt must be a struct whose fields may contain "url" tags.
509func addOptions(s string, opt interface{}) (string, error) {
510	v := reflect.ValueOf(opt)
511	if v.Kind() == reflect.Ptr && v.IsNil() {
512		return s, nil
513	}
514
515	u, err := url.Parse(s)
516	if err != nil {
517		return s, err
518	}
519
520	qs, err := query.Values(opt)
521	if err != nil {
522		return s, err
523	}
524
525	// If the url contained one or more query parameters (q) then we need
526	// to do some escaping on these values before Encode() is called.  By
527	// doing so we're ensuring that : and + don't get encoded which means
528	// they'll be passed along to Gerrit as raw ascii. Without this Gerrit
529	// could return 400 Bad Request depending on the query parameters. For
530	// more complete information see this issue on GitHub:
531	//   https://github.com/andygrunwald/go-gerrit/issues/18
532	_, hasQuery := qs["q"]
533	if hasQuery {
534		values := []string{}
535		for _, value := range qs["q"] {
536			for key, replacement := range queryParameterReplacements {
537				value = strings.Replace(value, key, replacement, -1)
538			}
539			values = append(values, value)
540		}
541
542		qs.Del("q")
543		for _, value := range values {
544			qs.Add("q", value)
545		}
546	}
547	encoded := qs.Encode()
548
549	if hasQuery {
550		for key, replacement := range queryParameterReplacements {
551			encoded = strings.Replace(encoded, replacement, key, -1)
552		}
553	}
554
555	u.RawQuery = encoded
556	return u.String(), nil
557}
558
559// getStringResponseWithoutOptions retrieved a single string Response for a GET request
560func getStringResponseWithoutOptions(client *Client, u string) (string, *Response, error) {
561	v := new(string)
562	resp, err := client.Call("GET", u, nil, v)
563	return *v, resp, err
564}
565