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::*;
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);
}
}