rust_aliyun/
client.rs

1use crate::error::Error::CommonError;
2use crate::{Result, __setter, __string_enum};
3use chrono::Utc;
4use hmac::{Hmac, Mac};
5use reqwest::header::{HeaderMap, HeaderName};
6use reqwest::{Method, Response as ReqResponse, StatusCode, Url};
7use serde::{Deserialize, Serialize, Serializer};
8use sha2::{Digest, Sha256};
9use std::collections::HashMap;
10use std::marker::PhantomData;
11use std::str::FromStr;
12use log::log;
13use serde::de::DeserializeOwned;
14use crate::date::acs_date;
15use crate::util::request::{canonical, hash, sign_string};
16
17#[derive(Debug)]
18pub struct Request<T: Serialize> {
19    pub action: String,
20    pub ssl: bool,
21    pub uri: String,
22    pub headers: Option<HashMap<String, String>>,
23    pub queries: Option<HashMap<String, String>>,
24    pub data: Option<T>,
25    pub method: Method,
26    pub content_type: ContentType,
27    pub version: String,
28}
29
30impl<T: Serialize> Request<T> {
31    pub fn url(&self, endpoint: &str) -> Result<Url> {
32        Ok(Url::parse(&format!(
33            "{}{}{}",
34            if self.ssl { "https://" } else { "http://" },
35            endpoint,
36            self.uri
37        ))
38        .map_err(|err| CommonError(err.to_string()))?)
39    }
40
41    pub fn build_headers(&self, endpoint: &str, access_key_id: &str, access_key_secret: &str) -> Result<HeaderMap> {
42        let payload_hex = self.hex_payload()?;
43        let mut headers = HeaderMap::new();
44
45        if let Some(req_headers) = &self.headers {
46            for (k, v) in req_headers {
47                headers.insert(HeaderName::from_str(k)?, v.parse()?);
48            }
49        }
50        let now = Utc::now();
51        let nonce = format!("{}", now.timestamp_millis());
52
53        headers.insert("host", endpoint.parse()?);
54        headers.insert("x-acs-action", self.action.parse()?);
55        headers.insert("x-acs-content-sha256", payload_hex.parse()?);
56        headers.insert("x-acs-date", acs_date().parse()?);
57        headers.insert("x-acs-signature-nonce", nonce.parse()?);
58        headers.insert("x-acs-version", self.version.parse()?);
59        headers.insert("content-type", self.content_type.to_string().parse()?);
60
61        let (header_string, header_name_string) = self.canonical_headers(&headers)?;
62        let canonical_request = format!(
63            "{}\n{}\n{}\n{}\n{}\n{}",
64            self.method,
65            self.canonical_uri(),
66            self.canonical_queries()?,
67            header_string,
68            header_name_string,
69            payload_hex
70        );
71        log::debug!("canonical request: {}", canonical_request);
72        let pre_sign = format!("{}\n{}", SIGN_ALGORITHM, hash(&canonical_request));
73        log::debug!("pre-sign: {}", pre_sign);
74        let sign = sign_string(access_key_secret.as_bytes(), &pre_sign)?;
75        let authorization = format!(
76            "{} Credential={},SignedHeaders={},Signature={}",
77            SIGN_ALGORITHM, access_key_id, header_name_string, sign
78        );
79        headers.insert("Authorization", authorization.parse()?);
80        Ok(headers)
81    }
82
83    fn canonical_uri(&self) -> String {
84        self.uri.split("/").map(|s| canonical(s)).collect::<Vec<String>>().join("/")
85    }
86
87    fn canonical_queries(&self) -> Result<String> {
88        if let Some(queries) = &self.queries {
89            Ok(canonical(serde_urlencoded::to_string(queries)?.as_str()))
90        } else {
91            Ok("".to_string())
92        }
93    }
94
95    fn hex_payload(&self) -> Result<String> {
96        let data = if let Some(data) = &self.data {
97            match self.content_type {
98                ContentType::Form => serde_urlencoded::to_string(data)?,
99                ContentType::Json => serde_json::to_string(data)?,
100            }
101        } else {
102            "".to_string()
103        };
104        Ok(hash(&data))
105    }
106
107    fn canonical_headers(&self, headers: &HeaderMap) -> Result<(String, String)> {
108        let mut canonical_headers = headers
109            .iter()
110            .map(|item| (item.0.to_string().to_lowercase(), item.1))
111            .filter(|item| {
112                item.0 == "host" || item.0 == "content-type" || item.0.starts_with("x-acs")
113            })
114            .collect::<Vec<(_, _)>>();
115
116        canonical_headers.sort_by(|a, b| a.0.cmp(&b.0));
117
118        let mut header_string = canonical_headers
119            .iter()
120            .map(|item| format!("{}:{}\n", item.0, item.1.to_str().unwrap().trim()))
121            .fold(String::new(), |acc, item| acc + &item)
122            ;
123
124        let canonical_header_name_string = canonical_headers
125            .iter()
126            .map(|item| item.0.clone())
127            .collect::<Vec<_>>()
128            .join(";");
129        Ok((header_string, canonical_header_name_string))
130    }
131}
132
133#[derive(Debug, Serialize)]
134pub enum ContentType {
135    Form,
136    Json,
137}
138
139__string_enum!{
140    ContentType {
141        Form = "application/x-www-form-urlencoded",
142        Json = "application/json",
143    }
144}
145
146#[derive(Serialize, Deserialize, Debug)]
147pub struct Response<T> {
148    #[serde(rename = "RequestId")]
149    pub request_id: String,
150    #[serde(flatten)]
151    pub data: Option<T>,
152    #[serde(rename = "Message", skip_serializing_if = "Option::is_none")]
153    pub message: Option<String>,
154    #[serde(rename = "Recommend", skip_serializing_if = "Option::is_none")]
155    pub recommend: Option<String>,
156    #[serde(rename = "Code", skip_serializing_if = "Option::is_none")]
157    pub code: Option<String>,
158    #[serde(rename = "HostId", skip_serializing_if = "Option::is_none")]
159    pub host: Option<String>,
160}
161
162pub trait Caculator<R> {
163    fn calculate(&self, client: &Client) -> Result<R>;
164}
165
166pub struct Client {
167    client: reqwest::Client,
168    pub access_key_id: String,
169    pub access_key_secret: String,
170    pub endpoint: String,
171    pub region: Option<String>,
172}
173const SIGN_ALGORITHM: &str = "ACS3-HMAC-SHA256";
174impl Client {
175    __setter!(client: reqwest::Client);
176    __setter!(access_key_id: String);
177    __setter!(access_key_secret: String);
178    __setter!(endpoint: String);
179    __setter!(region: Option<String>);
180
181    pub fn new(access_key_id: &str, access_key_secret: &str, endpoint: &str) -> Client {
182        Self {
183            access_key_id: access_key_id.to_string(),
184            access_key_secret: access_key_secret.to_string(),
185            endpoint: endpoint.to_string(),
186            client: reqwest::Client::new(),
187            region: None
188        }
189    }
190
191    pub async fn do_request<T: Serialize, R: DeserializeOwned>(&self, request: Request<T>) -> Result<Response<R>> {
192        let endpoint = if let Some(region) = &self.region {
193            format!("{}.{}", region, self.endpoint)
194        } else {
195            self.endpoint.clone()
196        };
197        let mut builder = self
198            .client
199            .request(request.method.clone(), request.url(&endpoint)?)
200            .headers(request.build_headers(&self.endpoint, &self.access_key_id, &self.access_key_secret)?);
201
202        if let Some(data) = &request.data {
203            match request.content_type {
204                ContentType::Form => builder = builder.form(&data),
205                ContentType::Json => builder = builder.json(&data),
206            };
207        }
208
209        if let Some(queries) = request.queries {
210            builder = builder.query(&queries);
211        }
212        let response = builder.send().await?;
213        Ok(response.json::<Response<R>>().await?)
214    }
215
216    pub fn do_calculate<T: Caculator<R>, R>(&self, calc: T) -> Result<R> {
217        calc.calculate(self)
218    }
219}
220
221mod test {
222    use crate::client::hash;
223
224    #[test]
225    fn test_hash() {
226        let raw = "POST\n/\n\ncontent-type:application/json\nhost:sts.cn-shanghai.aliyuncs.com\nx-acs-action:AssumeRole\nx-acs-content-sha256:992472389d0b7c44944968826f0bb6a5225f76220f7457498c7edd2a3ff8d7aa\nx-acs-date:2025-03-20T14:38:58Z\nx-acs-signature-nonce:1742481538630\nx-acs-version:2015-04-01\n\ncontent-type;host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version\n992472389d0b7c44944968826f0bb6a5225f76220f7457498c7edd2a3ff8d7aa";
227        assert_eq!("942734fd080698a94360a3634040119a821a0ed670663f998b610ae2090ae4c8", hash(raw));
228    }
229}