use super::AmzDate;
use super::CredentialV4;
use crate::http::OrderedQs;
use crate::utils::crypto::is_sha256_checksum;
use smallvec::SmallVec;
#[derive(Debug)]
pub struct PresignedUrlV4<'a> {
pub algorithm: &'a str,
pub credential: CredentialV4<'a>,
pub amz_date: AmzDate,
pub expires: time::Duration,
pub signed_headers: SmallVec<[&'a str; 16]>,
pub signature: &'a str,
}
#[derive(Debug, thiserror::Error)]
#[error("ParsePresignedUrlError")]
pub struct ParsePresignedUrlError {
_priv: (),
}
struct PresignedQs<'a> {
algorithm: &'a str,
credential: &'a str,
date: &'a str,
expires: &'a str,
signed_headers: &'a str,
signature: &'a str,
}
impl<'a> PresignedQs<'a> {
fn from_ordered_qs(qs: &'a OrderedQs) -> Option<Self> {
Some(PresignedQs {
algorithm: qs.get_unique("X-Amz-Algorithm")?,
credential: qs.get_unique("X-Amz-Credential")?,
date: qs.get_unique("X-Amz-Date")?,
expires: qs.get_unique("X-Amz-Expires")?,
signed_headers: qs.get_unique("X-Amz-SignedHeaders")?,
signature: qs.get_unique("X-Amz-Signature")?,
})
}
}
impl<'a> PresignedUrlV4<'a> {
pub fn parse(qs: &'a OrderedQs, max_expires_secs: u32) -> Result<Self, ParsePresignedUrlError> {
Self::parse_impl(qs, max_expires_secs)
}
fn parse_impl(qs: &'a OrderedQs, max_expires_secs: u32) -> Result<Self, ParsePresignedUrlError> {
let err = || ParsePresignedUrlError { _priv: () };
let info = PresignedQs::from_ordered_qs(qs).ok_or_else(err)?;
let algorithm = info.algorithm;
let credential = CredentialV4::parse(info.credential).map_err(|_e| err())?;
let amz_date = AmzDate::parse(info.date).map_err(|_e| err())?;
let expires = parse_expires(info.expires, max_expires_secs).ok_or_else(err)?;
if !info.signed_headers.is_ascii() {
return Err(err());
}
let signed_headers = info.signed_headers.split(';').collect();
if !is_sha256_checksum(info.signature) {
return Err(err());
}
let signature = info.signature;
Ok(Self {
algorithm,
credential,
amz_date,
expires,
signed_headers,
signature,
})
}
}
fn parse_expires(s: &str, max_expires_secs: u32) -> Option<time::Duration> {
let x = s.parse::<u32>().ok()?;
if x > max_expires_secs {
return None;
}
Some(time::Duration::new(i64::from(x), 0))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::OrderedQs;
fn default_max_expires_secs() -> u32 {
crate::config::DEFAULT_PRESIGNED_URL_MAX_EXPIRES_SECS
}
fn make_qs(pairs: &[(&str, &str)]) -> OrderedQs {
OrderedQs::from_vec_unchecked(
pairs
.iter()
.map(|&(name, value)| (name.to_owned(), value.to_owned()))
.collect(),
)
}
fn valid_query_strings() -> [(&'static str, &'static str); 6] {
[
("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
("X-Amz-Credential", "AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request"),
("X-Amz-Date", "20130524T000000Z"),
("X-Amz-Expires", "86400"),
("X-Amz-SignedHeaders", "host"),
("X-Amz-Signature", "aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404"),
]
}
#[test]
fn parse_extracts_presigned_url_fields() {
let qs = make_qs(&valid_query_strings());
let info = PresignedUrlV4::parse(&qs, default_max_expires_secs()).unwrap();
assert_eq!(info.algorithm, "AWS4-HMAC-SHA256");
assert_eq!(info.credential.access_key_id, "AKIAIOSFODNN7EXAMPLE");
assert_eq!(info.credential.aws_region, "us-east-1");
assert_eq!(info.credential.aws_service, "s3");
assert_eq!(info.expires.whole_seconds(), 86_400);
assert_eq!(info.signed_headers.as_slice(), ["host"]);
assert_eq!(info.signature, "aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404");
assert!(info.amz_date.to_time().is_some());
}
#[test]
fn parse_rejects_missing_query_fields() {
let qs = make_qs(&valid_query_strings()[..5]);
assert!(PresignedUrlV4::parse(&qs, default_max_expires_secs()).is_err());
}
#[test]
fn parse_rejects_non_ascii_signed_headers() {
let mut pairs = valid_query_strings();
pairs[4] = ("X-Amz-SignedHeaders", "höst");
let qs = make_qs(&pairs);
assert!(PresignedUrlV4::parse(&qs, default_max_expires_secs()).is_err());
}
#[test]
fn parse_rejects_invalid_credential() {
let mut pairs = valid_query_strings();
pairs[1] = ("X-Amz-Credential", "bad-credential");
let qs = make_qs(&pairs);
assert!(PresignedUrlV4::parse(&qs, default_max_expires_secs()).is_err());
}
#[test]
fn parse_rejects_invalid_date() {
let mut pairs = valid_query_strings();
pairs[2] = ("X-Amz-Date", "not-a-date");
let qs = make_qs(&pairs);
assert!(PresignedUrlV4::parse(&qs, default_max_expires_secs()).is_err());
}
#[test]
fn parse_rejects_invalid_signature() {
let mut pairs = valid_query_strings();
pairs[5] = ("X-Amz-Signature", "not-a-sha256");
let qs = make_qs(&pairs);
assert!(PresignedUrlV4::parse(&qs, default_max_expires_secs()).is_err());
}
#[test]
fn parse_rejects_invalid_expires() {
for expires in ["604801", "4294967295", "4294967296", "999999999999999999999999", "NaN", "-1"] {
let mut pairs = valid_query_strings();
pairs[3] = ("X-Amz-Expires", expires);
let qs = make_qs(&pairs);
assert!(
PresignedUrlV4::parse(&qs, default_max_expires_secs()).is_err(),
"X-Amz-Expires={expires} must be rejected"
);
}
}
#[test]
fn parse_accepts_expires_boundaries() {
for expires in ["0", "1", "604800"] {
let mut pairs = valid_query_strings();
pairs[3] = ("X-Amz-Expires", expires);
let qs = make_qs(&pairs);
let parsed = PresignedUrlV4::parse(&qs, default_max_expires_secs()).expect("boundary expiration should parse");
assert_eq!(parsed.expires.whole_seconds().to_string(), expires);
}
}
#[test]
fn parse_respects_custom_max_expires() {
let mut pairs = valid_query_strings();
pairs[3] = ("X-Amz-Expires", "604801");
let qs = make_qs(&pairs);
let parsed = PresignedUrlV4::parse(&qs, 700_000).expect("custom max should allow larger expires");
assert_eq!(parsed.expires.whole_seconds(), 604_801);
assert!(PresignedUrlV4::parse(&qs, 3_600).is_err(), "custom max should reject larger expires");
}
#[test]
fn parse_rejects_duplicate_expires() {
let mut pairs = valid_query_strings().to_vec();
pairs.push(("X-Amz-Expires", "1"));
let qs = make_qs(&pairs);
assert!(PresignedUrlV4::parse(&qs, default_max_expires_secs()).is_err());
}
}