1package common
2
3import (
4	"encoding/hex"
5	"encoding/json"
6	"fmt"
7	"log"
8	"net/http"
9	"net/http/httputil"
10	"strconv"
11	"strings"
12	"time"
13
14	"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
15	tchttp "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/http"
16	"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
17)
18
19type Client struct {
20	region          string
21	httpClient      *http.Client
22	httpProfile     *profile.HttpProfile
23	profile         *profile.ClientProfile
24	credential      *Credential
25	signMethod      string
26	unsignedPayload bool
27	debug           bool
28}
29
30func (c *Client) Send(request tchttp.Request, response tchttp.Response) (err error) {
31	if request.GetDomain() == "" {
32		domain := c.httpProfile.Endpoint
33		if domain == "" {
34			domain = tchttp.GetServiceDomain(request.GetService())
35		}
36		request.SetDomain(domain)
37	}
38
39	if request.GetHttpMethod() == "" {
40		request.SetHttpMethod(c.httpProfile.ReqMethod)
41	}
42
43	tchttp.CompleteCommonParams(request, c.GetRegion())
44
45	if c.signMethod == "HmacSHA1" || c.signMethod == "HmacSHA256" {
46		return c.sendWithSignatureV1(request, response)
47	} else {
48		return c.sendWithSignatureV3(request, response)
49	}
50}
51
52func (c *Client) sendWithSignatureV1(request tchttp.Request, response tchttp.Response) (err error) {
53	// TODO: not an elegant way, it should be done in common params, but finally it need to refactor
54	request.GetParams()["Language"] = c.profile.Language
55	err = tchttp.ConstructParams(request)
56	if err != nil {
57		return err
58	}
59	err = signRequest(request, c.credential, c.signMethod)
60	if err != nil {
61		return err
62	}
63	httpRequest, err := http.NewRequest(request.GetHttpMethod(), request.GetUrl(), request.GetBodyReader())
64	if err != nil {
65		return err
66	}
67	if request.GetHttpMethod() == "POST" {
68		httpRequest.Header["Content-Type"] = []string{"application/x-www-form-urlencoded"}
69	}
70	if c.debug {
71		outbytes, err := httputil.DumpRequest(httpRequest, true)
72		if err != nil {
73			log.Printf("[ERROR] dump request failed because %s", err)
74			return err
75		}
76		log.Printf("[DEBUG] http request = %s", outbytes)
77	}
78	httpResponse, err := c.httpClient.Do(httpRequest)
79	if err != nil {
80		msg := fmt.Sprintf("Fail to get response because %s", err)
81		return errors.NewTencentCloudSDKError("ClientError.NetworkError", msg, "")
82	}
83	err = tchttp.ParseFromHttpResponse(httpResponse, response)
84	return err
85}
86
87func (c *Client) sendWithSignatureV3(request tchttp.Request, response tchttp.Response) (err error) {
88	headers := map[string]string{
89		"Host":               request.GetDomain(),
90		"X-TC-Action":        request.GetAction(),
91		"X-TC-Version":       request.GetVersion(),
92		"X-TC-Timestamp":     request.GetParams()["Timestamp"],
93		"X-TC-RequestClient": request.GetParams()["RequestClient"],
94		"X-TC-Language":      c.profile.Language,
95	}
96	if c.region != "" {
97		headers["X-TC-Region"] = c.region
98	}
99	if c.credential.Token != "" {
100		headers["X-TC-Token"] = c.credential.Token
101	}
102	if request.GetHttpMethod() == "GET" {
103		headers["Content-Type"] = "application/x-www-form-urlencoded"
104	} else {
105		headers["Content-Type"] = "application/json"
106	}
107
108	// start signature v3 process
109
110	// build canonical request string
111	httpRequestMethod := request.GetHttpMethod()
112	canonicalURI := "/"
113	canonicalQueryString := ""
114	if httpRequestMethod == "GET" {
115		err = tchttp.ConstructParams(request)
116		if err != nil {
117			return err
118		}
119		params := make(map[string]string)
120		for key, value := range request.GetParams() {
121			params[key] = value
122		}
123		delete(params, "Action")
124		delete(params, "Version")
125		delete(params, "Nonce")
126		delete(params, "Region")
127		delete(params, "RequestClient")
128		delete(params, "Timestamp")
129		canonicalQueryString = tchttp.GetUrlQueriesEncoded(params)
130	}
131	canonicalHeaders := fmt.Sprintf("content-type:%s\nhost:%s\n", headers["Content-Type"], headers["Host"])
132	signedHeaders := "content-type;host"
133	requestPayload := ""
134	if httpRequestMethod == "POST" {
135		b, err := json.Marshal(request)
136		if err != nil {
137			return err
138		}
139		requestPayload = string(b)
140	}
141	hashedRequestPayload := ""
142	if c.unsignedPayload {
143		hashedRequestPayload = sha256hex("UNSIGNED-PAYLOAD")
144		headers["X-TC-Content-SHA256"] = "UNSIGNED-PAYLOAD"
145	} else {
146		hashedRequestPayload = sha256hex(requestPayload)
147	}
148	canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
149		httpRequestMethod,
150		canonicalURI,
151		canonicalQueryString,
152		canonicalHeaders,
153		signedHeaders,
154		hashedRequestPayload)
155	//log.Println("canonicalRequest:", canonicalRequest)
156
157	// build string to sign
158	algorithm := "TC3-HMAC-SHA256"
159	requestTimestamp := headers["X-TC-Timestamp"]
160	timestamp, _ := strconv.ParseInt(requestTimestamp, 10, 64)
161	t := time.Unix(timestamp, 0).UTC()
162	// must be the format 2006-01-02, ref to package time for more info
163	date := t.Format("2006-01-02")
164	credentialScope := fmt.Sprintf("%s/%s/tc3_request", date, request.GetService())
165	hashedCanonicalRequest := sha256hex(canonicalRequest)
166	string2sign := fmt.Sprintf("%s\n%s\n%s\n%s",
167		algorithm,
168		requestTimestamp,
169		credentialScope,
170		hashedCanonicalRequest)
171	//log.Println("string2sign", string2sign)
172
173	// sign string
174	secretDate := hmacsha256(date, "TC3"+c.credential.SecretKey)
175	secretService := hmacsha256(request.GetService(), secretDate)
176	secretKey := hmacsha256("tc3_request", secretService)
177	signature := hex.EncodeToString([]byte(hmacsha256(string2sign, secretKey)))
178	//log.Println("signature", signature)
179
180	// build authorization
181	authorization := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
182		algorithm,
183		c.credential.SecretId,
184		credentialScope,
185		signedHeaders,
186		signature)
187	//log.Println("authorization", authorization)
188
189	headers["Authorization"] = authorization
190	url := "https://" + request.GetDomain() + request.GetPath()
191	if canonicalQueryString != "" {
192		url = url + "?" + canonicalQueryString
193	}
194	httpRequest, err := http.NewRequest(httpRequestMethod, url, strings.NewReader(requestPayload))
195	if err != nil {
196		return err
197	}
198	for k, v := range headers {
199		httpRequest.Header[k] = []string{v}
200	}
201	if c.debug {
202		outbytes, err := httputil.DumpRequest(httpRequest, true)
203		if err != nil {
204			log.Printf("[ERROR] dump request failed because %s", err)
205			return err
206		}
207		log.Printf("[DEBUG] http request = %s", outbytes)
208	}
209	httpResponse, err := c.httpClient.Do(httpRequest)
210	if err != nil {
211		msg := fmt.Sprintf("Fail to get response because %s", err)
212		return errors.NewTencentCloudSDKError("ClientError.NetworkError", msg, "")
213	}
214	err = tchttp.ParseFromHttpResponse(httpResponse, response)
215	return err
216}
217
218func (c *Client) GetRegion() string {
219	return c.region
220}
221
222func (c *Client) Init(region string) *Client {
223	c.httpClient = &http.Client{}
224	c.region = region
225	c.signMethod = "TC3-HMAC-SHA256"
226	c.debug = false
227	log.SetFlags(log.LstdFlags | log.Lshortfile)
228	return c
229}
230
231func (c *Client) WithSecretId(secretId, secretKey string) *Client {
232	c.credential = NewCredential(secretId, secretKey)
233	return c
234}
235
236func (c *Client) WithCredential(cred *Credential) *Client {
237	c.credential = cred
238	return c
239}
240
241func (c *Client) WithProfile(clientProfile *profile.ClientProfile) *Client {
242	c.profile = clientProfile
243	c.signMethod = clientProfile.SignMethod
244	c.unsignedPayload = clientProfile.UnsignedPayload
245	c.httpProfile = clientProfile.HttpProfile
246	c.httpClient.Timeout = time.Duration(c.httpProfile.ReqTimeout) * time.Second
247	return c
248}
249
250func (c *Client) WithSignatureMethod(method string) *Client {
251	c.signMethod = method
252	return c
253}
254
255func (c *Client) WithHttpTransport(transport http.RoundTripper) *Client {
256	c.httpClient.Transport = transport
257	return c
258}
259
260func NewClientWithSecretId(secretId, secretKey, region string) (client *Client, err error) {
261	client = &Client{}
262	client.Init(region).WithSecretId(secretId, secretKey)
263	return
264}
265