aliyun_openapi_core_rust_sdk/
roa.rs1use 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
15const 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#[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#[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 access_key_id: String,
43 access_key_secret: String,
45 endpoint: String,
47 version: String,
49}
50
51impl Client {
52 #![allow(deprecated)]
53
54 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 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 pub fn get(&self, uri: &str) -> RequestBuilder {
87 self.execute("GET", uri)
88 }
89
90 pub fn post(&self, uri: &str) -> RequestBuilder {
94 self.execute("POST", uri)
95 }
96
97 pub fn put(&self, uri: &str) -> RequestBuilder {
101 self.execute("PUT", uri)
102 }
103}
104
105#[derive(Debug)]
107pub struct RequestBuilder<'a> {
108 access_key_id: &'a str,
110 access_key_secret: &'a str,
112 endpoint: &'a str,
114 http_client_builder: ClientBuilder,
116 request: Request,
118}
119
120impl<'a> RequestBuilder<'a> {
121 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 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 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 pub fn body(mut self, body: &str) -> Result<Self> {
167 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 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 self.request.body = Some(body);
183
184 Ok(self)
185 }
186
187 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 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 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 pub fn send(mut self) -> Result<String> {
222 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 let nonce = Uuid::new_v4().to_string();
234 self.request
235 .headers
236 .insert("x-acs-signature-nonce", nonce.parse()?);
237
238 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 let authorization = format!("acs {}:{}", self.access_key_id, self.signature()?);
247 self.request
248 .headers
249 .insert("Authorization", authorization.parse()?);
250
251 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 if let Some(body) = self.request.body {
260 http_client = http_client.body(body);
261 }
262
263 let response = http_client
265 .headers(self.request.headers)
266 .query(&self.request.query)
267 .send()?
268 .text()?;
269
270 Ok(response)
272 }
273
274 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 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 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 fn signature(&self) -> Result<String> {
327 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 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 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 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 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 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 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 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 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}