internal/apiclient/httpclient.go (224 lines of code) (raw):
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package apiclient
import (
"bytes"
"context"
"encoding/json"
"errors"
"internal/clilog"
"io"
"net/http"
"net/url"
"time"
"golang.org/x/time/rate"
)
// RateLimitedHttpClient
type RateLimitedHTTPClient struct {
client *http.Client
Ratelimiter *rate.Limiter
}
// allow 6 every 1 second (360 per min, limit is 480 per min)
var integrationAPIRateLimit = rate.NewLimiter(rate.Every(time.Second), 6)
// allow 1 every 1 second (60 per min, limit is 120 per min)
var connectorsAPIRateLimit = rate.NewLimiter(rate.Every(time.Second), 1)
// disable rate limit
var noAPIRateLimit = rate.NewLimiter(rate.Inf, 1)
// HttpClient method is used to GET,POST,PUT or DELETE JSON data
func HttpClient(params ...string) (respBody []byte, err error) {
// The first parameter is url. If only one parameter is sent, assume GET
// The second parameter is the payload. The two parameters are sent, assume POST
// THe third parameter is the method. If three parameters are sent, assume method in param
// The fourth parameter is content type
var req *http.Request
contentType := "application/json"
client, err := getHttpClient()
if err != nil {
return nil, err
}
clilog.Debug.Println("Connecting to: ", params[0])
ctx := context.Background()
switch paramLen := len(params); paramLen {
case 1:
clilog.Debug.Println("Method: GET")
req, err = http.NewRequestWithContext(ctx, http.MethodGet, params[0], nil)
case 2:
// some POST functions don't have a body
clilog.Debug.Println("Method: POST")
if len([]byte(params[1])) > 0 {
payload, _ := PrettifyJson([]byte(params[1]))
clilog.Debug.Println("Payload: ", string(payload))
}
req, err = http.NewRequestWithContext(ctx, http.MethodPost, params[0], bytes.NewBuffer([]byte(params[1])))
case 3:
if req, err = getRequest(params); err != nil {
return nil, err
}
case 4:
if req, err = getRequest(params); err != nil {
return nil, err
}
contentType = params[3]
default:
return nil, errors.New("unsupported method")
}
if err != nil {
clilog.Error.Println("error in client: ", err)
return nil, err
}
req, err = setAuthHeader(req)
if err != nil {
return nil, err
}
clilog.Debug.Println("Content-Type : ", contentType)
req.Header.Set("Content-Type", contentType)
if DryRun() {
return nil, nil
}
resp, err := client.Do(req)
if err != nil {
clilog.Error.Println("error connecting: ", err)
return nil, err
}
return handleResponse(resp)
}
// PrettyPrint method prints formatted json
func PrettyPrint(body []byte) error {
if GetCmdPrintHttpResponseSetting() && ClientPrintHttpResponse.Get() {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, body, "", "\t")
if err != nil {
clilog.Error.Println("error parsing response: ", err)
return err
}
clilog.HTTPResponse.Println(prettyJSON.String())
}
clilog.Debug.Println(string(body))
return nil
}
func PrettifyJson(body []byte) (prettyJson []byte, err error) {
prettyJSON := bytes.Buffer{}
err = json.Indent(&prettyJSON, body, "", "\t")
if err != nil {
clilog.Error.Printf("error parsing json response: %v, the original response was: %s\n", err, string(body))
return nil, err
}
return prettyJSON.Bytes(), err
}
func getRequest(params []string) (req *http.Request, err error) {
ctx := context.Background()
if params[2] == "DELETE" {
clilog.Debug.Println("Method: DELETE")
req, err = http.NewRequestWithContext(ctx, http.MethodDelete, params[0], nil)
} else if params[2] == "PUT" {
clilog.Debug.Println("Method: PUT")
clilog.Debug.Println("Payload: ", params[1])
req, err = http.NewRequestWithContext(ctx, http.MethodPut, params[0], bytes.NewBuffer([]byte(params[1])))
} else if params[2] == "PATCH" {
clilog.Debug.Println("Method: PATCH")
clilog.Debug.Println("Payload: ", params[1])
req, err = http.NewRequestWithContext(ctx, http.MethodPatch, params[0], bytes.NewBuffer([]byte(params[1])))
} else if params[2] == "POST" {
clilog.Debug.Println("Method: POST")
clilog.Debug.Println("Payload: ", params[1])
req, err = http.NewRequestWithContext(ctx, http.MethodPost, params[0], bytes.NewBuffer([]byte(params[1])))
} else {
return nil, errors.New("unsupported method")
}
return req, err
}
func setAuthHeader(req *http.Request) (*http.Request, error) {
if GetIntegrationToken() == "" {
if err := SetAccessToken(); err != nil {
return nil, err
}
}
clilog.Debug.Println("Setting token : ", GetIntegrationToken())
req.Header.Add("Authorization", "Bearer "+GetIntegrationToken())
return req, nil
}
// Do the HTTP request
func (c *RateLimitedHTTPClient) Do(req *http.Request) (*http.Response, error) {
ctx := context.Background()
// Wait until the rate is below Apigee limits
err := c.Ratelimiter.Wait(ctx)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
func getHttpClient() (client *RateLimitedHTTPClient, err error) {
var apiRateLimit *rate.Limiter
switch r := GetRate(); r {
case IntegrationAPI:
apiRateLimit = integrationAPIRateLimit
case ConnectorsAPI:
apiRateLimit = connectorsAPIRateLimit
case None:
apiRateLimit = noAPIRateLimit
default:
apiRateLimit = noAPIRateLimit
}
if GetProxyURL() != "" {
if proxyUrl, err := url.Parse(GetProxyURL()); err != nil {
integrationCLIAPIClient := &RateLimitedHTTPClient{
client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyUrl),
},
},
Ratelimiter: apiRateLimit,
}
return integrationCLIAPIClient, err
}
return nil, err
} else {
integrationCLIAPIClient := &RateLimitedHTTPClient{
client: http.DefaultClient,
Ratelimiter: apiRateLimit,
}
return integrationCLIAPIClient, nil
}
}
func handleResponse(resp *http.Response) (respBody []byte, err error) {
if resp != nil {
defer resp.Body.Close()
}
if resp == nil {
clilog.Error.Println("error in response: Response was null")
return nil, nil
}
respBody, err = io.ReadAll(resp.Body)
if err != nil {
clilog.Error.Printf("error in response: %v\n", err)
return nil, err
} else if resp.StatusCode > 399 {
if GetConflictsAsErrors() && resp.StatusCode == http.StatusConflict {
clilog.Warning.Printf("entity already exists, ignoring conflict")
return respBody, nil
}
clilog.Debug.Printf("status code %d, error in response: %s\n", resp.StatusCode, string(respBody))
clilog.HTTPError.Println(string(respBody))
return nil, errors.New(getErrorMessage(resp.StatusCode) + ": " + string(respBody))
}
return respBody, PrettyPrint(respBody)
}
func getErrorMessage(statusCode int) string {
switch statusCode {
case 400:
return "Bad Request - malformed request syntax"
case 401:
return "Unauthorized - the client must authenticate itself"
case 403:
return "Forbidden - the client does not have access rights"
case 404:
return "Not found - the server cannot find the requested resource"
case 405:
return "Method Not Allowed - the request method is not supported by the target resource"
case 409:
return "Conflict - request conflicts with the current state of the server"
case 415:
return "Unsupported media type - media format of the requested data is not supported by the server"
case 429:
return "Too Many Request - user has sent too many requests"
case 500:
return "Internal server error"
case 501:
return "Not Implemented - request method is not supported by the server"
case 502:
return "Bad Gateway"
case 503:
return "Service Unavaliable - the server is not ready to handle the request"
default:
return "unknown error"
}
}