rust_qcos/
signer.rs

1//! 接口签名
2use chrono::Utc;
3use ring::hmac;
4use sha1::{Digest, Sha1};
5use std::collections::HashMap;
6use std::str;
7use urlencoding::{decode, encode};
8
9/// [文档](https://cloud.tencent.com/document/product/436/7778)
10pub struct Signer<'a> {
11    method: &'a str,
12    url_path: &'a str,
13    headers: Option<&'a HashMap<String, String>>,
14    query: Option<&'a HashMap<String, String>>,
15}
16
17impl<'a> Signer<'a> {
18    pub fn new(
19        method: &'a str,
20        url_path: &'a str,
21        headers: Option<&'a HashMap<String, String>>,
22        query: Option<&'a HashMap<String, String>>,
23    ) -> Self {
24        Self {
25            method,
26            url_path,
27            headers,
28            query,
29        }
30    }
31
32    fn get_key_time(&self, valid_seconds: u32) -> String {
33        let start = Utc::now().timestamp();
34        let end = start + valid_seconds as i64;
35        format!("{};{}", start, end)
36    }
37
38    fn get_sign_key(&self, key_time: &str, secret_key: &str) -> String {
39        let key = hmac::Key::new(
40            hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY,
41            secret_key.to_string().as_bytes(),
42        );
43        let signature = hmac::sign(&key, key_time.as_bytes());
44        let s: Vec<String> = signature
45            .as_ref()
46            .iter()
47            .map(|x| format!("{:02x?}", x))
48            .collect();
49        s.join("")
50    }
51
52    fn encode_data(&self, data: &HashMap<String, String>) -> HashMap<String, String> {
53        let mut res = HashMap::new();
54        for (k, v) in data.iter() {
55            res.insert(encode(k).to_string().to_lowercase(), encode(v).to_string());
56        }
57        res
58    }
59
60    fn get_url_param_list(&self) -> String {
61        if let Some(query) = self.query {
62            let mut keys: Vec<String> = Vec::new();
63            let encoded_data = self.encode_data(query);
64            for k in encoded_data.keys() {
65                keys.push(k.to_string());
66            }
67            keys.sort();
68            return keys.join(";");
69        }
70        "".to_string()
71    }
72
73    fn get_http_parameters(&self) -> String {
74        if let Some(query) = self.query {
75            let mut keys: Vec<String> = Vec::new();
76            let encoded_data = self.encode_data(query);
77            for k in encoded_data.keys() {
78                keys.push(k.to_string());
79            }
80            keys.sort();
81            let mut res: Vec<String> = Vec::new();
82            for key in keys {
83                let v = encoded_data.get(&key).unwrap();
84                res.push([key, v.to_string()].join("="));
85            }
86            return res.join("&");
87        }
88        "".to_string()
89    }
90
91    fn get_header_list(&self) -> String {
92        if let Some(headers) = self.headers {
93            let mut keys: Vec<String> = Vec::new();
94            let encoded_data = self.encode_data(headers);
95            for k in encoded_data.keys() {
96                keys.push(k.to_string());
97            }
98            keys.sort();
99            return keys.join(";");
100        }
101        "".to_string()
102    }
103
104    fn get_heades(&self) -> String {
105        if let Some(headers) = self.headers {
106            let mut keys: Vec<String> = Vec::new();
107            let encoded_data = self.encode_data(headers);
108            for k in encoded_data.keys() {
109                keys.push(k.to_string());
110            }
111            keys.sort();
112            let mut res: Vec<String> = Vec::new();
113            for key in keys {
114                let v = encoded_data.get(&key).unwrap();
115                res.push([key, v.to_string()].join("="));
116            }
117            return res.join("&");
118        }
119        "".to_string()
120    }
121
122    fn get_http_string(&self) -> String {
123        let s = [
124            self.method.to_string(),
125            decode(self.url_path).unwrap().to_string(),
126            self.get_http_parameters(),
127            self.get_heades(),
128        ];
129        s.join("\n") + "\n"
130    }
131
132    fn get_string_to_sign(&self, key_time: &'a str) -> String {
133        let mut s = vec!["sha1".to_string(), key_time.to_string()];
134        let http_string = self.get_http_string();
135        let mut hasher = Sha1::new();
136        hasher.update(&http_string);
137        let result = hasher.finalize();
138        let digest: Vec<String> = result
139            .as_slice()
140            .iter()
141            .map(|x| format!("{:02x?}", x))
142            .collect();
143        s.push(digest.join(""));
144        s.join("\n") + "\n"
145    }
146
147    pub fn get_signature(&self, secret_key: &str, secret_id: &str, valid_seconds: u32) -> String {
148        let key_time = self.get_key_time(valid_seconds);
149        let string_to_sign = self.get_string_to_sign(&key_time);
150        let sign_key = self.get_sign_key(&key_time, secret_key);
151        let signature = self.get_sign_key(&string_to_sign, &sign_key);
152        let header_list = self.get_header_list();
153        let param_list = self.get_url_param_list();
154        format!("q-sign-algorithm=sha1&q-ak={}&q-sign-time={}&q-key-time={}&q-header-list={}&q-url-param-list={}&q-signature={}", secret_id, key_time, key_time, header_list, param_list, signature)
155    }
156}
157
158#[cfg(test)]
159mod test {
160    use crate::signer::Signer;
161    use std::collections::HashMap;
162
163    #[test]
164    fn test_get_key_time() {
165        let signer = Signer::new("", "", None, None);
166        println!("{}", signer.get_key_time(100));
167    }
168
169    #[test]
170    fn test_get_url_param_list() {
171        let mut query = HashMap::new();
172        query.insert("a".to_string(), "a ".to_string());
173        query.insert("B".to_string(), " b".to_string());
174        let signer = Signer::new("", "", None, Some(&query));
175        let s = signer.get_url_param_list();
176        assert_eq!(s, "a;b");
177        let s = signer.get_http_parameters();
178        assert_eq!(s, "a=a%20&b=%20b");
179    }
180
181    #[test]
182    fn test_get_http_string() {
183        let mut query = HashMap::new();
184        query.insert("a".to_string(), "a ".to_string());
185        query.insert("B".to_string(), " b".to_string());
186        let mut headers = HashMap::new();
187        headers.insert("h".to_string(), "h".to_string());
188        headers.insert("user-agent".to_string(), "test".to_string());
189        let signer = Signer::new("get", "/path", Some(&headers), Some(&query));
190        assert_eq!(
191            signer.get_http_string(),
192            "get\n/path\na=a%20&b=%20b\nh=h&user-agent=test\n"
193        );
194        assert_eq!(
195            signer.get_string_to_sign("1648999396;1648999496"),
196            "sha1\n1648999396;1648999496\n963bfe30ee40d402ee00506981bab650e72134f6\n"
197        );
198    }
199
200    #[test]
201    fn test_get_signature() {
202        let mut headers = HashMap::new();
203        headers.insert("Content-Type".to_string(), "text/plain".to_string());
204        headers.insert("Content-Length".to_string(), "13".to_string());
205        headers.insert(
206            "Host".to_string(),
207            "examplebucket-1250000000.cos.ap-beijing.myqcloud.com".to_string(),
208        );
209        headers.insert(
210            "Content-MD5".to_string(),
211            "mQ/fVh815F3k6TAUm8m0eg==".to_string(),
212        );
213        headers.insert("x-cos-acl".to_string(), "private".to_string());
214        headers.insert(
215            "x-cos-grant-read".to_string(),
216            "uin=\"100000000011\"".to_string(),
217        );
218        headers.insert(
219            "Date".to_string(),
220            "Thu, 16 May 2019 06:45:51 GMT".to_string(),
221        );
222        let signer = Signer::new(
223            "put",
224            "/exampleobject(%E8%85%BE%E8%AE%AF%E4%BA%91)",
225            Some(&headers),
226            None,
227        );
228        let key_time = "1557989151;1557996351";
229        assert_eq!(
230            signer.get_sign_key(key_time, "BQYIM75p8x0iWVFSIgqEKwFprpRSVHlz"),
231            "eb2519b498b02ac213cb1f3d1a3d27a3b3c9bc5f"
232        );
233
234        assert_eq!(signer.get_url_param_list(), "");
235        assert_eq!(signer.get_http_parameters(), "");
236        assert_eq!(
237            signer.get_header_list(),
238            "content-length;content-md5;content-type;date;host;x-cos-acl;x-cos-grant-read"
239        );
240
241        assert_eq!(signer.get_heades(), "content-length=13&content-md5=mQ%2FfVh815F3k6TAUm8m0eg%3D%3D&content-type=text%2Fplain&date=Thu%2C%2016%20May%202019%2006%3A45%3A51%20GMT&host=examplebucket-1250000000.cos.ap-beijing.myqcloud.com&x-cos-acl=private&x-cos-grant-read=uin%3D%22100000000011%22");
242
243        assert_eq!(signer.get_http_string(), "put\n/exampleobject(腾讯云)\n\ncontent-length=13&content-md5=mQ%2FfVh815F3k6TAUm8m0eg%3D%3D&content-type=text%2Fplain&date=Thu%2C%2016%20May%202019%2006%3A45%3A51%20GMT&host=examplebucket-1250000000.cos.ap-beijing.myqcloud.com&x-cos-acl=private&x-cos-grant-read=uin%3D%22100000000011%22\n");
244
245        assert_eq!(
246            signer.get_string_to_sign(key_time),
247            "sha1\n1557989151;1557996351\n8b2751e77f43a0995d6e9eb9477f4b685cca4172\n"
248        );
249    }
250}