Skip to main content

solo_lib/sdk/aliyun/
util.rs

1use core::str;
2use std::{borrow::Cow, collections::BTreeMap};
3
4use anyhow::Result;
5use chrono::DateTime;
6use http::Method;
7use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
8use rand::Rng;
9use reqwest::{
10    Client, Request,
11    header::{HeaderMap, HeaderValue},
12};
13use serde::{Deserialize, Serialize};
14use serde_json::{Value, from_str};
15
16use crate::{
17    error::SdkError,
18    util::{current_timestamp, hmac256, sha256_hex},
19};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Secret {
23    pub secret_id: String,
24    pub secret_key: String,
25}
26
27/// Machine Type
28#[derive(Debug, Clone)]
29pub enum MachineType {
30    /// SAS instance
31    Sas,
32
33    /// ECS instance
34    Ecs,
35}
36
37impl MachineType {
38    /// Returns (endpoint, version)
39    pub fn service_info(&self, region_id: &str) -> (String, &'static str) {
40        match self {
41            MachineType::Sas => {
42                (format!("swas.{region_id}.aliyuncs.com"), "2020-06-01")
43            }
44            MachineType::Ecs => {
45                (format!("ecs.{region_id}.aliyuncs.com"), "2014-05-26")
46            }
47        }
48    }
49}
50
51/// Common response
52///
53/// The response from the API is wrapped in this struct
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct CommonResponse<T> {
56    #[serde(rename = "RequestId")]
57    pub request_id: String,
58    #[serde(flatten)]
59    pub response: T,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct Empty {}
64
65#[derive(Debug, Clone)]
66pub(super) struct BasicRequest<'a> {
67    pub(super) machine_type: MachineType,
68    pub(super) region_id: &'a str,
69    pub(super) secret: &'a Secret,
70    pub(super) action: &'static str,
71    pub(super) params: &'a [(&'a str, &'a str)],
72    pub(super) body: &'a str,
73}
74
75pub(super) fn request_builder(
76    client: &Client,
77    basic_request: BasicRequest<'_>,
78) -> Result<Request> {
79    let (host, version) = basic_request
80        .machine_type
81        .service_info(basic_request.region_id);
82    let canonical_uri = "/";
83    let canonical_query_string =
84        build_sored_encoded_query_string(basic_request.params);
85    let hashed_request_payload = sha256_hex("");
86    let now_time = current_timestamp()?;
87    let datetime = DateTime::from_timestamp(now_time as i64, 0).unwrap();
88    let datetime_str = datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string();
89
90    let signature_nonce = generate_nonce();
91
92    // 构造请求头
93    let mut headers = HeaderMap::new();
94    headers.insert("Host", HeaderValue::from_str(&host)?);
95    headers.insert(
96        "Content-Type",
97        HeaderValue::from_str("application/json; charset=utf-8")?,
98    );
99    headers
100        .insert("x-acs-action", HeaderValue::from_str(basic_request.action)?);
101    headers.insert("x-acs-version", HeaderValue::from_str(version)?);
102    headers.insert("x-acs-date", HeaderValue::from_str(&datetime_str)?);
103    headers.insert(
104        "x-acs-signature-nonce",
105        HeaderValue::from_str(&signature_nonce)?,
106    );
107    headers.insert(
108        "x-acs-content-sha256",
109        HeaderValue::from_str(&hashed_request_payload)?,
110    );
111
112    let sign_header_arr = &[
113        "host",
114        "x-acs-action",
115        "x-acs-content-sha256",
116        "x-acs-date",
117        "x-acs-signature-nonce",
118        "x-acs-version",
119    ];
120
121    let http_request_method = "POST";
122
123    let sign_headers = sign_header_arr.join(";");
124    let canonical_request = format!(
125        "{}\n{}\n{}\n{}\n\n{}\n{}",
126        http_request_method,
127        canonical_uri,
128        canonical_query_string,
129        sign_header_arr
130            .iter()
131            .map(|&header| format!(
132                "{}:{}",
133                header,
134                headers[header].to_str().unwrap_or_default()
135            ))
136            .collect::<Vec<_>>()
137            .join("\n"),
138        sign_headers,
139        hashed_request_payload,
140    );
141
142    let result = sha256_hex(&canonical_request);
143    let string_to_sign = format!("ACS3-HMAC-SHA256\n{result}");
144    let signature =
145        hmac256(basic_request.secret.secret_key.as_bytes(), &string_to_sign)
146            .map_err(|e| anyhow::anyhow!("{}", e))?;
147    let data_sign = hex::encode(&signature);
148    let auth_data = format!(
149        "ACS3-HMAC-SHA256 Credential={},SignedHeaders={},Signature={}",
150        basic_request.secret.secret_id, sign_headers, data_sign
151    );
152
153    headers.insert("Authorization", HeaderValue::from_str(&auth_data)?);
154
155    let url = format!("https://{host}{canonical_uri}");
156    Ok(client
157        .request(Method::POST, url)
158        .headers(headers)
159        .query(&basic_request.params)
160        .body(basic_request.body.to_string())
161        .build()?)
162}
163
164fn to_error_response(response: &str) -> Option<SdkError> {
165    let response: Value = if let Ok(response) = from_str(response) {
166        response
167    } else {
168        return None;
169    };
170    let request_id = response.get("RequestId")?.as_str()?.to_string();
171    let code = response.get("Code")?.as_str()?.to_string();
172    let message = response.get("Message")?.as_str()?.to_string();
173    Some(SdkError {
174        request_id,
175        code,
176        message,
177    })
178}
179
180pub(super) fn parse_response<'a, T: Deserialize<'a>>(
181    result: &'a str,
182) -> Result<T> {
183    if let Some(error) = to_error_response(result) {
184        Err(error.into())
185    } else {
186        Ok(from_str::<T>(result)?)
187    }
188}
189
190fn build_sored_encoded_query_string(query_params: &[(&str, &str)]) -> String {
191    let sorted_query_params: BTreeMap<_, _> =
192        query_params.iter().copied().collect();
193
194    let encoded_params: Vec<String> = sorted_query_params
195        .into_iter()
196        .map(|(k, v)| {
197            let encoded_key = percent_code(k);
198            let encoded_value = percent_code(v);
199            format!("{encoded_key}={encoded_value}")
200        })
201        .collect();
202
203    encoded_params.join("&")
204}
205
206fn percent_code(encode_str: &str) -> Cow<'_, str> {
207    utf8_percent_encode(encode_str, NON_ALPHANUMERIC)
208        .collect::<String>()
209        .replace("%5F", "_")
210        .replace("%2D", "-")
211        .replace("%2E", ".")
212        .replace("%7E", "~")
213        .into()
214}
215
216fn generate_nonce() -> String {
217    const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
218    let mut rng = rand::rng();
219    (0..32)
220        .map(|_| CHARSET[rng.random_range(0..CHARSET.len())] as char)
221        .collect()
222}
223
224pub(super) fn flatten_json(
225    value: &Value,
226    prefix: &str,
227) -> Vec<(String, String)> {
228    let mut result = Vec::new();
229
230    fn process_value(
231        value: &Value,
232        current_prefix: &str,
233        result: &mut Vec<(String, String)>,
234    ) {
235        match value {
236            Value::Object(map) => {
237                for (key, val) in map {
238                    let new_prefix = if current_prefix.is_empty() {
239                        key.to_string()
240                    } else {
241                        format!("{current_prefix}.{key}")
242                    };
243                    process_value(val, &new_prefix, result);
244                }
245            }
246            Value::Array(arr) => {
247                for (index, elem) in arr.iter().enumerate() {
248                    let new_prefix =
249                        format!("{}.{}", current_prefix, index + 1);
250                    process_value(elem, &new_prefix, result);
251                }
252            }
253            Value::Null => {
254                result.push((current_prefix.to_string(), "null".to_string()))
255            }
256            Value::Bool(b) => {
257                result.push((current_prefix.to_string(), b.to_string()))
258            }
259            Value::Number(n) => {
260                result.push((current_prefix.to_string(), n.to_string()))
261            }
262            Value::String(s) => {
263                result.push((current_prefix.to_string(), s.clone()))
264            }
265        }
266    }
267
268    process_value(value, prefix, &mut result);
269    result
270}