1use chrono::Utc;
2use hmac::{Hmac, Mac};
3use reqwest::Client;
4use serde::de::DeserializeOwned;
5use sha2::{Digest, Sha256};
6use std::collections::BTreeMap;
7
8type HmacSha256 = Hmac<Sha256>;
9
10fn sign(key: &[u8], msg: &str) -> Vec<u8> {
11 let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can be created");
12 mac.update(msg.as_bytes());
13 mac.finalize().into_bytes().to_vec()
14}
15
16fn get_signature_key(secret_key: &str, date_stamp: &str, region: &str, service: &str) -> Vec<u8> {
17 let k_date = sign(secret_key.as_bytes(), date_stamp);
18 let k_region = sign(&k_date, region);
19 let k_service = sign(&k_region, service);
20 sign(&k_service, "request")
21}
22
23fn format_query(parameters: &BTreeMap<&str, &str>) -> String {
24 parameters
25 .iter()
26 .map(|(k, v)| format!("{}={}", k, v))
27 .collect::<Vec<_>>()
28 .join("&")
29}
30
31#[allow(clippy::too_many_arguments)]
32fn signature_v4(
33 access_key: &str,
34 secret_key: &str,
35 host: &str,
36 region: &str,
37 service: &str,
38 method: &str,
39 req_query: &str,
40 req_body: &str,
41) -> Result<(String, String), Box<dyn std::error::Error>> {
42 let now = Utc::now();
43 let current_date = now.format("%Y%m%dT%H%M%SZ").to_string();
44 let datestamp = now.format("%Y%m%d").to_string();
45
46 let payload_hash = {
47 let mut hasher = Sha256::new();
48 hasher.update(req_body.as_bytes());
49 hex::encode(hasher.finalize())
50 };
51
52 let content_type = "application/json";
53 let signed_headers = "content-type;host;x-content-sha256;x-date";
54
55 let canonical_headers = format!(
56 "content-type:{}\nhost:{}\nx-content-sha256:{}\nx-date:{}\n",
57 content_type, host, payload_hash, current_date
58 );
59
60 let canonical_request = format!(
61 "{}\n/\n{}\n{}\n{}\n{}",
62 method, req_query, canonical_headers, signed_headers, payload_hash
63 );
64
65 let algorithm = "HMAC-SHA256";
66 let credential_scope = format!("{}/{}/{}/request", datestamp, region, service);
67
68 let string_to_sign = format!(
69 "{}\n{}\n{}\n{}",
70 algorithm,
71 current_date,
72 credential_scope,
73 hex::encode(Sha256::digest(canonical_request.as_bytes()))
74 );
75
76 let signing_key = get_signature_key(secret_key, &datestamp, region, service);
77 let signature = hex::encode(sign(&signing_key, &string_to_sign));
78
79 let authorization_header = format!(
80 "{} Credential={}/{}, SignedHeaders={}, Signature={}",
81 algorithm, access_key, credential_scope, signed_headers, signature
82 );
83
84 Ok((authorization_header, current_date))
85}
86
87fn get_host(endpoint: &str) -> Result<String, String> {
88 reqwest::Url::parse(endpoint)
89 .map_err(|e| e.to_string())?
90 .host_str()
91 .map(|s| s.to_string())
92 .ok_or_else(|| "Invalid endpoint".into())
93}
94
95#[allow(clippy::too_many_arguments)]
96pub async fn send_request<T: DeserializeOwned>(
97 access_key: &str,
98 secret_key: &str,
99 endpoint: &str,
100 region: &str,
101 service: &str,
102 method: &str,
103 content_type: &str,
104 query_params: BTreeMap<&str, &str>,
105 body_params: serde_json::Value,
106) -> Result<T, Box<dyn std::error::Error>> {
107 let client = Client::new();
108 let formatted_query = format_query(&query_params);
109 let formatted_body = body_params.to_string();
110
111 let host = get_host(endpoint)?;
112
113 let (authorization_header, current_date) = signature_v4(
114 access_key,
115 secret_key,
116 &host,
117 region,
118 service,
119 method,
120 &formatted_query,
121 &formatted_body,
122 )?;
123
124 let payload_hash = hex::encode(Sha256::digest(formatted_body.as_bytes()));
125
126 let request_url = format!("{}?{}", endpoint, formatted_query);
127
128 let response = client
129 .request(method.parse()?, request_url)
130 .header("X-Date", current_date)
131 .header("Authorization", authorization_header)
132 .header("X-Content-Sha256", payload_hash)
133 .header("Content-Type", content_type)
134 .body(formatted_body)
135 .send()
136 .await?;
137
138 let data = response.json::<T>().await?;
139
140 Ok(data)
141}