use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
#[derive(Debug, Serialize, Deserialize)]
pub struct AuthorizationV4<'a> {
pub algorithm: &'a str,
pub credential: CredentialV4<'a>,
pub signed_headers: Vec<&'a str>,
pub signature: &'a str,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CredentialV4<'a> {
pub access_key_id: &'a str,
pub date: &'a str,
pub aws_region: &'a str,
pub aws_service: &'a str,
}
#[derive(Debug, thiserror::Error)]
#[error("ParseAuthorizationError")]
pub struct ParseAuthorizationError {
_priv: (),
}
#[derive(Debug, thiserror::Error)]
#[error("ParseAuthorizationError")]
pub struct ParseCredentialError {
_priv: (),
}
macro_rules! parse_and_bind {
(mut $input:expr => $f:expr => $id:pat ) => {
let $id = {
let (__input, output) = $f($input)?;
$input = __input;
output
};
};
($input:expr => $f:expr => $id:pat ) => {
let $id = {
let (_, output) = $f($input)?;
output
};
};
}
impl<'a> CredentialV4<'a> {
fn nom_parse(mut input: &'a str) -> nom::IResult<&'a str, Self> {
use nom::{
bytes::complete::{tag, take, take_till, take_till1},
sequence::terminated,
};
let mut slash_tail1 = terminated(take_till1(|c| c == '/'), take(1_usize));
let mut slash_tail0 = terminated(take_till(|c| c == '/'), take(1_usize));
parse_and_bind!(mut input => slash_tail0 => access_key_id);
parse_and_bind!(mut input => slash_tail1 => date);
parse_and_bind!(date => CredentialV4::verify_date => _);
parse_and_bind!(mut input => slash_tail0 => aws_region);
parse_and_bind!(mut input => slash_tail1 => aws_service);
parse_and_bind!(mut input => tag("aws4_request") => _);
let c = CredentialV4 {
access_key_id,
date,
aws_region,
aws_service,
};
Ok((input, c))
}
pub fn parse(input: &'a str) -> Result<Self, ParseCredentialError> {
match Self::nom_parse(input) {
Ok(("", ans)) => Ok(ans),
Ok(_) | Err(_) => Err(ParseCredentialError { _priv: () }),
}
}
fn verify_date(input: &str) -> nom::IResult<&str, (&str, &str, &str)> {
use chrono::NaiveDate;
use nom::{
bytes::complete::take,
combinator::{all_consuming, verify},
sequence::tuple,
};
verify(
all_consuming(tuple((take(4_usize), take(2_usize), take(2_usize)))),
|&(y, m, d): &(&str, &str, &str)| {
macro_rules! parse_num {
($x:expr) => {{
match $x.parse() {
Ok(x) => x,
Err(_) => return false,
}
}};
}
NaiveDate::from_ymd_opt(parse_num!(y), parse_num!(m), parse_num!(d)).is_some()
},
)(input)
}
}
impl<'a> AuthorizationV4<'a> {
pub fn parse(auth: &'a str) -> Result<Self, ParseAuthorizationError> {
fn nom_parse(mut input: &str) -> nom::IResult<&str, AuthorizationV4<'_>> {
use nom::{
bytes::complete::{tag, take, take_till, take_till1},
character::complete::{multispace0, multispace1},
combinator::all_consuming,
sequence::tuple,
};
let space_till1 = take_till1(|c: char| c.is_ascii_whitespace());
let space_till0 = take_till(|c: char| c.is_ascii_whitespace());
parse_and_bind!(mut input => space_till1 => algorithm);
parse_and_bind!(mut input => multispace1 => _);
parse_and_bind!(mut input => tag("Credential=") => _);
parse_and_bind!(mut input => CredentialV4::nom_parse => credential);
parse_and_bind!(mut input => tag(",") => _);
parse_and_bind!(mut input => multispace0 => _);
parse_and_bind!(mut input => tag("SignedHeaders=") => _);
let mut headers: SmallVec<[&str; 16]> = SmallVec::new();
loop {
let mut expect_header = tuple((take_till1(|c| c == ';' || c == ','), take(1_usize)));
parse_and_bind!(mut input => expect_header => (header, sep));
headers.push(header);
if sep == "," {
break;
}
}
parse_and_bind!(mut input => multispace0 => _);
parse_and_bind!(mut input => tag("Signature=") => _);
parse_and_bind!(mut input => space_till0 => signature);
parse_and_bind!(mut input => all_consuming(multispace0) => _);
let ans = AuthorizationV4 {
algorithm,
credential,
signed_headers: headers.into_vec(),
signature,
};
Ok((input, ans))
}
match nom_parse(auth) {
Ok(("", ans)) => Ok(ans),
Ok(_) | Err(_) => Err(ParseAuthorizationError { _priv: () }),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_header() {
{
let auth = r#"AWS4-HMAC-SHA256
Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,
SignedHeaders=host;range;x-amz-date,
Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024
"#;
let ans = AuthorizationV4::parse(auth).unwrap();
assert_eq!(ans.algorithm, "AWS4-HMAC-SHA256");
assert_eq!(ans.credential.access_key_id, "AKIAIOSFODNN7EXAMPLE");
assert_eq!(ans.credential.date, "20130524");
assert_eq!(ans.credential.aws_region, "us-east-1");
assert_eq!(ans.credential.aws_service, "s3");
assert_eq!(ans.signed_headers, &["host", "range", "x-amz-date"]);
assert_eq!(ans.signature, "fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024");
}
{
let auth = r#"AWS4-HMAC-SHA256
Credential=AKIAIOSFODNN7EXAMPLE/20200931/us-east-1/s3/aws4_request,
SignedHeaders=host;range;x-amz-date,
Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024
"#;
assert!(matches!(AuthorizationV4::parse(auth), Err(_)));
}
}
#[test]
fn special_20200921() {
let auth = concat!(
"AWS4-HMAC-SHA256 ",
"Credential=AKIAIOSFODNN7EXAMPLE/20200921/us-east-1/s3/aws4_request,",
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length,",
"Signature=7a7f7778618cadc05f112b44cca218e001a0a020c5c512d8aa2bca2afb713fad",
);
let ans = AuthorizationV4::parse(auth).unwrap();
assert_eq!(ans.algorithm, "AWS4-HMAC-SHA256");
assert_eq!(ans.credential.access_key_id, "AKIAIOSFODNN7EXAMPLE");
assert_eq!(ans.credential.date, "20200921");
assert_eq!(ans.credential.aws_region, "us-east-1");
assert_eq!(ans.credential.aws_service, "s3");
assert_eq!(
ans.signed_headers,
&["host", "x-amz-content-sha256", "x-amz-date", "x-amz-decoded-content-length"]
);
assert_eq!(ans.signature, "7a7f7778618cadc05f112b44cca218e001a0a020c5c512d8aa2bca2afb713fad");
}
#[test]
fn special_20230204() {
let auth = concat!(
"AWS4-HMAC-SHA256 ",
"Credential=/20230204/us-east-1/s3/aws4_request, ",
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-user-agent, ",
"Signature=d2ff90c5a29855fd7c56251aa4c02c49a1bc258a8cc9c191ba3cfc037c5dab80"
);
let ans = AuthorizationV4::parse(auth).unwrap();
assert_eq!(ans.algorithm, "AWS4-HMAC-SHA256");
assert_eq!(ans.credential.access_key_id, "");
assert_eq!(ans.credential.date, "20230204");
assert_eq!(ans.credential.aws_region, "us-east-1");
assert_eq!(ans.credential.aws_service, "s3");
assert_eq!(ans.signed_headers, &["host", "x-amz-content-sha256", "x-amz-date", "x-amz-user-agent"]);
assert_eq!(ans.signature, "d2ff90c5a29855fd7c56251aa4c02c49a1bc258a8cc9c191ba3cfc037c5dab80");
}
}