use serde::{Deserialize, Serialize};
#[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: (),
}
impl<'a> CredentialV4<'a> {
pub fn parse(input: &'a str) -> Result<Self, ParseCredentialError> {
match parser::parse_credential(input) {
Ok(("", ans)) => Ok(ans),
Ok(_) | Err(_) => Err(ParseCredentialError { _priv: () }),
}
}
}
impl<'a> AuthorizationV4<'a> {
pub fn parse(header: &'a str) -> Result<Self, ParseAuthorizationError> {
match parser::parse_authorization(header) {
Ok(("", ans)) => Ok(ans),
Ok(_) | Err(_) => Err(ParseAuthorizationError { _priv: () }),
}
}
}
mod parser {
use super::*;
use crate::utils::parser::{Error, consume, digit2, digit4};
use nom::IResult;
use nom::Parser;
use nom::bytes::complete::{tag, take, take_till, take_till1};
use nom::character::complete::{multispace0, multispace1};
use nom::combinator::verify;
use nom::multi::separated_list1;
use nom::sequence::{delimited, preceded, terminated};
pub fn parse_authorization(mut input: &str) -> IResult<&str, AuthorizationV4<'_>> {
let s = &mut input;
let algorithm = consume(s, till_space1)?;
consume(s, multispace1)?;
let credential = consume(s, |i| delimited(tag("Credential="), parse_credential, tag(",")).parse(i))?;
consume(s, multispace0)?;
let parse_headers = separated_list1(tag(";"), take_till(|c| c == ';' || c == ','));
let signed_headers = consume(s, |i| delimited(tag("SignedHeaders="), parse_headers, tag(",")).parse(i))?;
consume(s, multispace0)?;
let signature = consume(s, |i| preceded(tag("Signature="), till_space0).parse(i))?;
consume(s, multispace0)?;
let ans = AuthorizationV4 {
algorithm,
credential,
signed_headers,
signature,
};
Ok((input, ans))
}
fn till_space0(input: &str) -> IResult<&str, &str> {
take_till(|c: char| c.is_ascii_whitespace())(input)
}
fn till_space1(input: &str) -> IResult<&str, &str> {
take_till1(|c: char| c.is_ascii_whitespace())(input)
}
pub fn parse_credential(mut input: &str) -> IResult<&str, CredentialV4<'_>> {
let s = &mut input;
let access_key_id = consume(s, until_slash0)?;
let date = consume(s, |i| verify(until_slash1, |s| verify_date(s).is_ok()).parse(i))?;
let aws_region = consume(s, until_slash0)?;
let aws_service = consume(s, until_slash1)?;
consume(s, tag("aws4_request"))?;
let ans = CredentialV4 {
access_key_id,
date,
aws_region,
aws_service,
};
Ok((input, ans))
}
fn until_slash0(input: &str) -> IResult<&str, &str> {
terminated(take_till(|c| c == '/'), take(1_usize)).parse(input)
}
fn until_slash1(input: &str) -> IResult<&str, &str> {
terminated(take_till1(|c| c == '/'), take(1_usize)).parse(input)
}
fn verify_date(s: &str) -> Result<(), Error> {
let x = s.as_bytes();
if x.len() != 8 {
return Err(Error);
}
let yyyy = digit4([x[0], x[1], x[2], x[3]])?.into();
let mm = digit2([x[4], x[5]])?.into();
let dd = digit2([x[6], x[7]])?.into();
match chrono::NaiveDate::from_ymd_opt(yyyy, mm, dd) {
Some(_) => Ok(()),
None => Err(Error),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_header() {
{
let auth = "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 = "AWS4-HMAC-SHA256
Credential=AKIAIOSFODNN7EXAMPLE/20200931/us-east-1/s3/aws4_request,
SignedHeaders=host;range;x-amz-date,
Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024
";
assert!(AuthorizationV4::parse(auth).is_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");
}
}