aliyun_openapi_core_rust_sdk/client/
rpc.rs

1use std::{collections::HashMap, time::Duration};
2
3use hmac::{Hmac, Mac};
4use reqwest::{header::HeaderMap, ClientBuilder, Response};
5use serde::{de::DeserializeOwned, Deserialize, Serialize};
6use sha1::Sha1;
7use time::{format_description::well_known::Iso8601, OffsetDateTime};
8use url::form_urlencoded::byte_serialize;
9use uuid::Uuid;
10
11use crate::client::error::{Error, Result};
12
13#[derive(Debug, Deserialize, Serialize)]
14#[serde(rename_all = "PascalCase")]
15pub struct RPCServiceError {
16    /// Error code
17    pub code: String,
18    /// Error message
19    pub message: String,
20    /// Request id
21    #[serde(default)]
22    pub request_id: String,
23    /// Recommend
24    #[serde(default)]
25    pub recommend: String,
26}
27
28/// Default const header.
29const AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
30const DEFAULT_HEADER: &[(&str, &str)] = &[("user-agent", AGENT), ("x-sdk-client", AGENT)];
31const DEFAULT_PARAM: &[(&str, &str)] = &[
32    ("Format", "JSON"),
33    ("SignatureMethod", "HMAC-SHA1"),
34    ("SignatureVersion", "1.0"),
35];
36
37type HamcSha1 = Hmac<Sha1>;
38
39/// Config for request.
40#[derive(Clone, Debug, Default)]
41struct Request {
42    action: String,
43    method: String,
44    query: Vec<(String, String)>,
45    headers: HeaderMap,
46    version: String,
47    timeout: Option<Duration>,
48}
49
50#[derive(Clone, Debug)]
51pub struct RPClient {
52    /// The access key id of aliyun developer account.
53    access_key_id: String,
54    /// The access key secret of aliyun developer account.
55    access_key_secret: String,
56    /// The api endpoint of aliyun api service (need start with http:// or https://).
57    endpoint: String,
58    /// The config of http request.
59    request: Request,
60}
61
62impl RPClient {
63    /// Create a api client.
64    pub fn new(
65        access_key_id: impl Into<String>,
66        access_key_secret: impl Into<String>,
67        endpoint: impl Into<String>,
68    ) -> Self {
69        RPClient {
70            access_key_id: access_key_id.into(),
71            access_key_secret: access_key_secret.into(),
72            endpoint: endpoint.into(),
73            request: Default::default(),
74        }
75    }
76
77    /// Create a request with the `method` and `action`.
78    ///
79    /// Returns a `Self` for send request.
80    pub fn request(mut self, method: impl Into<String>, action: impl Into<String>) -> Self {
81        self.request.method = method.into();
82        self.request.action = action.into();
83
84        self
85    }
86
87    /// Create a `GET` request with the `action`.
88    ///
89    /// Returns a `Self` for send request.
90    pub fn get(self, action: impl Into<String>) -> Self {
91        self.request("GET".to_string(), action.into())
92    }
93
94    /// Create a `POST` request with the `action`.
95    ///
96    /// Returns a `Self` for send request.
97    pub fn post(self, action: impl Into<String>) -> Self {
98        self.request("POST".to_string(), action.into())
99    }
100
101    /// Set queries for request.
102    ///
103    /// Returns a `Self` for send request.
104    pub fn query<I, T>(mut self, queries: I) -> Self
105    where
106        I: IntoIterator<Item = (T, T)>,
107        T: Into<String>,
108    {
109        self.request.query = queries
110            .into_iter()
111            .map(|v| (v.0.into(), v.1.into()))
112            .collect();
113
114        self
115    }
116
117    /// Set version for request.
118    ///
119    /// Returns a `Self` for send request.
120    pub fn version(mut self, version: impl Into<String>) -> Self {
121        self.request.version = version.into();
122
123        self
124    }
125
126    /// Set header for request.
127    ///
128    /// Returns a `Self` for send request.
129    pub fn header(mut self, headers: impl Into<HashMap<String, String>>) -> Result<Self> {
130        self.request.headers = (&headers.into())
131            .try_into()
132            .map_err(|e| Error::InvalidRequest(format!("Cannot parse header: {e}")))?;
133        Ok(self)
134    }
135
136    /// Set a timeout for connect, read and write operations of a `Client`.
137    ///
138    /// Default is no timeout.
139    pub fn timeout(mut self, timeout: Duration) -> Self {
140        self.request.timeout = Some(timeout);
141
142        self
143    }
144
145    /// Send a request to service.
146    /// Try to deserialize the response body as JSON.
147    pub async fn json<T: DeserializeOwned>(self) -> Result<T> {
148        Ok(self.send().await?.json::<T>().await?)
149    }
150
151    /// Send a request to service.
152    /// Try to deserialize the response body as TEXT.
153    pub async fn text(self) -> Result<String> {
154        Ok(self.send().await?.text().await?)
155    }
156
157    /// Send a request to service.
158    /// Return client Response.
159    pub async fn send(mut self) -> Result<Response> {
160        // add const header
161        for (k, v) in DEFAULT_HEADER.iter() {
162            self.request.headers.insert(*k, v.parse()?);
163        }
164
165        // build params.
166        let nonce = Uuid::new_v4().to_string();
167        let ts = OffsetDateTime::now_utc()
168            .format(&Iso8601::DEFAULT)
169            .map_err(|e| Error::InvalidRequest(format!("Invalid ISO 8601 Date: {e}")))?;
170
171        let mut params = Vec::from(DEFAULT_PARAM);
172        params.push(("Action", &self.request.action));
173        params.push(("AccessKeyId", &self.access_key_id));
174        params.push(("SignatureNonce", &nonce));
175        params.push(("Timestamp", &ts));
176        params.push(("Version", &self.request.version));
177        params.extend(
178            self.request
179                .query
180                .iter()
181                .map(|(k, v)| (k.as_ref(), v.as_ref())),
182        );
183        params.sort_by_key(|item| item.0);
184
185        // encode params.
186        let params: Vec<String> = params
187            .into_iter()
188            .map(|(k, v)| format!("{}={}", url_encode(k), url_encode(v)))
189            .collect();
190        let sorted_query_string = params.join("&");
191        let string_to_sign = format!(
192            "{}&{}&{}",
193            self.request.method,
194            url_encode("/"),
195            url_encode(&sorted_query_string)
196        );
197
198        // sign params, get finnal request url.
199        let sign = sign(&format!("{}&", self.access_key_secret), &string_to_sign)?;
200        let signature = url_encode(&sign);
201        let final_url = format!(
202            "{}?Signature={}&{}",
203            self.endpoint, signature, sorted_query_string
204        );
205
206        // build http client.
207        let mut http_client_builder = ClientBuilder::new();
208        if let Some(timeout) = self.request.timeout {
209            http_client_builder = http_client_builder.timeout(timeout);
210        }
211        let http_client = http_client_builder.build()?.request(
212            self.request
213                .method
214                .parse()
215                .map_err(|e| Error::InvalidRequest(format!("Invalid HTTP method: {}", e)))?,
216            &final_url,
217        );
218
219        // send request.
220        let response = http_client.headers(self.request.headers).send().await?;
221
222        // check HTTP StatusCode.
223        if !response.status().is_success() {
224            let result = response.json::<RPCServiceError>().await?;
225            return Err(Error::InvalidResponse {
226                request_id: result.request_id,
227                error_code: result.code,
228                error_message: result.message,
229            });
230        }
231
232        // return response.
233        Ok(response)
234    }
235}
236
237fn sign(key: &str, body: &str) -> Result<String> {
238    let mut mac = HamcSha1::new_from_slice(key.as_bytes())
239        .map_err(|e| Error::InvalidRequest(format!("Invalid HMAC-SHA1 secret key: {}", e)))?;
240    mac.update(body.as_bytes());
241    let result = mac.finalize();
242    let code = result.into_bytes();
243
244    Ok(base64::encode(code))
245}
246
247/// URL encode following [RFC3986](https://www.rfc-editor.org/rfc/rfc3986)
248fn url_encode(s: &str) -> String {
249    let s: String = byte_serialize(s.as_bytes()).collect();
250    s.replace('+', "%20")
251        .replace('*', "%2A")
252        .replace("%7E", "~")
253}
254
255#[cfg(test)]
256mod tests {
257    use std::env;
258
259    use super::*;
260
261    #[test]
262    fn url_encode_test() -> Result<()> {
263        assert_eq!(
264            url_encode("begin_+_*_~_-_._\"_ end"),
265            "begin_%2B_%2A_~_-_._%22_%20end"
266        );
267
268        Ok(())
269    }
270
271    #[tokio::test]
272    async fn rpc_client_invalid_access_key_id_test() -> Result<()> {
273        // create rpc style api client.
274        let aliyun_openapi_client = RPClient::new(
275            env::var("ACCESS_KEY_ID").unwrap(),
276            env::var("ACCESS_KEY_SECRET").unwrap(),
277            "https://ecs-cn-hangzhou.aliyuncs.com",
278        );
279
280        // call `DescribeRegions` with empty queries.
281        let response = aliyun_openapi_client
282            .version("2014-05-26")
283            .get("DescribeRegions")
284            .text()
285            .await?;
286
287        assert!(response.contains("Regions"));
288
289        Ok(())
290    }
291
292    #[tokio::test]
293    async fn rpc_client_get_with_query_test() -> Result<()> {
294        // create rpc style api client.
295        let aliyun_openapi_client = RPClient::new(
296            env::var("ACCESS_KEY_ID").unwrap(),
297            env::var("ACCESS_KEY_SECRET").unwrap(),
298            "https://ecs-cn-hangzhou.aliyuncs.com",
299        );
300
301        // call `DescribeInstances` with queries.
302        let response = aliyun_openapi_client
303            .version("2014-05-26")
304            .get("DescribeInstances")
305            .query(vec![("RegionId", "cn-hangzhou")])
306            .text()
307            .await?;
308
309        assert!(response.contains("Instances"));
310
311        Ok(())
312    }
313}