1package common
2
3import (
4	"bytes"
5	"encoding/json"
6	"errors"
7	"fmt"
8	"io/ioutil"
9	"log"
10	"net/http"
11	"net/url"
12	"os"
13	"strconv"
14	"strings"
15	"time"
16
17	"github.com/denverdino/aliyungo/util"
18)
19
20// RemovalPolicy.N add index to array item
21// RemovalPolicy=["a", "b"] => RemovalPolicy.1="a" RemovalPolicy.2="b"
22type FlattenArray []string
23
24// string contains underline which will be replaced with dot
25// SystemDisk_Category => SystemDisk.Category
26type UnderlineString string
27
28// A Client represents a client of ECS services
29type Client struct {
30	AccessKeyId     string //Access Key Id
31	AccessKeySecret string //Access Key Secret
32	securityToken   string
33	debug           bool
34	httpClient      *http.Client
35	endpoint        string
36	version         string
37	serviceCode     string
38	regionID        Region
39	businessInfo    string
40	userAgent       string
41}
42
43// Initialize properties of a client instance
44func (client *Client) Init(endpoint, version, accessKeyId, accessKeySecret string) {
45	client.AccessKeyId = accessKeyId
46	ak := accessKeySecret
47	if !strings.HasSuffix(ak, "&") {
48		ak += "&"
49	}
50	client.AccessKeySecret = ak
51	client.debug = false
52	handshakeTimeout, err := strconv.Atoi(os.Getenv("TLSHandshakeTimeout"))
53	if err != nil {
54		handshakeTimeout = 0
55	}
56	if handshakeTimeout == 0 {
57		client.httpClient = &http.Client{}
58	} else {
59		t := &http.Transport{
60			TLSHandshakeTimeout: time.Duration(handshakeTimeout) * time.Second}
61		client.httpClient = &http.Client{Transport: t}
62	}
63	client.endpoint = endpoint
64	client.version = version
65}
66
67// Initialize properties of a client instance including regionID
68func (client *Client) NewInit(endpoint, version, accessKeyId, accessKeySecret, serviceCode string, regionID Region) {
69	client.Init(endpoint, version, accessKeyId, accessKeySecret)
70	client.serviceCode = serviceCode
71	client.regionID = regionID
72}
73
74// Intialize client object when all properties are ready
75func (client *Client) InitClient() *Client {
76	client.debug = false
77	handshakeTimeout, err := strconv.Atoi(os.Getenv("TLSHandshakeTimeout"))
78	if err != nil {
79		handshakeTimeout = 0
80	}
81	if handshakeTimeout == 0 {
82		client.httpClient = &http.Client{}
83	} else {
84		t := &http.Transport{
85			TLSHandshakeTimeout: time.Duration(handshakeTimeout) * time.Second}
86		client.httpClient = &http.Client{Transport: t}
87	}
88	return client
89}
90
91func (client *Client) NewInitForAssumeRole(endpoint, version, accessKeyId, accessKeySecret, serviceCode string, regionID Region, securityToken string) {
92	client.NewInit(endpoint, version, accessKeyId, accessKeySecret, serviceCode, regionID)
93	client.securityToken = securityToken
94}
95
96//getLocationEndpoint
97func (client *Client) getEndpointByLocation() string {
98	locationClient := NewLocationClient(client.AccessKeyId, client.AccessKeySecret, client.securityToken)
99	locationClient.SetDebug(true)
100	return locationClient.DescribeOpenAPIEndpoint(client.regionID, client.serviceCode)
101}
102
103//NewClient using location service
104func (client *Client) setEndpointByLocation(region Region, serviceCode, accessKeyId, accessKeySecret, securityToken string) {
105	locationClient := NewLocationClient(accessKeyId, accessKeySecret, securityToken)
106	locationClient.SetDebug(true)
107	ep := locationClient.DescribeOpenAPIEndpoint(region, serviceCode)
108	if ep == "" {
109		ep = loadEndpointFromFile(region, serviceCode)
110	}
111
112	if ep != "" {
113		client.endpoint = ep
114	}
115}
116
117// Ensure all necessary properties are valid
118func (client *Client) ensureProperties() error {
119	var msg string
120
121	if client.endpoint == "" {
122		msg = fmt.Sprintf("endpoint cannot be empty!")
123	} else if client.version == "" {
124		msg = fmt.Sprintf("version cannot be empty!")
125	} else if client.AccessKeyId == "" {
126		msg = fmt.Sprintf("AccessKeyId cannot be empty!")
127	} else if client.AccessKeySecret == "" {
128		msg = fmt.Sprintf("AccessKeySecret cannot be empty!")
129	}
130
131	if msg != "" {
132		return errors.New(msg)
133	}
134
135	return nil
136}
137
138// ----------------------------------------------------
139// WithXXX methods
140// ----------------------------------------------------
141
142// WithEndpoint sets custom endpoint
143func (client *Client) WithEndpoint(endpoint string) *Client {
144	client.SetEndpoint(endpoint)
145	return client
146}
147
148// WithVersion sets custom version
149func (client *Client) WithVersion(version string) *Client {
150	client.SetVersion(version)
151	return client
152}
153
154// WithRegionID sets Region ID
155func (client *Client) WithRegionID(regionID Region) *Client {
156	client.SetRegionID(regionID)
157	return client
158}
159
160//WithServiceCode sets serviceCode
161func (client *Client) WithServiceCode(serviceCode string) *Client {
162	client.SetServiceCode(serviceCode)
163	return client
164}
165
166// WithAccessKeyId sets new AccessKeyId
167func (client *Client) WithAccessKeyId(id string) *Client {
168	client.SetAccessKeyId(id)
169	return client
170}
171
172// WithAccessKeySecret sets new AccessKeySecret
173func (client *Client) WithAccessKeySecret(secret string) *Client {
174	client.SetAccessKeySecret(secret)
175	return client
176}
177
178// WithSecurityToken sets securityToken
179func (client *Client) WithSecurityToken(securityToken string) *Client {
180	client.SetSecurityToken(securityToken)
181	return client
182}
183
184// WithDebug sets debug mode to log the request/response message
185func (client *Client) WithDebug(debug bool) *Client {
186	client.SetDebug(debug)
187	return client
188}
189
190// WithBusinessInfo sets business info to log the request/response message
191func (client *Client) WithBusinessInfo(businessInfo string) *Client {
192	client.SetBusinessInfo(businessInfo)
193	return client
194}
195
196// WithUserAgent sets user agent to the request/response message
197func (client *Client) WithUserAgent(userAgent string) *Client {
198	client.SetUserAgent(userAgent)
199	return client
200}
201
202// ----------------------------------------------------
203// SetXXX methods
204// ----------------------------------------------------
205
206// SetEndpoint sets custom endpoint
207func (client *Client) SetEndpoint(endpoint string) {
208	client.endpoint = endpoint
209}
210
211// SetEndpoint sets custom version
212func (client *Client) SetVersion(version string) {
213	client.version = version
214}
215
216// SetEndpoint sets Region ID
217func (client *Client) SetRegionID(regionID Region) {
218	client.regionID = regionID
219}
220
221//SetServiceCode sets serviceCode
222func (client *Client) SetServiceCode(serviceCode string) {
223	client.serviceCode = serviceCode
224}
225
226// SetAccessKeyId sets new AccessKeyId
227func (client *Client) SetAccessKeyId(id string) {
228	client.AccessKeyId = id
229}
230
231// SetAccessKeySecret sets new AccessKeySecret
232func (client *Client) SetAccessKeySecret(secret string) {
233	client.AccessKeySecret = secret + "&"
234}
235
236// SetDebug sets debug mode to log the request/response message
237func (client *Client) SetDebug(debug bool) {
238	client.debug = debug
239}
240
241// SetBusinessInfo sets business info to log the request/response message
242func (client *Client) SetBusinessInfo(businessInfo string) {
243	if strings.HasPrefix(businessInfo, "/") {
244		client.businessInfo = businessInfo
245	} else if businessInfo != "" {
246		client.businessInfo = "/" + businessInfo
247	}
248}
249
250// SetUserAgent sets user agent to the request/response message
251func (client *Client) SetUserAgent(userAgent string) {
252	client.userAgent = userAgent
253}
254
255//set SecurityToken
256func (client *Client) SetSecurityToken(securityToken string) {
257	client.securityToken = securityToken
258}
259
260func (client *Client) initEndpoint() error {
261	// if set any value to "CUSTOMIZED_ENDPOINT" could skip location service.
262	// example: export CUSTOMIZED_ENDPOINT=true
263	if os.Getenv("CUSTOMIZED_ENDPOINT") != "" {
264		return nil
265	}
266
267	if client.serviceCode != "" && client.regionID != "" {
268		endpoint := client.getEndpointByLocation()
269		if endpoint == "" {
270			return GetCustomError("InvalidEndpoint", "endpoint is empty,pls check")
271		}
272		client.endpoint = endpoint
273	}
274	return nil
275}
276
277// Invoke sends the raw HTTP request for ECS services
278func (client *Client) Invoke(action string, args interface{}, response interface{}) error {
279	if err := client.ensureProperties(); err != nil {
280		return err
281	}
282
283	//init endpoint
284	if err := client.initEndpoint(); err != nil {
285		return err
286	}
287
288	request := Request{}
289	request.init(client.version, action, client.AccessKeyId, client.securityToken, client.regionID)
290
291	query := util.ConvertToQueryValues(request)
292	util.SetQueryValues(args, &query)
293
294	// Sign request
295	signature := util.CreateSignatureForRequest(ECSRequestMethod, &query, client.AccessKeySecret)
296
297	// Generate the request URL
298	requestURL := client.endpoint + "?" + query.Encode() + "&Signature=" + url.QueryEscape(signature)
299
300	httpReq, err := http.NewRequest(ECSRequestMethod, requestURL, nil)
301
302	if err != nil {
303		return GetClientError(err)
304	}
305
306	// TODO move to util and add build val flag
307	httpReq.Header.Set("X-SDK-Client", `AliyunGO/`+Version+client.businessInfo)
308
309	httpReq.Header.Set("User-Agent", httpReq.UserAgent()+" "+client.userAgent)
310
311	t0 := time.Now()
312	httpResp, err := client.httpClient.Do(httpReq)
313	t1 := time.Now()
314	if err != nil {
315		return GetClientError(err)
316	}
317	statusCode := httpResp.StatusCode
318
319	if client.debug {
320		log.Printf("Invoke %s %s %d (%v)", ECSRequestMethod, requestURL, statusCode, t1.Sub(t0))
321	}
322
323	defer httpResp.Body.Close()
324	body, err := ioutil.ReadAll(httpResp.Body)
325
326	if err != nil {
327		return GetClientError(err)
328	}
329
330	if client.debug {
331		var prettyJSON bytes.Buffer
332		err = json.Indent(&prettyJSON, body, "", "    ")
333		log.Println(string(prettyJSON.Bytes()))
334	}
335
336	if statusCode >= 400 && statusCode <= 599 {
337		errorResponse := ErrorResponse{}
338		err = json.Unmarshal(body, &errorResponse)
339		ecsError := &Error{
340			ErrorResponse: errorResponse,
341			StatusCode:    statusCode,
342		}
343		return ecsError
344	}
345
346	err = json.Unmarshal(body, response)
347	//log.Printf("%++v", response)
348	if err != nil {
349		return GetClientError(err)
350	}
351
352	return nil
353}
354
355// Invoke sends the raw HTTP request for ECS services
356func (client *Client) InvokeByFlattenMethod(action string, args interface{}, response interface{}) error {
357	if err := client.ensureProperties(); err != nil {
358		return err
359	}
360
361	//init endpoint
362	if err := client.initEndpoint(); err != nil {
363		return err
364	}
365
366	request := Request{}
367	request.init(client.version, action, client.AccessKeyId, client.securityToken, client.regionID)
368
369	query := util.ConvertToQueryValues(request)
370
371	util.SetQueryValueByFlattenMethod(args, &query)
372
373	// Sign request
374	signature := util.CreateSignatureForRequest(ECSRequestMethod, &query, client.AccessKeySecret)
375
376	// Generate the request URL
377	requestURL := client.endpoint + "?" + query.Encode() + "&Signature=" + url.QueryEscape(signature)
378
379	httpReq, err := http.NewRequest(ECSRequestMethod, requestURL, nil)
380
381	if err != nil {
382		return GetClientError(err)
383	}
384
385	// TODO move to util and add build val flag
386	httpReq.Header.Set("X-SDK-Client", `AliyunGO/`+Version+client.businessInfo)
387
388	httpReq.Header.Set("User-Agent", httpReq.UserAgent()+" "+client.userAgent)
389
390	t0 := time.Now()
391	httpResp, err := client.httpClient.Do(httpReq)
392	t1 := time.Now()
393	if err != nil {
394		return GetClientError(err)
395	}
396	statusCode := httpResp.StatusCode
397
398	if client.debug {
399		log.Printf("Invoke %s %s %d (%v)", ECSRequestMethod, requestURL, statusCode, t1.Sub(t0))
400	}
401
402	defer httpResp.Body.Close()
403	body, err := ioutil.ReadAll(httpResp.Body)
404
405	if err != nil {
406		return GetClientError(err)
407	}
408
409	if client.debug {
410		var prettyJSON bytes.Buffer
411		err = json.Indent(&prettyJSON, body, "", "    ")
412		log.Println(string(prettyJSON.Bytes()))
413	}
414
415	if statusCode >= 400 && statusCode <= 599 {
416		errorResponse := ErrorResponse{}
417		err = json.Unmarshal(body, &errorResponse)
418		ecsError := &Error{
419			ErrorResponse: errorResponse,
420			StatusCode:    statusCode,
421		}
422		return ecsError
423	}
424
425	err = json.Unmarshal(body, response)
426	//log.Printf("%++v", response)
427	if err != nil {
428		return GetClientError(err)
429	}
430
431	return nil
432}
433
434// Invoke sends the raw HTTP request for ECS services
435//改进了一下上面那个方法,可以使用各种Http方法
436//2017.1.30 增加了一个path参数,用来拓展访问的地址
437func (client *Client) InvokeByAnyMethod(method, action, path string, args interface{}, response interface{}) error {
438	if err := client.ensureProperties(); err != nil {
439		return err
440	}
441
442	//init endpoint
443	if err := client.initEndpoint(); err != nil {
444		return err
445	}
446
447	request := Request{}
448	request.init(client.version, action, client.AccessKeyId, client.securityToken, client.regionID)
449	data := util.ConvertToQueryValues(request)
450	util.SetQueryValues(args, &data)
451
452	// Sign request
453	signature := util.CreateSignatureForRequest(method, &data, client.AccessKeySecret)
454
455	data.Add("Signature", signature)
456	// Generate the request URL
457	var (
458		httpReq *http.Request
459		err     error
460	)
461	if method == http.MethodGet {
462		requestURL := client.endpoint + path + "?" + data.Encode()
463		//fmt.Println(requestURL)
464		httpReq, err = http.NewRequest(method, requestURL, nil)
465	} else {
466		//fmt.Println(client.endpoint + path)
467		httpReq, err = http.NewRequest(method, client.endpoint+path, strings.NewReader(data.Encode()))
468		httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
469	}
470
471	if err != nil {
472		return GetClientError(err)
473	}
474
475	// TODO move to util and add build val flag
476	httpReq.Header.Set("X-SDK-Client", `AliyunGO/`+Version+client.businessInfo)
477	httpReq.Header.Set("User-Agent", httpReq.Header.Get("User-Agent")+" "+client.userAgent)
478
479	t0 := time.Now()
480	httpResp, err := client.httpClient.Do(httpReq)
481	t1 := time.Now()
482	if err != nil {
483		return GetClientError(err)
484	}
485	statusCode := httpResp.StatusCode
486
487	if client.debug {
488		log.Printf("Invoke %s %s %d (%v) %v", ECSRequestMethod, client.endpoint, statusCode, t1.Sub(t0), data.Encode())
489	}
490
491	defer httpResp.Body.Close()
492	body, err := ioutil.ReadAll(httpResp.Body)
493
494	if err != nil {
495		return GetClientError(err)
496	}
497
498	if client.debug {
499		var prettyJSON bytes.Buffer
500		err = json.Indent(&prettyJSON, body, "", "    ")
501		log.Println(string(prettyJSON.Bytes()))
502	}
503
504	if statusCode >= 400 && statusCode <= 599 {
505		errorResponse := ErrorResponse{}
506		err = json.Unmarshal(body, &errorResponse)
507		ecsError := &Error{
508			ErrorResponse: errorResponse,
509			StatusCode:    statusCode,
510		}
511		return ecsError
512	}
513
514	err = json.Unmarshal(body, response)
515	//log.Printf("%++v", response)
516	if err != nil {
517		return GetClientError(err)
518	}
519
520	return nil
521}
522
523// GenerateClientToken generates the Client Token with random string
524func (client *Client) GenerateClientToken() string {
525	return util.CreateRandomString()
526}
527
528func GetClientErrorFromString(str string) error {
529	return &Error{
530		ErrorResponse: ErrorResponse{
531			Code:    "AliyunGoClientFailure",
532			Message: str,
533		},
534		StatusCode: -1,
535	}
536}
537
538func GetClientError(err error) error {
539	return GetClientErrorFromString(err.Error())
540}
541
542func GetCustomError(code, message string) error {
543	return &Error{
544		ErrorResponse: ErrorResponse{
545			Code:    code,
546			Message: message,
547		},
548		StatusCode: 400,
549	}
550}
551