| 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 |
| } |