use crate::{aws_canonical, aws_format, aws_math};
use hmac::Mac;
use std::borrow::Cow;
use std::fmt;
use url::Url;
pub const EMPTY_PAYLOAD_SHA: &str =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
#[derive(Debug)]
pub enum S3HeadersError {
InvalidUrl(url::ParseError),
MissingHost,
}
impl fmt::Display for S3HeadersError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
S3HeadersError::InvalidUrl(e) => write!(f, "invalid URL: {e}"),
S3HeadersError::MissingHost => write!(f, "URL must have a host"),
}
}
}
impl std::error::Error for S3HeadersError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
S3HeadersError::InvalidUrl(e) => Some(e),
S3HeadersError::MissingHost => None,
}
}
}
#[derive(Debug)]
pub struct ValidS3Url(Url);
impl ValidS3Url {
pub fn parse(input: &str) -> Result<Self, S3HeadersError> {
let url = Url::parse(input).map_err(S3HeadersError::InvalidUrl)?;
if url.host_str().is_none() {
return Err(S3HeadersError::MissingHost);
}
Ok(Self(url))
}
pub fn as_url(&self) -> &Url {
&self.0
}
}
#[derive(Debug, Default, Clone, Copy)]
pub enum S3DateTime {
#[default]
Now,
UnixTimestamp(i64),
Resolved(time::OffsetDateTime),
}
impl S3DateTime {
pub fn get_offset_datetime(&self) -> time::OffsetDateTime {
match self {
S3DateTime::Now => time::OffsetDateTime::now_utc(),
S3DateTime::UnixTimestamp(timestamp) => {
time::OffsetDateTime::from_unix_timestamp(*timestamp).expect("Always valid")
}
S3DateTime::Resolved(dt) => *dt,
}
}
}
#[derive(Debug, Clone)]
pub struct S3HeadersBuilder<'a> {
pub(crate) datetime: S3DateTime,
pub(crate) access_key: &'a str,
pub(crate) secret_key: &'a str,
pub(crate) region: &'a str,
pub(crate) service: &'a str,
pub(crate) url: &'a ValidS3Url,
pub(crate) method: &'a str,
pub(crate) headers: &'a [(&'static str, std::string::String)],
pub(crate) payload_hash: Cow<'a, str>,
}
impl<'a> S3HeadersBuilder<'a> {
pub fn new(url: &'a ValidS3Url) -> Self {
Self {
datetime: Default::default(),
access_key: Default::default(),
secret_key: Default::default(),
region: Default::default(),
service: "s3",
url,
method: Default::default(),
headers: Default::default(),
payload_hash: Cow::Borrowed(EMPTY_PAYLOAD_SHA),
}
}
pub fn set_access_key(mut self, value: &'a str) -> Self {
self.access_key = value;
self
}
pub fn set_secret_key(mut self, value: &'a str) -> Self {
self.secret_key = value;
self
}
pub fn set_region(mut self, value: &'a str) -> Self {
self.region = value;
self
}
pub fn set_datetime(mut self, value: S3DateTime) -> Self {
self.datetime = value;
self
}
pub fn set_payload_hash(mut self, value: &'a str) -> Self {
self.payload_hash = Cow::Borrowed(value);
self
}
pub fn set_payload_hash_with_content(mut self, content: &[u8]) -> Self {
let sha = aws_math::get_sha256(content);
self.payload_hash = Cow::Owned(sha);
self
}
pub fn set_method(mut self, value: &'a str) -> Self {
self.method = value;
self
}
pub fn set_service(mut self, value: &'a str) -> Self {
self.service = value;
self
}
pub fn set_url(mut self, url: &'a ValidS3Url) -> Self {
self.url = url;
self
}
pub fn set_headers(mut self, headers: &'a [(&'static str, std::string::String)]) -> Self {
self.headers = headers;
self
}
pub fn build(self) -> Vec<(&'static str, String)> {
get_headers(self)
}
}
pub fn get_headers(options: S3HeadersBuilder) -> Vec<(&'static str, String)> {
let url = options.url;
let payload_hash = &options.payload_hash;
let datetime = options.datetime.get_offset_datetime();
let amz_date = aws_format::to_long_datetime(&datetime);
let mut headers: Vec<(&'static str, String)> = [
options.headers,
&[
("Host", url.0.host_str().unwrap().to_owned()),
("x-amz-content-sha256", payload_hash.to_string()),
("x-amz-date", amz_date),
],
]
.concat();
let auth_header = get_authorization_header(
options
.set_datetime(S3DateTime::Resolved(datetime))
.set_headers(&headers),
);
headers.push(("Authorization", auth_header));
headers
}
pub fn get_range_headers(start: u64, end: Option<u64>) -> Vec<(&'static str, String)> {
let mut range = format!("bytes={}-", start);
if let Some(end) = end {
range.push_str(&end.to_string());
}
let headers: Vec<(&'static str, String)> = vec![
("Accept", "application/octet-stream".to_string()),
("Range", range),
];
headers
}
pub fn get_authorization_header(options: S3HeadersBuilder) -> String {
let datetime = options.datetime.get_offset_datetime();
let region = options.region;
let access_key = options.access_key;
let secret_key = options.secret_key;
let service = options.service;
let url = options.url;
let method = options.method;
let payload_hash = options.payload_hash;
let canonical_headers = aws_canonical::to_canonical_headers(options.headers);
let canonical_request =
aws_format::canonical_request_string(method, &url.0, &canonical_headers, &payload_hash);
let string_to_sign = aws_format::string_to_sign(&datetime, region, service, &canonical_request);
let signing_key = aws_math::get_signature_key(&datetime, secret_key, region, service);
let hmac: aws_math::HmacSha256 = aws_math::sign(&signing_key, string_to_sign.as_bytes());
let signature = hex::encode(hmac.finalize().into_bytes());
let signed_headers = aws_format::get_keys(&canonical_headers).join(";");
aws_format::authorization_header_string(
access_key,
&datetime,
region,
service,
&signed_headers,
&signature,
)
}
pub fn presign_get(
url: &ValidS3Url,
access_key: &str,
secret_key: &str,
region: &str,
service: &str,
datetime: S3DateTime,
expires: u32,
) -> String {
let datetime = datetime.get_offset_datetime();
let signing_key = aws_math::get_signature_key(&datetime, secret_key, region, service);
presign_get_with_key(
url,
access_key,
region,
service,
datetime,
expires,
&signing_key,
)
}
pub fn presign_get_with_key(
url: &ValidS3Url,
access_key: &str,
region: &str,
service: &str,
datetime: time::OffsetDateTime,
expires: u32,
signing_key: &[u8],
) -> String {
let host = host_with_port(url);
let canonical_uri = aws_format::canonical_uri_string(&url.0);
let presign_query = aws_math::authorization_query_params_no_sig(
access_key, &datetime, region, service, expires, None, None,
);
let canonical_query = presign_query.strip_prefix('?').unwrap_or(&presign_query);
let canonical_request = format!(
"GET\n{canonical_uri}\n{canonical_query}\nhost:{host}\n\nhost\n{UNSIGNED_PAYLOAD}",
);
let string_to_sign = aws_format::string_to_sign(&datetime, region, service, &canonical_request);
let hmac = aws_math::sign(signing_key, string_to_sign.as_bytes());
let signature = hex::encode(hmac.finalize().into_bytes());
format!(
"{}://{host}{canonical_uri}?{canonical_query}&X-Amz-Signature={signature}",
url.0.scheme(),
)
}
fn host_with_port(url: &ValidS3Url) -> String {
let host = url.0.host_str().expect("url must have a host");
match url.0.port() {
Some(port) => format!("{host}:{port}"),
None => host.to_string(),
}
}