1use crate::error::{CosError, Result};
6use chrono::{DateTime, Utc};
7use hmac::{Hmac, Mac};
8use sha1::{Digest, Sha1};
9use std::collections::HashMap;
10use url::Url;
11
12type HmacSha1 = Hmac<Sha1>;
13
14#[derive(Debug, Clone)]
16pub struct Auth {
17 pub secret_id: String,
18 pub secret_key: String,
19}
20
21impl Auth {
22 pub fn new<S: Into<String>>(secret_id: S, secret_key: S) -> Self {
24 Self {
25 secret_id: secret_id.into(),
26 secret_key: secret_key.into(),
27 }
28 }
29
30 pub fn sign(
32 &self,
33 method: &str,
34 uri: &str,
35 headers: &HashMap<String, String>,
36 params: &HashMap<String, String>,
37 start_time: DateTime<Utc>,
38 end_time: DateTime<Utc>,
39 ) -> Result<String> {
40 let key_time = format!("{};{}", start_time.timestamp(), end_time.timestamp());
42
43 let sign_key = self.hmac_sha1(&key_time)?;
45
46 let http_string = self.build_http_string(method, uri, headers, params)?;
48
49 let string_to_sign = format!("sha1\n{}\n{}\n", key_time, self.sha1(&http_string)?);
51
52 let signature = self.hmac_sha1_with_key(&string_to_sign, &sign_key)?;
54
55 let authorization = format!(
57 "q-sign-algorithm=sha1&q-ak={}&q-sign-time={}&q-key-time={}&q-header-list={}&q-url-param-list={}&q-signature={}",
58 self.secret_id,
59 key_time,
60 key_time,
61 self.build_header_list(headers),
62 self.build_param_list(params),
63 signature
64 );
65
66 Ok(authorization)
67 }
68
69 fn build_http_string(
71 &self,
72 method: &str,
73 uri: &str,
74 headers: &HashMap<String, String>,
75 params: &HashMap<String, String>,
76 ) -> Result<String> {
77 let method = method.to_lowercase();
78 let uri_path = self.encode_uri_path(uri)?;
79 let params_string = self.build_params_string(params);
80 let headers_string = self.build_headers_string(headers);
81
82 Ok(format!(
83 "{}\n{}\n{}\n{}\n",
84 method, uri_path, params_string, headers_string
85 ))
86 }
87
88 fn encode_uri_path(&self, uri: &str) -> Result<String> {
90 let url = Url::parse(&format!("http://example.com{}", uri))
91 .map_err(|e| CosError::other(format!("Invalid URI: {}", e)))?;
92 Ok(url.path().to_string())
93 }
94
95 fn build_params_string(&self, params: &HashMap<String, String>) -> String {
97 let mut sorted_params: Vec<_> = params.iter().collect();
98 sorted_params.sort_by_key(|(k, _)| k.to_lowercase());
99
100 sorted_params
101 .iter()
102 .map(|(k, v)| format!("{}={}", k.to_lowercase(), urlencoding::encode(v)))
103 .collect::<Vec<_>>()
104 .join("&")
105 }
106
107 fn build_headers_string(&self, headers: &HashMap<String, String>) -> String {
109 let mut sorted_headers: Vec<_> = headers.iter().collect();
110 sorted_headers.sort_by_key(|(k, _)| k.to_lowercase());
111
112 sorted_headers
113 .iter()
114 .map(|(k, v)| format!("{}={}", k.to_lowercase(), urlencoding::encode(v)))
115 .collect::<Vec<_>>()
116 .join("&")
117 }
118
119 fn build_header_list(&self, headers: &HashMap<String, String>) -> String {
121 let mut header_keys: Vec<_> = headers.keys().map(|k| k.to_lowercase()).collect();
122 header_keys.sort();
123 header_keys.join(";")
124 }
125
126 fn build_param_list(&self, params: &HashMap<String, String>) -> String {
128 let mut param_keys: Vec<_> = params.keys().map(|k| k.to_lowercase()).collect();
129 param_keys.sort();
130 param_keys.join(";")
131 }
132
133 fn hmac_sha1(&self, data: &str) -> Result<String> {
135 self.hmac_sha1_with_key(data, &self.secret_key)
136 }
137
138 fn hmac_sha1_with_key(&self, data: &str, key: &str) -> Result<String> {
140 let mut mac = HmacSha1::new_from_slice(key.as_bytes())
141 .map_err(|e| CosError::auth(format!("HMAC key error: {}", e)))?;
142 mac.update(data.as_bytes());
143 let result = mac.finalize();
144 Ok(hex::encode(result.into_bytes()))
145 }
146
147 fn sha1(&self, data: &str) -> Result<String> {
149 let mut hasher = Sha1::new();
150 hasher.update(data.as_bytes());
151 Ok(hex::encode(hasher.finalize()))
152 }
153}
154
155mod urlencoding {
157 pub fn encode(input: &str) -> String {
158 url::form_urlencoded::byte_serialize(input.as_bytes()).collect()
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use chrono::TimeZone;
166
167 #[test]
168 fn test_auth_sign() {
169 let auth = Auth::new("test_secret_id", "test_secret_key");
170 let mut headers = HashMap::new();
171 headers.insert("host".to_string(), "example.com".to_string());
172 headers.insert("content-type".to_string(), "application/json".to_string());
173
174 let mut params = HashMap::new();
175 params.insert("param1".to_string(), "value1".to_string());
176
177 let start_time = Utc.timestamp_opt(1234567890, 0).unwrap();
178 let end_time = Utc.timestamp_opt(1234567890 + 3600, 0).unwrap();
179
180 let result = auth.sign("GET", "/test", &headers, ¶ms, start_time, end_time);
181 assert!(result.is_ok());
182 }
183
184 #[test]
185 fn test_build_params_string() {
186 let auth = Auth::new("id", "key");
187 let mut params = HashMap::new();
188 params.insert("b".to_string(), "value2".to_string());
189 params.insert("a".to_string(), "value1".to_string());
190
191 let result = auth.build_params_string(¶ms);
192 assert_eq!(result, "a=value1&b=value2");
193 }
194}