package gerrit import ( "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "net/url" "reflect" "regexp" "strings" "github.com/google/go-querystring/query" ) // TODO Try to reduce the code duplications of a std API req // Maybe with http://play.golang.org/p/j-667shCCB // and https://groups.google.com/forum/#!topic/golang-nuts/D-gIr24k5uY // A Client manages communication with the Gerrit API. type Client struct { // client is the HTTP client used to communicate with the API. client *http.Client // baseURL is the base URL of the Gerrit instance for API requests. // It must have a trailing slash. baseURL *url.URL // Gerrit service for authentication. Authentication *AuthenticationService // Services used for talking to different parts of the standard Gerrit API. Access *AccessService Accounts *AccountsService Changes *ChangesService Config *ConfigService Groups *GroupsService Plugins *PluginsService Projects *ProjectsService // Additional services used for talking to non-standard Gerrit APIs. EventsLog *EventsLogService } // Response is a Gerrit API response. // This wraps the standard http.Response returned from Gerrit. type Response struct { *http.Response } var ( // ErrNoInstanceGiven is returned by NewClient in the event the // gerritURL argument was blank. ErrNoInstanceGiven = errors.New("no Gerrit instance given") // ErrUserProvidedWithoutPassword is returned by NewClient // if a user name is provided without a password. ErrUserProvidedWithoutPassword = errors.New("a username was provided without a password") // ErrAuthenticationFailed is returned by NewClient in the event the provided // credentials didn't allow us to query account information using digest, basic or cookie // auth. ErrAuthenticationFailed = errors.New("failed to authenticate using the provided credentials") // ReParseURL is used to parse the url provided to NewClient(). This // regular expression contains five groups which capture the scheme, // username, password, hostname and port. If we parse the url with this // regular expression ReParseURL = regexp.MustCompile(`^(http|https)://(.+):(.+)@(.+):(\d+)(.*)$`) ) // NewClient returns a new Gerrit API client. gerritURL specifies the // HTTP endpoint of the Gerrit instance. For example, "http://localhost:8080/". // If gerritURL does not have a trailing slash, one is added automatically. // If a nil httpClient is provided, http.DefaultClient will be used. // // The url may contain credentials, http://admin:secret@localhost:8081/ for // example. These credentials may either be a user name and password or // name and value as in the case of cookie based authentication. If the url contains // credentials then this function will attempt to validate the credentials before // returning the client. ErrAuthenticationFailed will be returned if the credentials // cannot be validated. The process of validating the credentials is relatively simple and // only requires that the provided user have permission to GET /a/accounts/self. func NewClient(gerritURL string, httpClient *http.Client) (*Client, error) { if httpClient == nil { httpClient = http.DefaultClient } endpoint := gerritURL if endpoint == "" { return nil, ErrNoInstanceGiven } hasAuth := false username := "" password := "" // Depending on the contents of the username and password the default // url.Parse may not work. The below is an example URL that // would end up being parsed incorrectly with url.Parse: // http://admin:ZOSOKjgV/kgEkN0bzPJp+oGeJLqpXykqWFJpon/Ckg@localhost:38607 // So instead of depending on url.Parse we'll try using a regular expression // first to match a specific pattern. If that ends up working we modify // the incoming endpoint to remove the username and password so the rest // of this function will run as expected. submatches := ReParseURL.FindAllStringSubmatch(endpoint, -1) if len(submatches) > 0 && len(submatches[0]) > 5 { submatch := submatches[0] username = submatch[2] password = submatch[3] endpoint = fmt.Sprintf( "%s://%s:%s%s", submatch[1], submatch[4], submatch[5], submatch[6]) hasAuth = true } baseURL, err := url.Parse(endpoint) if err != nil { return nil, err } if !strings.HasSuffix(baseURL.Path, "/") { baseURL.Path += "/" } // Note, if we retrieved the URL and password using the regular // expression above then the below code will do nothing. if baseURL.User != nil { username = baseURL.User.Username() parsedPassword, haspassword := baseURL.User.Password() // Catches cases like http://user@localhost:8081/ where no password // was at all. If a blank password is required if !haspassword { return nil, ErrUserProvidedWithoutPassword } password = parsedPassword // Reconstruct the url but without the username and password. baseURL, err = url.Parse( fmt.Sprintf("%s://%s%s", baseURL.Scheme, baseURL.Host, baseURL.RequestURI())) if err != nil { return nil, err } hasAuth = true } c := &Client{ client: httpClient, baseURL: baseURL, } c.Authentication = &AuthenticationService{client: c} c.Access = &AccessService{client: c} c.Accounts = &AccountsService{client: c} c.Changes = &ChangesService{client: c} c.Config = &ConfigService{client: c} c.Groups = &GroupsService{client: c} c.Plugins = &PluginsService{client: c} c.Projects = &ProjectsService{client: c} c.EventsLog = &EventsLogService{client: c} if hasAuth { // Digest auth (first since that's the default auth type) c.Authentication.SetDigestAuth(username, password) if success, err := checkAuth(c); success || err != nil { return c, err } // Basic auth c.Authentication.SetBasicAuth(username, password) if success, err := checkAuth(c); success || err != nil { return c, err } // Cookie auth c.Authentication.SetCookieAuth(username, password) if success, err := checkAuth(c); success || err != nil { return c, err } // Reset auth in case the consumer needs to do something special. c.Authentication.ResetAuth() return c, ErrAuthenticationFailed } return c, nil } // checkAuth is used by NewClient to check if the current credentials are // valid. If the response is 401 Unauthorized then the error will be discarded. func checkAuth(client *Client) (bool, error) { _, response, err := client.Accounts.GetAccount("self") switch err { case ErrWWWAuthenticateHeaderMissing: return false, nil case ErrWWWAuthenticateHeaderNotDigest: return false, nil default: // Response could be nil if the connection outright failed // or some other error occurred before we got a response. if response == nil && err != nil { return false, err } if err != nil && response.StatusCode == http.StatusUnauthorized { err = nil } return response.StatusCode == http.StatusOK, err } } // NewRequest creates an API request. // A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. // Relative URLs should always be specified without a preceding slash. // If specified, the value pointed to by body is JSON encoded and included as the request body. func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { // Build URL for request u, err := c.buildURLForRequest(urlStr) if err != nil { return nil, err } var buf io.ReadWriter if body != nil { buf = new(bytes.Buffer) err = json.NewEncoder(buf).Encode(body) if err != nil { return nil, err } } req, err := http.NewRequest(method, u, buf) if err != nil { return nil, err } // Apply Authentication if err := c.addAuthentication(req); err != nil { return nil, err } // Request compact JSON // See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/json") // TODO: Add gzip encoding // Accept-Encoding request header is set to gzip // See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output return req, nil } // NewRawPutRequest creates a raw PUT request and makes no attempt to encode // or marshal the body. Just passes it straight through. func (c *Client) NewRawPutRequest(urlStr string, body string) (*http.Request, error) { // Build URL for request u, err := c.buildURLForRequest(urlStr) if err != nil { return nil, err } buf := bytes.NewBuffer([]byte(body)) req, err := http.NewRequest("PUT", u, buf) if err != nil { return nil, err } // Apply Authentication if err := c.addAuthentication(req); err != nil { return nil, err } // Request compact JSON // See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/x-www-form-urlencoded") // TODO: Add gzip encoding // Accept-Encoding request header is set to gzip // See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output return req, nil } // Call is a combine function for Client.NewRequest and Client.Do. // // Most API methods are quite the same. // Get the URL, apply options, make a request, and get the response. // Without adding special headers or something. // To avoid a big amount of code duplication you can Client.Call. // // method is the HTTP method you want to call. // u is the URL you want to call. // body is the HTTP body. // v is the HTTP response. // // For more information read https://github.com/google/go-github/issues/234 func (c *Client) Call(method, u string, body interface{}, v interface{}) (*Response, error) { req, err := c.NewRequest(method, u, body) if err != nil { return nil, err } resp, err := c.Do(req, v) if err != nil { return resp, err } return resp, err } // buildURLForRequest will build the URL (as string) that will be called. // We need such a utility method, because the URL.Path needs to be escaped (partly). // // E.g. if a project is called via "projects/%s" and the project is named "plugin/delete-project" // there has to be "projects/plugin%25Fdelete-project" instead of "projects/plugin/delete-project". // The second url will return nothing. func (c *Client) buildURLForRequest(urlStr string) (string, error) { // If there is a "/" at the start, remove it. // TODO: It can be arranged for all callers of buildURLForRequest to never have a "/" prefix, // which can be ensured via tests. This is how it's done in go-github. // Then, this run-time check becomes unnecessary and can be removed. urlStr = strings.TrimPrefix(urlStr, "/") // If we are authenticated, let's apply the "a/" prefix, // but only if it has not already been applied. if c.Authentication.HasAuth() && !strings.HasPrefix(urlStr, "a/") { urlStr = "a/" + urlStr } rel, err := url.Parse(urlStr) if err != nil { return "", err } return c.baseURL.String() + rel.String(), nil } // Do sends an API request and returns the API response. // The API response is JSON decoded and stored in the value pointed to by v, // or returned as an error if an API error has occurred. // If v implements the io.Writer interface, the raw response body will be written to v, // without attempting to first decode it. func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { resp, err := c.client.Do(req) if err != nil { return nil, err } // Wrap response response := &Response{Response: resp} err = CheckResponse(resp) if err != nil { // even though there was an error, we still return the response // in case the caller wants to inspect it further return response, err } if v != nil { defer resp.Body.Close() // nolint: errcheck if w, ok := v.(io.Writer); ok { if _, err := io.Copy(w, resp.Body); err != nil { // nolint: vetshadow return nil, err } } else { var body []byte body, err = ioutil.ReadAll(resp.Body) if err != nil { // even though there was an error, we still return the response // in case the caller wants to inspect it further return response, err } body = RemoveMagicPrefixLine(body) err = json.Unmarshal(body, v) } } return response, err } func (c *Client) addAuthentication(req *http.Request) error { // Apply HTTP Basic Authentication if c.Authentication.HasBasicAuth() { req.SetBasicAuth(c.Authentication.name, c.Authentication.secret) return nil } // Apply HTTP Cookie if c.Authentication.HasCookieAuth() { req.AddCookie(&http.Cookie{ Name: c.Authentication.name, Value: c.Authentication.secret, }) return nil } // Apply Digest Authentication. If we're using digest based // authentication we need to make a request, process the // WWW-Authenticate header, then set the Authorization header on the // incoming request. We do not need to send a body along because // the request itself should fail first. if c.Authentication.HasDigestAuth() { uri, err := c.buildURLForRequest(req.URL.RequestURI()) if err != nil { return err } // WARNING: Don't use c.NewRequest here unless you like // infinite recursion. digestRequest, err := http.NewRequest(req.Method, uri, nil) digestRequest.Header.Set("Accept", "*/*") digestRequest.Header.Set("Content-Type", "application/json") if err != nil { return err } response, err := c.client.Do(digestRequest) if err != nil { return err } // When the function exits discard the rest of the // body and close it. This should cause go to // reuse the connection. defer io.Copy(ioutil.Discard, response.Body) // nolint: errcheck defer response.Body.Close() // nolint: errcheck if response.StatusCode == http.StatusUnauthorized { authorization, err := c.Authentication.digestAuthHeader(response) if err != nil { return err } req.Header.Set("Authorization", authorization) } } return nil } // DeleteRequest sends an DELETE API Request to urlStr with optional body. // It is a shorthand combination for Client.NewRequest with Client.Do. // // Relative URLs should always be specified without a preceding slash. // If specified, the value pointed to by body is JSON encoded and included as the request body. func (c *Client) DeleteRequest(urlStr string, body interface{}) (*Response, error) { req, err := c.NewRequest("DELETE", urlStr, body) if err != nil { return nil, err } return c.Do(req, nil) } // BaseURL returns the client's Gerrit instance HTTP endpoint. func (c *Client) BaseURL() url.URL { return *c.baseURL } // RemoveMagicPrefixLine removes the "magic prefix line" of Gerris JSON // response if present. The JSON response body starts with a magic prefix line // that must be stripped before feeding the rest of the response body to a JSON // parser. The reason for this is to prevent against Cross Site Script // Inclusion (XSSI) attacks. By default all standard Gerrit APIs include this // prefix line though some plugins may not. // // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api.html#output func RemoveMagicPrefixLine(body []byte) []byte { if bytes.HasPrefix(body, magicPrefix) { return body[5:] } return body } var magicPrefix = []byte(")]}'\n") // CheckResponse checks the API response for errors, and returns them if present. // A response is considered an error if it has a status code outside the 200 range. // API error responses are expected to have no response body. // // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api.html#response-codes func CheckResponse(r *http.Response) error { if c := r.StatusCode; 200 <= c && c <= 299 { return nil } // Some calls require an authentification // In such cases errors like: // API call to https://review.typo3.org/accounts/self failed: 403 Forbidden // will be thrown. err := fmt.Errorf("API call to %s failed: %s", r.Request.URL.String(), r.Status) return err } // queryParameterReplacements are values in a url, specifically the query // portion of the url, which should not be escaped before being sent to // Gerrit. Note, Gerrit itself does not escape these values when using the // search box so we shouldn't escape them either. var queryParameterReplacements = map[string]string{ "+": "GOGERRIT_URL_PLACEHOLDER_PLUS", ":": "GOGERRIT_URL_PLACEHOLDER_COLON"} // addOptions adds the parameters in opt as URL query parameters to s. // opt must be a struct whose fields may contain "url" tags. func addOptions(s string, opt interface{}) (string, error) { v := reflect.ValueOf(opt) if v.Kind() == reflect.Ptr && v.IsNil() { return s, nil } u, err := url.Parse(s) if err != nil { return s, err } qs, err := query.Values(opt) if err != nil { return s, err } // If the url contained one or more query parameters (q) then we need // to do some escaping on these values before Encode() is called. By // doing so we're ensuring that : and + don't get encoded which means // they'll be passed along to Gerrit as raw ascii. Without this Gerrit // could return 400 Bad Request depending on the query parameters. For // more complete information see this issue on GitHub: // https://github.com/andygrunwald/go-gerrit/issues/18 _, hasQuery := qs["q"] if hasQuery { values := []string{} for _, value := range qs["q"] { for key, replacement := range queryParameterReplacements { value = strings.Replace(value, key, replacement, -1) } values = append(values, value) } qs.Del("q") for _, value := range values { qs.Add("q", value) } } encoded := qs.Encode() if hasQuery { for key, replacement := range queryParameterReplacements { encoded = strings.Replace(encoded, replacement, key, -1) } } u.RawQuery = encoded return u.String(), nil } // getStringResponseWithoutOptions retrieved a single string Response for a GET request func getStringResponseWithoutOptions(client *Client, u string) (string, *Response, error) { v := new(string) resp, err := client.Call("GET", u, nil, v) return *v, resp, err }