aliyun_openapi_core_rust_sdk/client/
rpc.rs1use 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 pub code: String,
18 pub message: String,
20 #[serde(default)]
22 pub request_id: String,
23 #[serde(default)]
25 pub recommend: String,
26}
27
28const 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#[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 access_key_id: String,
54 access_key_secret: String,
56 endpoint: String,
58 request: Request,
60}
61
62impl RPClient {
63 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 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 pub fn get(self, action: impl Into<String>) -> Self {
91 self.request("GET".to_string(), action.into())
92 }
93
94 pub fn post(self, action: impl Into<String>) -> Self {
98 self.request("POST".to_string(), action.into())
99 }
100
101 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 pub fn version(mut self, version: impl Into<String>) -> Self {
121 self.request.version = version.into();
122
123 self
124 }
125
126 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 pub fn timeout(mut self, timeout: Duration) -> Self {
140 self.request.timeout = Some(timeout);
141
142 self
143 }
144
145 pub async fn json<T: DeserializeOwned>(self) -> Result<T> {
148 Ok(self.send().await?.json::<T>().await?)
149 }
150
151 pub async fn text(self) -> Result<String> {
154 Ok(self.send().await?.text().await?)
155 }
156
157 pub async fn send(mut self) -> Result<Response> {
160 for (k, v) in DEFAULT_HEADER.iter() {
162 self.request.headers.insert(*k, v.parse()?);
163 }
164
165 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 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 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 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 let response = http_client.headers(self.request.headers).send().await?;
221
222 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 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
247fn 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 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 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 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 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}