aliyun_openapi_core_rust_sdk/
roa.rs

1use anyhow::{anyhow, Result};
2use hmac::{Hmac, Mac};
3use md5::{Digest, Md5};
4use reqwest::blocking::ClientBuilder;
5use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
6use sha1::Sha1;
7use std::env;
8use std::time::Duration;
9use std::{borrow::Borrow, str::FromStr};
10use time::macros::format_description;
11use time::OffsetDateTime;
12use url::Url;
13use uuid::Uuid;
14
15/// Default const header.
16const DEFAULT_HEADER: &[(&str, &str)] = &[
17    ("accept", "application/json"),
18    ("x-acs-signature-method", "HMAC-SHA1"),
19    ("x-acs-signature-version", "1.0"),
20];
21
22type HamcSha1 = Hmac<Sha1>;
23
24/// Config for request.
25#[derive(Debug)]
26struct Request {
27    method: String,
28    uri: String,
29    body: Option<String>,
30    query: Vec<(String, String)>,
31    headers: HeaderMap,
32}
33
34/// The roa style api client.
35#[deprecated(
36    since = "1.0.0",
37    note = "Please use the `aliyun_openapi_core_rust_sdk::client::roa::ROAClient` instead"
38)]
39#[derive(Clone, Debug)]
40pub struct Client {
41    /// The access key id of aliyun developer account.
42    access_key_id: String,
43    /// The access key secret of aliyun developer account.
44    access_key_secret: String,
45    /// The api endpoint of aliyun api service (need start with http:// or https://).
46    endpoint: String,
47    /// The api version of aliyun api service.
48    version: String,
49}
50
51impl Client {
52    #![allow(deprecated)]
53
54    /// Create a roa style api client.
55    pub fn new(
56        access_key_id: String,
57        access_key_secret: String,
58        endpoint: String,
59        version: String,
60    ) -> Self {
61        Client {
62            access_key_id,
63            access_key_secret,
64            endpoint,
65            version,
66        }
67    }
68
69    /// Create a request with the `method` and `uri`.
70    ///
71    /// Returns a `RequestBuilder` for send request.
72    pub fn execute(&self, method: &str, uri: &str) -> RequestBuilder {
73        RequestBuilder::new(
74            &self.access_key_id,
75            &self.access_key_secret,
76            &self.endpoint,
77            &self.version,
78            String::from(method),
79            String::from(uri),
80        )
81    }
82
83    /// Create a `GET` request with the `uri`.
84    ///
85    /// Returns a `RequestBuilder` for send request.
86    pub fn get(&self, uri: &str) -> RequestBuilder {
87        self.execute("GET", uri)
88    }
89
90    /// Create a `POST` request with the `uri`.
91    ///
92    /// Returns a `RequestBuilder` for send request.
93    pub fn post(&self, uri: &str) -> RequestBuilder {
94        self.execute("POST", uri)
95    }
96
97    /// Create a `PUT` request with the `uri`.
98    ///
99    /// Returns a `RequestBuilder` for send request.
100    pub fn put(&self, uri: &str) -> RequestBuilder {
101        self.execute("PUT", uri)
102    }
103}
104
105/// The request builder struct.
106#[derive(Debug)]
107pub struct RequestBuilder<'a> {
108    /// The access key id of aliyun developer account.
109    access_key_id: &'a str,
110    /// The access key secret of aliyun developer account.
111    access_key_secret: &'a str,
112    /// The api endpoint of aliyun api service (need start with http:// or https://).
113    endpoint: &'a str,
114    /// The http client builder used to send request.
115    http_client_builder: ClientBuilder,
116    /// The config of http request.
117    request: Request,
118}
119
120impl<'a> RequestBuilder<'a> {
121    /// Create a request object.
122    pub fn new(
123        access_key_id: &'a str,
124        access_key_secret: &'a str,
125        endpoint: &'a str,
126        version: &'a str,
127        method: String,
128        uri: String,
129    ) -> Self {
130        // init http headers.
131        let mut headers = HeaderMap::new();
132        for (k, v) in DEFAULT_HEADER.iter() {
133            headers.insert(*k, v.parse().unwrap());
134        }
135        headers.insert(
136            "user-agent",
137            format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
138                .parse()
139                .unwrap(),
140        );
141        headers.insert(
142            "x-sdk-client",
143            format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
144                .parse()
145                .unwrap(),
146        );
147        headers.insert("x-acs-version", version.parse().unwrap());
148
149        // return RequestBuilder.
150        RequestBuilder {
151            access_key_id,
152            access_key_secret,
153            endpoint,
154            http_client_builder: ClientBuilder::new(),
155            request: Request {
156                method,
157                uri,
158                body: None,
159                query: Vec::new(),
160                headers,
161            },
162        }
163    }
164
165    /// Set body for request.
166    pub fn body(mut self, body: &str) -> Result<Self> {
167        // compute body length and md5.
168        let body = body.to_string();
169        let mut hasher = Md5::new();
170        hasher.update(body.as_bytes());
171        let md5_result = hasher.finalize();
172
173        // update headers.
174        self.request
175            .headers
176            .insert("content-length", body.len().to_string().parse()?);
177        self.request
178            .headers
179            .insert("content-md5", base64::encode(md5_result).parse()?);
180
181        // store body string.
182        self.request.body = Some(body);
183
184        Ok(self)
185    }
186
187    /// Set header for request.
188    pub fn header<I>(mut self, iter: I) -> Self
189    where
190        I: IntoIterator,
191        I::Item: Borrow<(&'a str, &'a str)>,
192    {
193        for i in iter.into_iter() {
194            let h = i.borrow();
195            let key = HeaderName::from_str(h.0);
196            let value = HeaderValue::from_str(h.1);
197            // ingore invailid header.
198            if let Ok(key) = key {
199                if let Ok(value) = value {
200                    self.request.headers.insert(key, value);
201                }
202            }
203        }
204        self
205    }
206
207    /// Set queries for request.
208    pub fn query<I>(mut self, iter: I) -> Self
209    where
210        I: IntoIterator,
211        I::Item: Borrow<(&'a str, &'a str)>,
212    {
213        for i in iter.into_iter() {
214            let b = i.borrow();
215            self.request.query.push((b.0.to_string(), b.1.to_string()));
216        }
217        self
218    }
219
220    /// Send a request to api service.
221    pub fn send(mut self) -> Result<String> {
222        // add date header.
223        // RFC 1123: %a, %d %b %Y %H:%M:%S GMT
224        let format = format_description!(
225            "[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT"
226        );
227        let ts = OffsetDateTime::now_utc()
228            .format(&format)
229            .map_err(|e| anyhow!(format!("Invalid RFC 1123 Date: {}", e)))?;
230        self.request.headers.insert("date", ts.parse()?);
231
232        // add nonce header.
233        let nonce = Uuid::new_v4().to_string();
234        self.request
235            .headers
236            .insert("x-acs-signature-nonce", nonce.parse()?);
237
238        // parse host of self.endpoint.
239        let endpoint = Url::parse(self.endpoint)?;
240        let host = endpoint
241            .host_str()
242            .ok_or_else(|| anyhow!("parse endpoint failed"))?;
243        self.request.headers.insert("host", host.parse()?);
244
245        // compute `Authorization` field.
246        let authorization = format!("acs {}:{}", self.access_key_id, self.signature()?);
247        self.request
248            .headers
249            .insert("Authorization", authorization.parse()?);
250
251        // build http client.
252        let final_url = format!("{}{}", self.endpoint, self.request.uri);
253        let mut http_client = self
254            .http_client_builder
255            .build()?
256            .request(self.request.method.parse()?, final_url);
257
258        // set body.
259        if let Some(body) = self.request.body {
260            http_client = http_client.body(body);
261        }
262
263        // send request.
264        let response = http_client
265            .headers(self.request.headers)
266            .query(&self.request.query)
267            .send()?
268            .text()?;
269
270        // return response.
271        Ok(response)
272    }
273
274    /// Set a timeout for connect, read and write operations of a `Client`.
275    ///
276    /// Default is 30 seconds.
277    ///
278    /// Pass `None` to disable timeout.
279    pub fn timeout<T>(mut self, timeout: T) -> Self
280    where
281        T: Into<Option<Duration>>,
282    {
283        self.http_client_builder = self.http_client_builder.timeout(timeout);
284        self
285    }
286
287    /// Compute canonicalized headers.
288    fn canonicalized_headers(&self) -> String {
289        let mut headers: Vec<(String, String)> = self
290            .request
291            .headers
292            .iter()
293            .filter_map(|(k, v)| {
294                let k = k.as_str().to_lowercase();
295                if k.starts_with("x-acs-") {
296                    Some((k, v.to_str().unwrap().to_string()))
297                } else {
298                    None
299                }
300            })
301            .collect();
302        headers.sort_by(|a, b| a.0.cmp(&b.0));
303
304        let headers: Vec<String> = headers
305            .iter()
306            .map(|(k, v)| format!("{}:{}", k, v))
307            .collect();
308
309        headers.join("\n")
310    }
311
312    /// Compute canonicalized resource.
313    fn canonicalized_resource(&self) -> String {
314        if !self.request.query.is_empty() {
315            let mut params = self.request.query.clone();
316            params.sort_by_key(|item| item.0.clone());
317            let params: Vec<String> = params.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
318            let sorted_query_string = params.join("&");
319            format!("{}?{}", self.request.uri, sorted_query_string)
320        } else {
321            self.request.uri.clone()
322        }
323    }
324
325    /// Compute signature for request.
326    fn signature(&self) -> Result<String> {
327        // build body.
328        let canonicalized_headers = self.canonicalized_headers();
329        let canonicalized_resource = self.canonicalized_resource();
330        let body = format!(
331            "{}\n{}\n{}\n{}\n{}\n{}\n{}",
332            self.request.method.to_uppercase(),
333            self.request.headers["accept"].to_str().unwrap(),
334            self.request
335                .headers
336                .get("content-md5")
337                .unwrap_or(&HeaderValue::from_static(""))
338                .to_str()
339                .unwrap(),
340            self.request
341                .headers
342                .get("content-type")
343                .unwrap_or(&HeaderValue::from_static(""))
344                .to_str()
345                .unwrap(),
346            self.request.headers["date"].to_str().unwrap(),
347            canonicalized_headers,
348            canonicalized_resource
349        );
350
351        // sign body.
352        let mut mac = HamcSha1::new_from_slice(self.access_key_secret.as_bytes())
353            .map_err(|e| anyhow!(format!("Invalid HMAC-SHA1 secret key: {}", e)))?;
354        mac.update(body.as_bytes());
355        let result = mac.finalize();
356        let code = result.into_bytes();
357
358        Ok(base64::encode(code))
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    #![allow(deprecated)]
365
366    use std::collections::HashMap;
367
368    use serde_json::json;
369
370    use super::*;
371
372    #[test]
373    fn roa_client_get_no_query() -> Result<()> {
374        // create roa style api client.
375        let aliyun_openapi_client = Client::new(
376            env::var("ACCESS_KEY_ID")?,
377            env::var("ACCESS_KEY_SECRET")?,
378            String::from("https://ros.aliyuncs.com"),
379            String::from("2015-09-01"),
380        );
381
382        // call `DescribeRegions` with empty queries.
383        let response = aliyun_openapi_client.get("/regions").send()?;
384
385        assert!(response.contains("Regions"));
386
387        Ok(())
388    }
389
390    #[test]
391    fn roa_client_get_with_timeout() -> Result<()> {
392        // create roa style api client.
393        let aliyun_openapi_client = Client::new(
394            env::var("ACCESS_KEY_ID")?,
395            env::var("ACCESS_KEY_SECRET")?,
396            String::from("https://ros.aliyuncs.com"),
397            String::from("2015-09-01"),
398        );
399
400        // call `DescribeRegions` with empty queries.
401        let response = aliyun_openapi_client
402            .get("/regions")
403            .timeout(Duration::from_millis(1))
404            .send();
405
406        assert!(response.is_err());
407
408        Ok(())
409    }
410
411    #[test]
412    fn roa_client_post_with_json_params() -> Result<()> {
413        // create roa style api client.
414        let aliyun_openapi_client = Client::new(
415            env::var("ACCESS_KEY_ID")?,
416            env::var("ACCESS_KEY_SECRET")?,
417            String::from("http://mt.aliyuncs.com"),
418            String::from("2019-01-02"),
419        );
420
421        // create params.
422        let mut params = HashMap::new();
423        params.insert("SourceText", "你好");
424        params.insert("SourceLanguage", "zh");
425        params.insert("TargetLanguage", "en");
426        params.insert("FormatType", "text");
427        params.insert("Scene", "general");
428
429        // call `DescribeRegions` with empty queries.
430        let response = aliyun_openapi_client
431            .post("/api/translate/web/general")
432            .header(&[("Content-Type", "application/json")])
433            .body(&json!(params).to_string())?
434            .send()?;
435
436        assert!(response.contains("Hello"));
437
438        Ok(())
439    }
440}