apolloconfig-sig 0.1.0

apolloconfig sig
Documentation
//! [Ref](https://github.com/apolloconfig/agollo/blob/master/protocol/auth/sign/sign.go)

use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};

use hmac::{digest::crypto_common::InvalidLength as HmacInvalidLength, Hmac, Mac as _};
use http::{
    header::InvalidHeaderValue, uri::InvalidUri as HttpInvalidUri, HeaderMap, HeaderValue, Uri,
};
use sha1::Sha1;

//
type HmacSha1 = Hmac<Sha1>;
const DELIMITER: char = '\n';

//
pub const HTTP_HEADER_AUTHORIZATION: &str = "Authorization";
pub const HTTP_HEADER_TIMESTAMP: &str = "Timestamp";

//
#[derive(Debug, Clone)]
pub struct AuthSignature;

impl AuthSignature {
    pub fn http_headers(
        &self,
        url: impl AsRef<str>,
        app_id: impl AsRef<str>,
        secret: impl AsRef<str>,
    ) -> Result<HeaderMap, AuthSignatureError> {
        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_err(AuthSignatureError::GetTimestampFailed)?
            .as_millis();

        let path_with_query =
            url_to_path_with_query(url).map_err(AuthSignatureError::UrlInvalid)?;

        let string_to_sign = format!("{}{}{}", timestamp, DELIMITER, path_with_query);
        let signature = sign_string(string_to_sign, secret.as_ref())
            .map_err(|err| AuthSignatureError::AccessKeySecretInvalidLength(err.0))?;

        //
        let mut headers = HeaderMap::new();

        headers.insert(
            HTTP_HEADER_AUTHORIZATION,
            format!("Apollo {}:{}", app_id.as_ref(), signature)
                .parse::<HeaderValue>()
                .map_err(AuthSignatureError::ToHeaderValueFailed)?,
        );

        headers.insert(
            HTTP_HEADER_TIMESTAMP,
            timestamp
                .to_string()
                .parse::<HeaderValue>()
                .map_err(AuthSignatureError::ToHeaderValueFailed)?,
        );

        Ok(headers)
    }
}

error_macro::r#enum! {
    pub enum AuthSignatureError {
        GetTimestampFailed(SystemTimeError),
        UrlInvalid(UrlInvalid),
        AccessKeySecretInvalidLength(HmacInvalidLength),
        ToHeaderValueFailed(InvalidHeaderValue),
    }
}

//
fn sign_string(
    string_to_sign: impl AsRef<[u8]>,
    access_key_secret: impl AsRef<[u8]>,
) -> Result<String, AccessKeySecretInvalidLength> {
    let mut hmac = HmacSha1::new_from_slice(access_key_secret.as_ref())
        .map_err(AccessKeySecretInvalidLength)?;
    hmac.update(string_to_sign.as_ref());
    Ok(base64::encode(hmac.finalize().into_bytes()))
}

error_macro::r#struct! {
    pub struct AccessKeySecretInvalidLength(HmacInvalidLength);
}

//
fn url_to_path_with_query(raw_url: impl AsRef<str>) -> Result<String, UrlInvalid> {
    let uri = raw_url
        .as_ref()
        .parse::<Uri>()
        .map_err(UrlInvalid::InvalidUri)?;

    uri.path_and_query()
        .map(|x| x.to_string())
        .ok_or(UrlInvalid::PathAndQueryEmpty(()))
}

error_macro::r#enum! {
    pub enum UrlInvalid {
        InvalidUri(HttpInvalidUri),
        PathAndQueryEmpty(()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // https://github.com/apolloconfig/agollo/blob/master/protocol/auth/sign/sign_test.go

    const RAW_URL: &str = "http://baidu.com/a/b?key=1";
    const SECRET: &str = "6ce3ff7e96a24335a9634fe9abca6d51";
    const APP_ID: &str = "testApplication_yang";

    #[test]
    fn test_sign_string() {
        assert_eq!(
            sign_string(RAW_URL, SECRET).unwrap(),
            "mcS95GXa7CpCjIfrbxgjKr0lRu8="
        );
    }

    #[test]
    fn test_url_to_path_with_query() {
        assert_eq!(url_to_path_with_query(RAW_URL).unwrap(), "/a/b?key=1");
        assert_eq!(
            url_to_path_with_query("http://baidu.com/a/b").unwrap(),
            "/a/b"
        );
    }

    #[test]
    fn test_http_headers() {
        let headers = AuthSignature.http_headers(RAW_URL, APP_ID, SECRET).unwrap();
        println!("{:?}", headers);
        assert!(headers
            .get("Authorization")
            .unwrap()
            .as_bytes()
            .starts_with(b"Apollo testApplication_yang:"));
        assert_eq!(headers.get("Timestamp").unwrap().len(), 13);
    }
}