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