apolloconfig_sig/
lib.rs

1//! [Ref](https://github.com/apolloconfig/agollo/blob/master/protocol/auth/sign/sign.go)
2
3use 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
11//
12type HmacSha1 = Hmac<Sha1>;
13const DELIMITER: char = '\n';
14
15//
16pub const HTTP_HEADER_AUTHORIZATION: &str = "Authorization";
17pub const HTTP_HEADER_TIMESTAMP: &str = "Timestamp";
18
19//
20#[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        //
43        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
73//
74fn 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
88//
89fn 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    // https://github.com/apolloconfig/agollo/blob/master/protocol/auth/sign/sign_test.go
112
113    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}