1use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};
4
5use hmac::{digest::crypto_common::InvalidLength as HmacInvalidLength, Hmac, Mac as _};
6use http::{
7 header::InvalidHeaderValue, uri::InvalidUri as HttpInvalidUri, HeaderMap, HeaderValue, Uri,
8};
9use sha1::Sha1;
10
11type HmacSha1 = Hmac<Sha1>;
13const DELIMITER: char = '\n';
14
15pub const HTTP_HEADER_AUTHORIZATION: &str = "Authorization";
17pub const HTTP_HEADER_TIMESTAMP: &str = "Timestamp";
18
19#[derive(Debug, Clone)]
21pub struct AuthSignature;
22
23impl AuthSignature {
24 pub fn http_headers(
25 &self,
26 url: impl AsRef<str>,
27 app_id: impl AsRef<str>,
28 secret: impl AsRef<str>,
29 ) -> Result<HeaderMap, AuthSignatureError> {
30 let timestamp = SystemTime::now()
31 .duration_since(UNIX_EPOCH)
32 .map_err(AuthSignatureError::GetTimestampFailed)?
33 .as_millis();
34
35 let path_with_query =
36 url_to_path_with_query(url).map_err(AuthSignatureError::UrlInvalid)?;
37
38 let string_to_sign = format!("{}{}{}", timestamp, DELIMITER, path_with_query);
39 let signature = sign_string(string_to_sign, secret.as_ref())
40 .map_err(|err| AuthSignatureError::AccessKeySecretInvalidLength(err.0))?;
41
42 let mut headers = HeaderMap::new();
44
45 headers.insert(
46 HTTP_HEADER_AUTHORIZATION,
47 format!("Apollo {}:{}", app_id.as_ref(), signature)
48 .parse::<HeaderValue>()
49 .map_err(AuthSignatureError::ToHeaderValueFailed)?,
50 );
51
52 headers.insert(
53 HTTP_HEADER_TIMESTAMP,
54 timestamp
55 .to_string()
56 .parse::<HeaderValue>()
57 .map_err(AuthSignatureError::ToHeaderValueFailed)?,
58 );
59
60 Ok(headers)
61 }
62}
63
64error_macro::r#enum! {
65 pub enum AuthSignatureError {
66 GetTimestampFailed(SystemTimeError),
67 UrlInvalid(UrlInvalid),
68 AccessKeySecretInvalidLength(HmacInvalidLength),
69 ToHeaderValueFailed(InvalidHeaderValue),
70 }
71}
72
73fn sign_string(
75 string_to_sign: impl AsRef<[u8]>,
76 access_key_secret: impl AsRef<[u8]>,
77) -> Result<String, AccessKeySecretInvalidLength> {
78 let mut hmac = HmacSha1::new_from_slice(access_key_secret.as_ref())
79 .map_err(AccessKeySecretInvalidLength)?;
80 hmac.update(string_to_sign.as_ref());
81 Ok(base64::encode(hmac.finalize().into_bytes()))
82}
83
84error_macro::r#struct! {
85 pub struct AccessKeySecretInvalidLength(HmacInvalidLength);
86}
87
88fn url_to_path_with_query(raw_url: impl AsRef<str>) -> Result<String, UrlInvalid> {
90 let uri = raw_url
91 .as_ref()
92 .parse::<Uri>()
93 .map_err(UrlInvalid::InvalidUri)?;
94
95 uri.path_and_query()
96 .map(|x| x.to_string())
97 .ok_or(UrlInvalid::PathAndQueryEmpty(()))
98}
99
100error_macro::r#enum! {
101 pub enum UrlInvalid {
102 InvalidUri(HttpInvalidUri),
103 PathAndQueryEmpty(()),
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 const RAW_URL: &str = "http://baidu.com/a/b?key=1";
114 const SECRET: &str = "6ce3ff7e96a24335a9634fe9abca6d51";
115 const APP_ID: &str = "testApplication_yang";
116
117 #[test]
118 fn test_sign_string() {
119 assert_eq!(
120 sign_string(RAW_URL, SECRET).unwrap(),
121 "mcS95GXa7CpCjIfrbxgjKr0lRu8="
122 );
123 }
124
125 #[test]
126 fn test_url_to_path_with_query() {
127 assert_eq!(url_to_path_with_query(RAW_URL).unwrap(), "/a/b?key=1");
128 assert_eq!(
129 url_to_path_with_query("http://baidu.com/a/b").unwrap(),
130 "/a/b"
131 );
132 }
133
134 #[test]
135 fn test_http_headers() {
136 let headers = AuthSignature.http_headers(RAW_URL, APP_ID, SECRET).unwrap();
137 println!("{:?}", headers);
138 assert!(headers
139 .get("Authorization")
140 .unwrap()
141 .as_bytes()
142 .starts_with(b"Apollo testApplication_yang:"));
143 assert_eq!(headers.get("Timestamp").unwrap().len(), 13);
144 }
145}