aliyun_openapi_core_rust_sdk/
rpc.rs1use 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
12const DEFAULT_PARAM: &[(&str, &str)] = &[
14 ("Format", "JSON"),
15 ("SignatureMethod", "HMAC-SHA1"),
16 ("SignatureVersion", "1.0"),
17];
18
19type HamcSha1 = Hmac<Sha1>;
20
21#[derive(Debug)]
23struct Request {
24 action: String,
25 method: String,
26 query: Vec<(String, String)>,
27}
28
29#[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 access_key_id: String,
38 access_key_secret: String,
40 endpoint: String,
42 version: String,
44}
45
46impl Client {
47 #![allow(deprecated)]
48
49 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 pub fn get(&self, action: &str) -> RequestBuilder {
68 self.execute("GET", action)
69 }
70
71 pub fn post(&self, action: &str) -> RequestBuilder {
75 self.execute("POST", action)
76 }
77
78 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 #[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 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 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 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 let response = reqwest::blocking::get(final_url)?.text()?;
132
133 Ok(response)
135 }
136}
137
138pub struct RequestBuilder<'a> {
140 access_key_id: &'a str,
142 access_key_secret: &'a str,
144 endpoint: &'a str,
146 version: &'a str,
148 request: Request,
150 http_client_builder: ClientBuilder,
152}
153
154impl<'a> RequestBuilder<'a> {
155 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 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 pub fn send(self) -> Result<String> {
193 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 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 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 let http_client = self
236 .http_client_builder
237 .build()?
238 .request(self.request.method.parse()?, final_url);
239
240 let response = http_client.send()?.text()?;
242
243 Ok(response)
245 }
246
247 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 #[test]
288 fn rpc_client_no_query_compatibility_020() -> Result<()> {
289 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 let response = aliyun_openapi_client.request("DescribeRegions", &[])?;
299
300 assert!(response.contains("Regions"));
301
302 Ok(())
303 }
304
305 #[test]
307 fn rpc_client_with_query_compatibility_020() -> Result<()> {
308 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 let response =
318 aliyun_openapi_client.request("DescribeInstances", &[("RegionId", "cn-hangzhou")])?;
319
320 assert!(response.contains("Instances"));
321
322 Ok(())
323 }
324
325 #[test]
327 fn rpc_client_get_no_query() -> Result<()> {
328 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 let response = aliyun_openapi_client.get("DescribeRegions").send()?;
338
339 assert!(response.contains("Regions"));
340
341 Ok(())
342 }
343
344 #[test]
346 fn rpc_client_get_with_query() -> Result<()> {
347 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 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 #[test]
368 fn rpc_client_get_with_timeout() -> Result<()> {
369 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 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 #[test]
390 fn rpc_client_post_no_query() -> Result<()> {
391 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 let response = aliyun_openapi_client.post("DescribeRegions").send()?;
401
402 assert!(response.contains("Regions"));
403
404 Ok(())
405 }
406
407 #[test]
409 fn rpc_client_post_with_query() -> Result<()> {
410 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 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}