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