client.go raw

   1  package base
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/base64"
   7  	"encoding/json"
   8  	"errors"
   9  	"fmt"
  10  	"io"
  11  	"io/ioutil"
  12  	"mime/multipart"
  13  	"net/http"
  14  	"net/url"
  15  	"os"
  16  	"strings"
  17  	"time"
  18  
  19  	"github.com/cenkalti/backoff/v4"
  20  	"golang.org/x/net/http/httpproxy"
  21  )
  22  
  23  const (
  24  	accessKey = "VOLC_ACCESSKEY"
  25  	secretKey = "VOLC_SECRETKEY"
  26  
  27  	// volc proxy
  28  	httpProxy     = "VOLC_HTTP_PROXY"
  29  	httpsProxy    = "VOLC_HTTPS_PROXY"
  30  	noProxy       = "VOLC_NO_PROXY"
  31  	requestMethod = "REQUEST_METHOD"
  32  
  33  	defaultScheme = "http"
  34  )
  35  
  36  var (
  37  	_GlobalClient   *http.Client
  38  	emptyBytes      []byte
  39  	emptyReadSeeker = bytes.Buffer{}
  40  )
  41  
  42  func volcProxy() func(req *http.Request) (*url.URL, error) {
  43  	c := &httpproxy.Config{
  44  		HTTPProxy:  os.Getenv(httpProxy),
  45  		HTTPSProxy: os.Getenv(httpsProxy),
  46  		NoProxy:    os.Getenv(noProxy),
  47  		CGI:        os.Getenv(requestMethod) != "",
  48  	}
  49  	p := c.ProxyFunc()
  50  	return func(req *http.Request) (*url.URL, error) { return p(req.URL) }
  51  }
  52  
  53  func init() {
  54  	_GlobalClient = &http.Client{
  55  		Transport: &http.Transport{
  56  			MaxIdleConns:        1000,
  57  			MaxIdleConnsPerHost: 100,
  58  			IdleConnTimeout:     10 * time.Second,
  59  			Proxy:               volcProxy(),
  60  		},
  61  	}
  62  }
  63  
  64  // Client
  65  type Client struct {
  66  	Client        *http.Client
  67  	ServiceInfo   *ServiceInfo
  68  	ApiInfoList   map[string]*ApiInfo
  69  	CustomTimeout time.Duration
  70  }
  71  
  72  // NewClient
  73  func NewClient(info *ServiceInfo, apiInfoList map[string]*ApiInfo) *Client {
  74  	client := &Client{Client: _GlobalClient, ServiceInfo: info.Clone(), ApiInfoList: apiInfoList}
  75  
  76  	if client.ServiceInfo.Scheme == "" {
  77  		client.ServiceInfo.Scheme = defaultScheme
  78  	}
  79  
  80  	if os.Getenv(accessKey) != "" && os.Getenv(secretKey) != "" {
  81  		client.ServiceInfo.Credentials.AccessKeyID = os.Getenv(accessKey)
  82  		client.ServiceInfo.Credentials.SecretAccessKey = os.Getenv(secretKey)
  83  	} else if _, err := os.Stat(os.Getenv("HOME") + "/.volc/config"); err == nil {
  84  		if content, err := ioutil.ReadFile(os.Getenv("HOME") + "/.volc/config"); err == nil {
  85  			m := make(map[string]string)
  86  			json.Unmarshal(content, &m)
  87  			if accessKey, ok := m["ak"]; ok {
  88  				client.ServiceInfo.Credentials.AccessKeyID = accessKey
  89  			}
  90  			if secretKey, ok := m["sk"]; ok {
  91  				client.ServiceInfo.Credentials.SecretAccessKey = secretKey
  92  			}
  93  		}
  94  	}
  95  	return client
  96  }
  97  
  98  func (serviceInfo *ServiceInfo) Clone() *ServiceInfo {
  99  	ret := new(ServiceInfo)
 100  	// base info
 101  	ret.Timeout = serviceInfo.Timeout
 102  	ret.Host = serviceInfo.Host
 103  	ret.Scheme = serviceInfo.Scheme
 104  
 105  	// credential
 106  	ret.Credentials = serviceInfo.Credentials.Clone()
 107  
 108  	// header
 109  	ret.Header = serviceInfo.Header.Clone()
 110  	return ret
 111  }
 112  
 113  func (cred Credentials) Clone() Credentials {
 114  	return Credentials{
 115  		Service:         cred.Service,
 116  		Region:          cred.Region,
 117  		SecretAccessKey: cred.SecretAccessKey,
 118  		AccessKeyID:     cred.AccessKeyID,
 119  		SessionToken:    cred.SessionToken,
 120  	}
 121  }
 122  
 123  // SetRetrySettings
 124  func (client *Client) SetRetrySettings(retrySettings *RetrySettings) {
 125  	if retrySettings != nil {
 126  		client.ServiceInfo.Retry = *retrySettings
 127  	}
 128  }
 129  
 130  // SetAccessKey
 131  func (client *Client) SetAccessKey(ak string) {
 132  	if ak != "" {
 133  		client.ServiceInfo.Credentials.AccessKeyID = ak
 134  	}
 135  }
 136  
 137  // SetSecretKey
 138  func (client *Client) SetSecretKey(sk string) {
 139  	if sk != "" {
 140  		client.ServiceInfo.Credentials.SecretAccessKey = sk
 141  	}
 142  }
 143  
 144  // SetSessionToken
 145  func (client *Client) SetSessionToken(token string) {
 146  	if token != "" {
 147  		client.ServiceInfo.Credentials.SessionToken = token
 148  	}
 149  }
 150  
 151  // SetHost
 152  func (client *Client) SetHost(host string) {
 153  	if host != "" {
 154  		client.ServiceInfo.Host = host
 155  	}
 156  }
 157  
 158  func (client *Client) SetScheme(scheme string) {
 159  	if scheme != "" {
 160  		client.ServiceInfo.Scheme = scheme
 161  	}
 162  }
 163  
 164  // SetCredential
 165  func (client *Client) SetCredential(c Credentials) {
 166  	if c.AccessKeyID != "" {
 167  		client.ServiceInfo.Credentials.AccessKeyID = c.AccessKeyID
 168  	}
 169  
 170  	if c.SecretAccessKey != "" {
 171  		client.ServiceInfo.Credentials.SecretAccessKey = c.SecretAccessKey
 172  	}
 173  
 174  	if c.Region != "" {
 175  		client.ServiceInfo.Credentials.Region = c.Region
 176  	}
 177  
 178  	if c.SessionToken != "" {
 179  		client.ServiceInfo.Credentials.SessionToken = c.SessionToken
 180  	}
 181  
 182  	if c.Service != "" {
 183  		client.ServiceInfo.Credentials.Service = c.Service
 184  	}
 185  }
 186  
 187  func (client *Client) SetTimeout(timeout time.Duration) {
 188  	if timeout > 0 {
 189  		client.ServiceInfo.Timeout = timeout
 190  	}
 191  }
 192  
 193  func (client *Client) SetCustomTimeout(timeout time.Duration) {
 194  	if timeout > 0 {
 195  		client.CustomTimeout = timeout
 196  	}
 197  }
 198  
 199  // GetSignUrl
 200  func (client *Client) GetSignUrl(api string, query url.Values) (string, error) {
 201  	apiInfo := client.ApiInfoList[api]
 202  
 203  	if apiInfo == nil {
 204  		return "", errors.New("The related api does not exist")
 205  	}
 206  
 207  	query = mergeQuery(query, apiInfo.Query)
 208  
 209  	u := url.URL{
 210  		Scheme:   client.ServiceInfo.Scheme,
 211  		Host:     client.ServiceInfo.Host,
 212  		Path:     apiInfo.Path,
 213  		RawQuery: query.Encode(),
 214  	}
 215  	req, err := http.NewRequest(strings.ToUpper(apiInfo.Method), u.String(), nil)
 216  
 217  	if err != nil {
 218  		return "", errors.New("Failed to build request")
 219  	}
 220  
 221  	return client.ServiceInfo.Credentials.SignUrl(req), nil
 222  }
 223  
 224  // SignSts2
 225  func (client *Client) SignSts2(inlinePolicy *Policy, expire time.Duration) (*SecurityToken2, error) {
 226  	var err error
 227  	sts := new(SecurityToken2)
 228  	if sts.AccessKeyID, sts.SecretAccessKey, err = createTempAKSK(); err != nil {
 229  		return nil, err
 230  	}
 231  
 232  	if expire < time.Minute {
 233  		expire = time.Minute
 234  	}
 235  
 236  	now := time.Now()
 237  	expireTime := now.Add(expire)
 238  	sts.CurrentTime = now.Format(time.RFC3339)
 239  	sts.ExpiredTime = expireTime.Format(time.RFC3339)
 240  
 241  	innerToken, err := createInnerToken(client.ServiceInfo.Credentials, sts, inlinePolicy, expireTime.Unix())
 242  	if err != nil {
 243  		return nil, err
 244  	}
 245  
 246  	b, _ := json.Marshal(innerToken)
 247  	sts.SessionToken = "STS2" + base64.StdEncoding.EncodeToString(b)
 248  	return sts, nil
 249  }
 250  
 251  // Query Initiate a Get query request
 252  func (client *Client) Query(api string, query url.Values) ([]byte, int, error) {
 253  	return client.CtxQuery(context.Background(), api, query)
 254  }
 255  
 256  func (client *Client) CtxQuery(ctx context.Context, api string, query url.Values) ([]byte, int, error) {
 257  	return client.request(ctx, api, query, emptyBytes, "")
 258  }
 259  
 260  // Json Initiate a Json post request
 261  func (client *Client) Json(api string, query url.Values, body string) ([]byte, int, error) {
 262  	return client.CtxJson(context.Background(), api, query, body)
 263  }
 264  
 265  func (client *Client) CtxJson(ctx context.Context, api string, query url.Values, body string) ([]byte, int, error) {
 266  	return client.request(ctx, api, query, []byte(body), "application/json")
 267  }
 268  func (client *Client) PostWithContentType(api string, query url.Values, body string, ct string) ([]byte, int, error) {
 269  	return client.CtxPostWithContentType(context.Background(), api, query, body, ct)
 270  }
 271  
 272  // CtxPostWithContentType Initiate a post request with a custom Content-Type, Content-Type cannot be empty
 273  func (client *Client) CtxPostWithContentType(ctx context.Context, api string, query url.Values, body string, ct string) ([]byte, int, error) {
 274  	return client.request(ctx, api, query, []byte(body), ct)
 275  }
 276  
 277  func (client *Client) Post(api string, query url.Values, form url.Values) ([]byte, int, error) {
 278  	return client.CtxPost(context.Background(), api, query, form)
 279  }
 280  
 281  // CtxPost Initiate a Post request
 282  func (client *Client) CtxPost(ctx context.Context, api string, query url.Values, form url.Values) ([]byte, int, error) {
 283  	apiInfo := client.ApiInfoList[api]
 284  	form = mergeQuery(form, apiInfo.Form)
 285  	return client.request(ctx, api, query, []byte(form.Encode()), "application/x-www-form-urlencoded")
 286  }
 287  
 288  func (client *Client) CtxMultiPart(ctx context.Context, api string, query url.Values, form []*MultiPartItem) ([]byte, int, error) {
 289  	body := &bytes.Buffer{}
 290  	writer := multipart.NewWriter(body)
 291  	for _, item := range form {
 292  		part, err := writer.CreatePart(item.header)
 293  		if err != nil {
 294  			return nil, 400, err
 295  		}
 296  		_, err = io.Copy(part, item.data)
 297  		if err != nil {
 298  			return nil, 400, err
 299  		}
 300  	}
 301  	writer.Close()
 302  	return client.request(ctx, api, query, body.Bytes(), writer.FormDataContentType())
 303  }
 304  
 305  func (client *Client) makeRequest(inputContext context.Context, api string, req *http.Request, timeout time.Duration) ([]byte, int, error, bool) {
 306  	req = client.ServiceInfo.Credentials.Sign(req)
 307  
 308  	ctx := inputContext
 309  	if ctx == nil {
 310  		ctx = context.Background()
 311  	}
 312  
 313  	ctx, cancel := context.WithTimeout(ctx, timeout)
 314  	defer cancel()
 315  	req = req.WithContext(ctx)
 316  	resp, err := client.Client.Do(req)
 317  	if err != nil {
 318  		// should retry when client sends request error.
 319  		return []byte(""), 500, err, true
 320  	}
 321  	defer resp.Body.Close()
 322  
 323  	body, err := ioutil.ReadAll(resp.Body)
 324  	if err != nil {
 325  		return []byte(""), resp.StatusCode, err, false
 326  	}
 327  
 328  	if resp.StatusCode < 200 || resp.StatusCode > 299 {
 329  		needRetry := false
 330  		// should retry when server returns 5xx error.
 331  		if resp.StatusCode >= http.StatusInternalServerError {
 332  			needRetry = true
 333  		}
 334  		return body, resp.StatusCode, fmt.Errorf("api %s http code %d body %s", api, resp.StatusCode, string(body)), needRetry
 335  	}
 336  
 337  	return body, resp.StatusCode, nil, false
 338  }
 339  
 340  func (client *Client) request(ctx context.Context, api string, query url.Values, body []byte, ct string) ([]byte, int, error) {
 341  	apiInfo := client.ApiInfoList[api]
 342  
 343  	if apiInfo == nil {
 344  		return []byte(""), 500, errors.New("The related api does not exist")
 345  	}
 346  	return client.requestThumb(ctx, api, apiInfo, query, body, ct)
 347  }
 348  
 349  func (client *Client) requestThumb(ctx context.Context, api string, apiInfo *ApiInfo, query url.Values, body []byte, ct string) ([]byte, int, error) {
 350  	timeout := getTimeout(client.ServiceInfo.Timeout, apiInfo.Timeout, client.CustomTimeout)
 351  	header := mergeHeader(client.ServiceInfo.Header, apiInfo.Header)
 352  	query = mergeQuery(query, apiInfo.Query)
 353  	retrySettings := getRetrySetting(&client.ServiceInfo.Retry, &apiInfo.Retry)
 354  
 355  	u := url.URL{
 356  		Scheme:   client.ServiceInfo.Scheme,
 357  		Host:     client.ServiceInfo.Host,
 358  		Path:     apiInfo.Path,
 359  		RawQuery: query.Encode(),
 360  	}
 361  	requestBody := bytes.NewReader(body)
 362  	req, err := http.NewRequest(strings.ToUpper(apiInfo.Method), u.String(), nil)
 363  	if err != nil {
 364  		return []byte(""), 500, fmt.Errorf("Failed to build request, err %w", err)
 365  	}
 366  	req.Header = header
 367  	if ct != "" {
 368  		req.Header.Set("Content-Type", ct)
 369  	}
 370  	// Because service info could be changed by SetRegion, so set UA header for every request here.
 371  	req.Header.Set("User-Agent", strings.Join([]string{SDKName, SDKVersion}, "/"))
 372  
 373  	var resp []byte
 374  	var code int
 375  
 376  	err = backoff.Retry(func() error {
 377  		_, err = requestBody.Seek(0, io.SeekStart)
 378  		if err != nil {
 379  			// if seek failed, stop retry.
 380  			return backoff.Permanent(err)
 381  		}
 382  		req.Body = ioutil.NopCloser(requestBody)
 383  		var needRetry bool
 384  		resp, code, err, needRetry = client.makeRequest(ctx, api, req, timeout)
 385  		if needRetry {
 386  			return err
 387  		} else {
 388  			return backoff.Permanent(err)
 389  		}
 390  	}, backoff.WithMaxRetries(backoff.NewConstantBackOff(*retrySettings.RetryInterval), *retrySettings.RetryTimes))
 391  	return resp, code, err
 392  }
 393  
 394  func (client *Client) CtxQueryThumb(ctx context.Context, api string, apiInfo *ApiInfo, query url.Values) ([]byte, int, error) {
 395  	return client.requestThumb(ctx, api, apiInfo, query, emptyBytes, "")
 396  }
 397  
 398  func (client *Client) CtxJsonThumb(ctx context.Context, api string, apiInfo *ApiInfo, query url.Values, body []byte) ([]byte, int, error) {
 399  	return client.requestThumb(ctx, api, apiInfo, query, body, "application/json")
 400  }
 401