tencent_sdk/
client.rs

1use chrono::{TimeZone, Utc};
2use reqwest::header::{HeaderMap, HeaderValue};
3use reqwest::Client;
4use serde_json::Value;
5use sha2::{Digest, Sha256};
6use std::error::Error;
7
8use crate::signing::hmac_sha256;
9use hmac::{Hmac, Mac};
10
11type HmacSha256 = Hmac<Sha256>;
12
13/// Tencent Cloud SDK Client
14///
15/// This client is used to send authenticated requests to Tencent Cloud APIs.
16/// It constructs the canonical request string, computes the TC3-HMAC-SHA256 signature,
17/// builds the necessary headers, and sends an HTTPS POST request.
18/// This version returns the response as a `serde_json::Value`, preserving Chinese characters.
19pub struct TencentCloudClient {
20    /// Your Tencent Cloud SecretId.
21    pub secret_id: String,
22    /// Your Tencent Cloud SecretKey.
23    pub secret_key: String,
24    /// Optional token.
25    pub token: Option<String>,
26}
27
28impl TencentCloudClient {
29    /// Creates a new TencentCloudClient.
30    ///
31    /// # Arguments
32    ///
33    /// * `secret_id` - Your Tencent Cloud SecretId.
34    /// * `secret_key` - Your Tencent Cloud SecretKey.
35    /// * `token` - An optional token.
36    pub fn new(secret_id: &str, secret_key: &str, token: Option<&str>) -> Self {
37        Self {
38            secret_id: secret_id.to_owned(),
39            secret_key: secret_key.to_owned(),
40            token: token.map(|s| s.to_owned()),
41        }
42    }
43
44    /// Asynchronous general request function.
45    ///
46    /// This method constructs the canonical request, computes the TC3-HMAC-SHA256 signature,
47    /// builds the Authorization header, and sends an HTTPS POST request.
48    ///
49    /// Instead of returning plain text, this version parses the response as JSON
50    /// and returns a `serde_json::Value`, which preserves Chinese characters.
51    ///
52    /// # Arguments
53    ///
54    /// * `service` - The service name (e.g., "cvm").
55    /// * `host` - The request host (e.g., "cvm.tencentcloudapi.com").
56    /// * `region` - Optional region string.
57    /// * `version` - API version (e.g., "2017-03-12").
58    /// * `action` - API action name (e.g., "DescribeInstances").
59    /// * `payload` - The request body as a JSON string.
60    ///
61    /// # Returns
62    ///
63    /// A `Result` containing the response parsed as `serde_json::Value` on success,
64    /// or a boxed error on failure.
65    pub async fn request(
66        &self,
67        service: &str,
68        host: &str,
69        region: Option<&str>,
70        version: &str,
71        action: &str,
72        payload: &str,
73    ) -> Result<Value, Box<dyn Error>> {
74        let algorithm = "TC3-HMAC-SHA256";
75        let ct = "application/json; charset=utf-8";
76
77        // Step 1: Construct the canonical request string.
78        let http_request_method = "POST";
79        let canonical_uri = "/";
80        let canonical_querystring = "";
81        let canonical_headers = format!(
82            "content-type:{}\nhost:{}\nx-tc-action:{}\n",
83            ct,
84            host,
85            action.to_lowercase()
86        );
87        let signed_headers = "content-type;host;x-tc-action";
88        let hashed_request_payload = {
89            let mut hasher = Sha256::new();
90            hasher.update(payload.as_bytes());
91            format!("{:x}", hasher.finalize())
92        };
93        let canonical_request = format!(
94            "{}\n{}\n{}\n{}\n{}\n{}",
95            http_request_method,
96            canonical_uri,
97            canonical_querystring,
98            canonical_headers,
99            signed_headers,
100            hashed_request_payload
101        );
102
103        // Step 2: Construct the string to sign.
104        let timestamp = Utc::now().timestamp();
105        let date = Utc.timestamp(timestamp, 0).format("%Y-%m-%d").to_string();
106        let credential_scope = format!("{}/{}/tc3_request", date, service);
107        let hashed_canonical_request = {
108            let mut hasher = Sha256::new();
109            hasher.update(canonical_request.as_bytes());
110            format!("{:x}", hasher.finalize())
111        };
112        let string_to_sign = format!(
113            "{}\n{}\n{}\n{}",
114            algorithm, timestamp, credential_scope, hashed_canonical_request
115        );
116
117        // Step 3: Compute the signature.
118        let secret_date = hmac_sha256(format!("TC3{}", self.secret_key).as_bytes(), &date);
119        let secret_service = hmac_sha256(&secret_date, service);
120        let secret_signing = hmac_sha256(&secret_service, "tc3_request");
121        let signature = {
122            let mut mac = HmacSha256::new_from_slice(&secret_signing)
123                .expect("HMAC can accept any key length");
124            mac.update(string_to_sign.as_bytes());
125            format!("{:x}", mac.finalize().into_bytes())
126        };
127
128        // Step 4: Construct the Authorization header.
129        let authorization = format!(
130            "{} Credential={}/{}, SignedHeaders={}, Signature={}",
131            algorithm, self.secret_id, credential_scope, signed_headers, signature
132        );
133
134        // Step 5: Build headers and send the request.
135        let mut headers = HeaderMap::new();
136        headers.insert("Authorization", HeaderValue::from_str(&authorization)?);
137        headers.insert("Content-Type", HeaderValue::from_static(ct));
138        headers.insert("Host", HeaderValue::from_str(host)?);
139        headers.insert("X-TC-Action", HeaderValue::from_str(action)?);
140        headers.insert("X-TC-Timestamp", HeaderValue::from_str(&timestamp.to_string())?);
141        headers.insert("X-TC-Version", HeaderValue::from_str(version)?);
142        if let Some(r) = region {
143            headers.insert("X-TC-Region", HeaderValue::from_str(r)?);
144        }
145        if let Some(t) = &self.token {
146            headers.insert("X-TC-Token", HeaderValue::from_str(t)?);
147        }
148
149        let url = format!("https://{}", host);
150        let client = reqwest::Client::builder()
151            .no_proxy()
152            .build()
153            .expect("build Client error");
154        let resp_json: Value = client
155            .post(&url)
156            .headers(headers)
157            .body(payload.to_owned())
158            .send()
159            .await?
160            .json()
161            .await?;
162        Ok(resp_json)
163    }
164}