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}