150 lines
3.8 KiB
Rust
150 lines
3.8 KiB
Rust
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<Unsigned, ToStrError> {
|
|
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<F, T>(&self, unvalidated: Unvalidated, f: F) -> Result<T, ValidateError>
|
|
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<String>, BTreeMap<String, String>), ToStrError> {
|
|
let btm: BTreeMap<String, String> = headers
|
|
.iter()
|
|
.map(|(k, v)| {
|
|
v.to_str()
|
|
.map(|v| (k.as_str().to_lowercase().to_owned(), v.to_owned()))
|
|
})
|
|
.collect::<Result<BTreeMap<String, String>, _>>()?;
|
|
|
|
let http_header_keys: Vec<String> = 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<DateTime<Utc>>,
|
|
expires: Option<DateTime<Utc>>,
|
|
sig_headers: &[String],
|
|
btm: &mut BTreeMap<String, String>,
|
|
) -> 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::<Vec<_>>()
|
|
.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);
|
|
}
|
|
}
|