use std::time::{Duration, SystemTime, UNIX_EPOCH};
use base64::engine::{general_purpose as b64_general_purpose, Engine};
use hmac::Mac;
use reqwest::header::{HeaderMap, HeaderValue};
use crate::ExternalMatchClientError;
const RENEGADE_HEADER_NAMESPACE: &str = "x-renegade";
pub const RENEGADE_AUTH_HEADER_NAME: &str = "x-renegade-auth";
pub const RENEGADE_SIG_EXPIRATION_HEADER_NAME: &str = "x-renegade-auth-expiration";
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct HmacKey([u8; 32]);
impl HmacKey {
pub fn from_base64_string(s: &str) -> Result<Self, ExternalMatchClientError> {
let bytes = b64_general_purpose::STANDARD
.decode(s)
.map_err(|_| ExternalMatchClientError::InvalidApiSecret)?;
Ok(Self(bytes.try_into().unwrap()))
}
}
pub fn add_expiring_auth_to_headers(
path: &str,
headers: &mut HeaderMap,
body: &[u8],
key: &HmacKey,
expiration: Duration,
) {
let expiration_ts = get_current_time_millis() + expiration.as_millis() as u64;
headers.insert(RENEGADE_SIG_EXPIRATION_HEADER_NAME, expiration_ts.into());
let sig = create_request_signature(path, headers, body, key);
let b64_sig = b64_general_purpose::STANDARD_NO_PAD.encode(sig);
let sig_header = HeaderValue::from_str(&b64_sig).expect("b64 encoding should not fail");
headers.insert(RENEGADE_AUTH_HEADER_NAME, sig_header);
}
pub fn create_request_signature(
path: &str,
headers: &HeaderMap,
body: &[u8],
key: &HmacKey,
) -> Vec<u8> {
let path_bytes = path.as_bytes();
let header_bytes = get_header_bytes(headers);
let payload = [path_bytes, &header_bytes, body].concat();
let mut hmac = HmacSha256::new_from_slice(&key.0).expect("hmac can handle all slice lengths");
hmac.update(&payload);
hmac.finalize().into_bytes().to_vec()
}
fn get_header_bytes(headers: &HeaderMap) -> Vec<u8> {
let mut headers_buf = Vec::new();
let mut renegade_headers: Vec<(String, &HeaderValue)> = headers
.iter()
.filter_map(|(k, v)| {
let key = k.to_string().to_lowercase();
if key.starts_with(RENEGADE_HEADER_NAMESPACE) && key != RENEGADE_AUTH_HEADER_NAME {
Some((key, v))
} else {
None
}
})
.collect();
renegade_headers.sort_by(|a, b| a.0.cmp(&b.0));
for (key, value) in renegade_headers {
headers_buf.extend_from_slice(key.as_bytes());
headers_buf.extend_from_slice(value.as_bytes());
}
headers_buf
}
pub fn get_current_time_millis() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).expect("negative timestamp").as_millis() as u64
}