use chrono::{DateTime, Duration, Utc}; use http::{ header::{HeaderMap, ToStrError}, method::Method, uri::PathAndQuery, }; use std::collections::BTreeMap; pub mod create; pub mod verify; use self::{ create::Unsigned, verify::{Unvalidated, ValidateError}, }; const REQUEST_TARGET: &'static str = "(request-target)"; const CREATED: &'static str = "(crated)"; const EXPIRES: &'static str = "(expires)"; const KEY_ID_FIELD: &'static str = "keyId"; const ALGORITHM_FIELD: &'static str = "algorithm"; const ALGORITHM_VALUE: &'static str = "hs2019"; const CREATED_FIELD: &'static str = "created"; const EXPIRES_FIELD: &'static str = "expires"; const HEADERS_FIELD: &'static str = "headers"; const SIGNATURE_FIELD: &'static str = "signature"; #[derive(Clone)] pub struct Config { pub expires: Duration, } impl Config { pub fn normalize( &self, method: Method, path_and_query: &PathAndQuery, headers: &HeaderMap, ) -> Result { let (sig_headers, mut btm) = build_headers_list(headers)?; let created = Utc::now(); let expires = created + self.expires; let signing_string = build_signing_string( method, path_and_query, Some(created), Some(expires), &sig_headers, &mut btm, ); Ok(Unsigned { signing_string, sig_headers, created, expires, }) } pub fn validate(&self, unvalidated: Unvalidated, f: F) -> Result where F: FnOnce(&[u8], &str) -> T, { if let Some(expires) = unvalidated.expires { if expires < unvalidated.parsed_at { return Err(ValidateError::Expired); } } if let Some(created) = unvalidated.created { if created + self.expires < unvalidated.parsed_at { return Err(ValidateError::Expired); } } let v = base64::decode(&unvalidated.signature).map_err(|_| ValidateError::Decode)?; Ok((f)(&v, &unvalidated.signing_string)) } } fn build_headers_list( headers: &HeaderMap, ) -> Result<(Vec, BTreeMap), ToStrError> { let btm: BTreeMap = headers .iter() .map(|(k, v)| { v.to_str() .map(|v| (k.as_str().to_lowercase().to_owned(), v.to_owned())) }) .collect::, _>>()?; let http_header_keys: Vec = btm.keys().cloned().collect(); let mut sig_headers = vec![ REQUEST_TARGET.to_owned(), CREATED.to_owned(), EXPIRES.to_owned(), ]; sig_headers.extend(http_header_keys); Ok((sig_headers, btm)) } fn build_signing_string( method: Method, path_and_query: &PathAndQuery, created: Option>, expires: Option>, sig_headers: &[String], btm: &mut BTreeMap, ) -> String { let request_target = format!("{} {}", method.to_string().to_lowercase(), path_and_query); btm.insert(REQUEST_TARGET.to_owned(), request_target.clone()); if let Some(created) = created { btm.insert(CREATED.to_owned(), created.timestamp().to_string()); } if let Some(expires) = expires { btm.insert(EXPIRES.to_owned(), expires.timestamp().to_string()); } let signing_string = sig_headers .iter() .filter_map(|h| btm.remove(h).map(|v| format!("{}: {}", h, v))) .collect::>() .join("\n"); signing_string } impl Default for Config { fn default() -> Self { Config { expires: Duration::seconds(10), } } } #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } }