aliyun_openapi_core_rust_sdk/
rpc.rs

1use anyhow::{anyhow, Result};
2use hmac::{Hmac, Mac};
3use reqwest::blocking::ClientBuilder;
4use sha1::Sha1;
5use std::borrow::Borrow;
6use std::time::Duration;
7use time::format_description::well_known::Iso8601;
8use time::OffsetDateTime;
9use url::form_urlencoded::byte_serialize;
10use uuid::Uuid;
11
12/// Default const param.
13const DEFAULT_PARAM: &[(&str, &str)] = &[
14    ("Format", "JSON"),
15    ("SignatureMethod", "HMAC-SHA1"),
16    ("SignatureVersion", "1.0"),
17];
18
19type HamcSha1 = Hmac<Sha1>;
20
21/// Config for request.
22#[derive(Debug)]
23struct Request {
24    action: String,
25    method: String,
26    query: Vec<(String, String)>,
27}
28
29/// The rpc style api client.
30#[deprecated(
31    since = "1.0.0",
32    note = "Please use the `aliyun_openapi_core_rust_sdk::client::rpc::RPClient` instead"
33)]
34#[derive(Clone, Debug)]
35pub struct Client {
36    /// The access key id of aliyun developer account.
37    access_key_id: String,
38    /// The access key secret of aliyun developer account.
39    access_key_secret: String,
40    /// The api endpoint of aliyun api service (need start with http:// or https://).
41    endpoint: String,
42    /// The api version of aliyun api service.
43    version: String,
44}
45
46impl Client {
47    #![allow(deprecated)]
48
49    /// Create a rpc style api client.
50    pub fn new(
51        access_key_id: String,
52        access_key_secret: String,
53        endpoint: String,
54        version: String,
55    ) -> Self {
56        Client {
57            access_key_id,
58            access_key_secret,
59            endpoint,
60            version,
61        }
62    }
63
64    /// Create a `GET` request with the `action`.
65    ///
66    /// Returns a `RequestBuilder` for send request.
67    pub fn get(&self, action: &str) -> RequestBuilder {
68        self.execute("GET", action)
69    }
70
71    /// Create a `POST` request with the `action`.
72    ///
73    /// Returns a `RequestBuilder` for send request.
74    pub fn post(&self, action: &str) -> RequestBuilder {
75        self.execute("POST", action)
76    }
77
78    /// Create a request with the `method` and `action`.
79    fn execute(&self, method: &str, action: &str) -> RequestBuilder {
80        RequestBuilder::new(
81            &self.access_key_id,
82            &self.access_key_secret,
83            &self.endpoint,
84            &self.version,
85            String::from(method),
86            String::from(action),
87        )
88    }
89
90    /// Send a request to api service.
91    ///
92    /// if queries is empty, can pass `&[]`
93    #[deprecated(since = "0.3.0", note = "Please use the `get` function instead")]
94    pub fn request(&self, action: &str, queries: &[(&str, &str)]) -> Result<String> {
95        // build params.
96        let nonce = Uuid::new_v4().to_string();
97        let ts = OffsetDateTime::now_utc()
98            .format(&Iso8601::DEFAULT)
99            .map_err(|e| anyhow!(format!("Invalid ISO 8601 Date: {e}")))?;
100
101        let mut params = Vec::from(DEFAULT_PARAM);
102        params.push(("Action", action));
103        params.push(("AccessKeyId", &self.access_key_id));
104        params.push(("SignatureNonce", &nonce));
105        params.push(("Timestamp", &ts));
106        params.push(("Version", &self.version));
107        params.extend_from_slice(queries);
108        params.sort_by_key(|item| item.0);
109
110        // encode params.
111        let params: Vec<String> = params
112            .into_iter()
113            .map(|(k, v)| format!("{}={}", url_encode(k), url_encode(v)))
114            .collect();
115        let sorted_query_string = params.join("&");
116        let string_to_sign = format!(
117            "GET&{}&{}",
118            url_encode("/"),
119            url_encode(&sorted_query_string)
120        );
121
122        // sign params, get finnal request url.
123        let sign = sign(&format!("{}&", self.access_key_secret), &string_to_sign)?;
124        let signature = url_encode(&sign);
125        let final_url = format!(
126            "{}?Signature={}&{}",
127            self.endpoint, signature, sorted_query_string
128        );
129
130        // send request.
131        let response = reqwest::blocking::get(final_url)?.text()?;
132
133        // return response.
134        Ok(response)
135    }
136}
137
138/// The RequestBuilder.
139pub struct RequestBuilder<'a> {
140    /// The access key id of aliyun developer account.
141    access_key_id: &'a str,
142    /// The access key secret of aliyun developer account.
143    access_key_secret: &'a str,
144    /// The api endpoint of aliyun api service (need start with http:// or https://).
145    endpoint: &'a str,
146    /// The api version of aliyun api service.
147    version: &'a str,
148    /// The config of http request.
149    request: Request,
150    /// The http client builder used to send request.
151    http_client_builder: ClientBuilder,
152}
153
154impl<'a> RequestBuilder<'a> {
155    /// Create a request object.
156    pub fn new(
157        access_key_id: &'a str,
158        access_key_secret: &'a str,
159        endpoint: &'a str,
160        version: &'a str,
161        method: String,
162        action: String,
163    ) -> Self {
164        RequestBuilder {
165            access_key_id,
166            access_key_secret,
167            endpoint,
168            version,
169            request: Request {
170                action,
171                method,
172                query: Vec::new(),
173            },
174            http_client_builder: ClientBuilder::new(),
175        }
176    }
177
178    /// Set queries for request.
179    pub fn query<I>(mut self, iter: I) -> Self
180    where
181        I: IntoIterator,
182        I::Item: Borrow<(&'a str, &'a str)>,
183    {
184        for i in iter.into_iter() {
185            let b = i.borrow();
186            self.request.query.push((b.0.to_string(), b.1.to_string()));
187        }
188        self
189    }
190
191    /// Send a request to api service.
192    pub fn send(self) -> Result<String> {
193        // build params.
194        let nonce = Uuid::new_v4().to_string();
195        let ts = OffsetDateTime::now_utc()
196            .format(&Iso8601::DEFAULT)
197            .map_err(|e| anyhow!(format!("Invalid ISO 8601 Date: {e}")))?;
198
199        let mut params = Vec::from(DEFAULT_PARAM);
200        params.push(("Action", &self.request.action));
201        params.push(("AccessKeyId", self.access_key_id));
202        params.push(("SignatureNonce", &nonce));
203        params.push(("Timestamp", &ts));
204        params.push(("Version", self.version));
205        params.extend(
206            self.request
207                .query
208                .iter()
209                .map(|(k, v)| (k.as_ref(), v.as_ref())),
210        );
211        params.sort_by_key(|item| item.0);
212
213        // encode params.
214        let params: Vec<String> = params
215            .into_iter()
216            .map(|(k, v)| format!("{}={}", url_encode(k), url_encode(v)))
217            .collect();
218        let sorted_query_string = params.join("&");
219        let string_to_sign = format!(
220            "{}&{}&{}",
221            self.request.method,
222            url_encode("/"),
223            url_encode(&sorted_query_string)
224        );
225
226        // sign params, get finnal request url.
227        let sign = sign(&format!("{}&", self.access_key_secret), &string_to_sign)?;
228        let signature = url_encode(&sign);
229        let final_url = format!(
230            "{}?Signature={}&{}",
231            self.endpoint, signature, sorted_query_string
232        );
233
234        // build http client.
235        let http_client = self
236            .http_client_builder
237            .build()?
238            .request(self.request.method.parse()?, final_url);
239
240        // send request.
241        let response = http_client.send()?.text()?;
242
243        // return response.
244        Ok(response)
245    }
246
247    /// Set a timeout for connect, read and write operations of a `Client`.
248    ///
249    /// Default is 30 seconds.
250    ///
251    /// Pass `None` to disable timeout.
252    pub fn timeout<T>(mut self, timeout: T) -> Self
253    where
254        T: Into<Option<Duration>>,
255    {
256        self.http_client_builder = self.http_client_builder.timeout(timeout);
257        self
258    }
259}
260
261fn sign(key: &str, body: &str) -> Result<String> {
262    let mut mac = HamcSha1::new_from_slice(key.as_bytes())
263        .map_err(|e| anyhow!(format!("Invalid HMAC-SHA1 secret key: {}", e)))?;
264    mac.update(body.as_bytes());
265    let result = mac.finalize();
266    let code = result.into_bytes();
267
268    Ok(base64::encode(code))
269}
270
271fn url_encode(s: &str) -> String {
272    let s: String = byte_serialize(s.as_bytes()).collect();
273    s.replace('+', "%20")
274        .replace('*', "%2A")
275        .replace("%7E", "~")
276}
277
278#[cfg(test)]
279mod tests {
280    #![allow(deprecated)]
281
282    use std::env;
283
284    use super::*;
285
286    // 0.2.0 version, rpc style client test.
287    #[test]
288    fn rpc_client_no_query_compatibility_020() -> Result<()> {
289        // create rpc style api client.
290        let aliyun_openapi_client = Client::new(
291            env::var("ACCESS_KEY_ID")?,
292            env::var("ACCESS_KEY_SECRET")?,
293            String::from("https://ecs.aliyuncs.com/"),
294            String::from("2014-05-26"),
295        );
296
297        // call `DescribeRegions` with empty queries.
298        let response = aliyun_openapi_client.request("DescribeRegions", &[])?;
299
300        assert!(response.contains("Regions"));
301
302        Ok(())
303    }
304
305    // 0.2.0 version, rpc style client test with query.
306    #[test]
307    fn rpc_client_with_query_compatibility_020() -> Result<()> {
308        // create rpc style api client.
309        let aliyun_openapi_client = Client::new(
310            env::var("ACCESS_KEY_ID")?,
311            env::var("ACCESS_KEY_SECRET")?,
312            String::from("https://ecs.aliyuncs.com/"),
313            String::from("2014-05-26"),
314        );
315
316        // call `DescribeInstances` with queries.
317        let response =
318            aliyun_openapi_client.request("DescribeInstances", &[("RegionId", "cn-hangzhou")])?;
319
320        assert!(response.contains("Instances"));
321
322        Ok(())
323    }
324
325    // rpc style client `GET` test.
326    #[test]
327    fn rpc_client_get_no_query() -> Result<()> {
328        // create rpc style api client.
329        let aliyun_openapi_client = Client::new(
330            env::var("ACCESS_KEY_ID")?,
331            env::var("ACCESS_KEY_SECRET")?,
332            String::from("https://ecs.aliyuncs.com/"),
333            String::from("2014-05-26"),
334        );
335
336        // call `DescribeRegions` with empty queries.
337        let response = aliyun_openapi_client.get("DescribeRegions").send()?;
338
339        assert!(response.contains("Regions"));
340
341        Ok(())
342    }
343
344    // rpc style client `GET` test with query.
345    #[test]
346    fn rpc_client_get_with_query() -> Result<()> {
347        // create rpc style api client.
348        let aliyun_openapi_client = Client::new(
349            env::var("ACCESS_KEY_ID")?,
350            env::var("ACCESS_KEY_SECRET")?,
351            String::from("https://ecs.aliyuncs.com/"),
352            String::from("2014-05-26"),
353        );
354
355        // call `DescribeInstances` with queries.
356        let response = aliyun_openapi_client
357            .get("DescribeInstances")
358            .query(&[("RegionId", "cn-hangzhou")])
359            .send()?;
360
361        assert!(response.contains("Instances"));
362
363        Ok(())
364    }
365
366    // rpc style client `GET` test with timeout.
367    #[test]
368    fn rpc_client_get_with_timeout() -> Result<()> {
369        // create rpc style api client.
370        let aliyun_openapi_client = Client::new(
371            env::var("ACCESS_KEY_ID")?,
372            env::var("ACCESS_KEY_SECRET")?,
373            String::from("https://ecs.aliyuncs.com/"),
374            String::from("2014-05-26"),
375        );
376
377        // call `DescribeRegions` with empty queries.
378        let response = aliyun_openapi_client
379            .get("DescribeRegions")
380            .timeout(Duration::from_millis(1))
381            .send();
382
383        assert!(response.is_err());
384
385        Ok(())
386    }
387
388    // rpc style client `POST` test.
389    #[test]
390    fn rpc_client_post_no_query() -> Result<()> {
391        // create rpc style api client.
392        let aliyun_openapi_client = Client::new(
393            env::var("ACCESS_KEY_ID")?,
394            env::var("ACCESS_KEY_SECRET")?,
395            String::from("https://ecs.aliyuncs.com/"),
396            String::from("2014-05-26"),
397        );
398
399        // call `DescribeRegions` with empty queries.
400        let response = aliyun_openapi_client.post("DescribeRegions").send()?;
401
402        assert!(response.contains("Regions"));
403
404        Ok(())
405    }
406
407    // rpc style client `POST` test with query.
408    #[test]
409    fn rpc_client_post_with_query() -> Result<()> {
410        // create rpc style api client.
411        let aliyun_openapi_client = Client::new(
412            env::var("ACCESS_KEY_ID")?,
413            env::var("ACCESS_KEY_SECRET")?,
414            String::from("https://ecs.aliyuncs.com/"),
415            String::from("2014-05-26"),
416        );
417
418        // call `DescribeInstances` with queries.
419        let response = aliyun_openapi_client
420            .post("DescribeInstances")
421            .query(&[("RegionId", "cn-hangzhou")])
422            .send()?;
423
424        assert!(response.contains("Instances"));
425
426        Ok(())
427    }
428}