use core::mem::size_of;
use std::io::Write as _;
use bstr::BStr;
use bun_core::strings;
use bun_http_types::Method::Method;
use bun_picohttp::Header as PicoHeader;
use bun_ptr::{IntrusiveRc, RawSlice, RefCount};
use super::acl::ACL;
use super::storage_class::StorageClass;
bun_core::declare_scope!(AWS, visible);
use bun_core::fmt::buf_print;
macro_rules! alloc_print {
($($arg:tt)*) => {{
let mut v: Vec<u8> = Vec::new();
write!(&mut v, $($arg)*).expect("write to Vec<u8> never fails");
v
}};
}
use bun_core::fmt::hex_lower as HexLower;
#[inline]
fn pico_header_empty() -> PicoHeader {
PicoHeader::ZERO
}
#[inline]
fn pico_header_new(name: &[u8], value: &[u8]) -> PicoHeader {
PicoHeader::new(name, value)
}
#[derive(Clone, Copy, Debug)]
pub struct MultiPartUploadOptions {
pub queue_size: u8,
pub part_size: u64,
pub retry: u8,
}
impl MultiPartUploadOptions {
pub const ONE_MIB: usize = 1_048_576;
pub const MAX_SINGLE_UPLOAD_SIZE: usize = 5120 * Self::ONE_MIB;
pub const MIN_SINGLE_UPLOAD_SIZE: usize = 5 * Self::ONE_MIB;
pub const DEFAULT_PART_SIZE: usize = Self::MIN_SINGLE_UPLOAD_SIZE;
pub const MAX_QUEUE_SIZE: u8 = 64;
}
impl Default for MultiPartUploadOptions {
fn default() -> Self {
Self {
queue_size: 5,
part_size: Self::DEFAULT_PART_SIZE as u64,
retry: 3,
}
}
}
use bun_collections::StringArrayHashMap;
use bun_core::Mutex;
#[derive(Default)]
pub struct AWSSignatureCache(Mutex<AWSSignatureCacheInner>);
#[derive(Default)]
struct AWSSignatureCacheInner {
cache: StringArrayHashMap<[u8; DIGESTED_HMAC_256_LEN]>,
date: u64,
}
impl AWSSignatureCache {
pub fn get(&self, numeric_day: u64, key: &[u8]) -> Option<[u8; DIGESTED_HMAC_256_LEN]> {
let inner = self.0.lock();
if inner.date == 0 || inner.date != numeric_day {
return None;
}
inner.cache.get(key).copied()
}
pub fn set(&self, numeric_day: u64, key: &[u8], value: [u8; DIGESTED_HMAC_256_LEN]) {
let mut inner = self.0.lock();
if inner.date == 0 {
inner.cache = StringArrayHashMap::new();
} else if inner.date != numeric_day {
inner.cache.clear();
}
inner.date = numeric_day;
bun_core::handle_oom(inner.cache.put(key, value));
}
}
static AWS_SIGNATURE_CACHE: std::sync::LazyLock<AWSSignatureCache> =
std::sync::LazyLock::new(AWSSignatureCache::default);
#[inline]
fn aws_cache_get(day: u64, key: &[u8]) -> Option<[u8; DIGESTED_HMAC_256_LEN]> {
AWS_SIGNATURE_CACHE.get(day, key)
}
#[inline]
fn aws_cache_set(day: u64, key: &[u8], digest: [u8; DIGESTED_HMAC_256_LEN]) {
AWS_SIGNATURE_CACHE.set(day, key, digest)
}
#[inline]
fn boring_engine() -> *mut bun_sha_hmac::sha::ffi::ENGINE {
core::ptr::null_mut()
}
#[derive(bun_ptr::RefCounted)]
pub struct S3Credentials {
ref_count: RefCount<S3Credentials>,
pub access_key_id: Box<[u8]>,
pub secret_access_key: Box<[u8]>,
pub region: Box<[u8]>,
pub endpoint: Box<[u8]>,
pub bucket: Box<[u8]>,
pub session_token: Box<[u8]>,
pub storage_class: Option<StorageClass>,
pub insecure_http: bool,
pub virtual_hosted_style: bool,
}
impl Clone for S3Credentials {
fn clone(&self) -> Self {
Self {
ref_count: RefCount::init(),
access_key_id: dupe_slice(&self.access_key_id),
secret_access_key: dupe_slice(&self.secret_access_key),
region: dupe_slice(&self.region),
endpoint: dupe_slice(&self.endpoint),
bucket: dupe_slice(&self.bucket),
session_token: dupe_slice(&self.session_token),
storage_class: self.storage_class,
insecure_http: self.insecure_http,
virtual_hosted_style: self.virtual_hosted_style,
}
}
}
impl Default for S3Credentials {
fn default() -> Self {
Self {
ref_count: RefCount::init(),
access_key_id: Box::default(),
secret_access_key: Box::default(),
region: Box::default(),
endpoint: Box::default(),
bucket: Box::default(),
session_token: Box::default(),
storage_class: None,
insecure_http: false,
virtual_hosted_style: false,
}
}
}
impl S3Credentials {
#[allow(clippy::too_many_arguments)]
pub fn new_value(
access_key_id: Box<[u8]>,
secret_access_key: Box<[u8]>,
region: Box<[u8]>,
endpoint: Box<[u8]>,
bucket: Box<[u8]>,
session_token: Box<[u8]>,
insecure_http: bool,
) -> Self {
Self {
ref_count: RefCount::init(),
access_key_id,
secret_access_key,
region,
endpoint,
bucket,
session_token,
storage_class: None,
insecure_http,
virtual_hosted_style: false,
}
}
pub fn estimated_size(&self) -> usize {
size_of::<S3Credentials>()
+ self.access_key_id.len()
+ self.region.len()
+ self.secret_access_key.len()
+ self.endpoint.len()
+ self.bucket.len()
}
pub fn dupe(&self) -> IntrusiveRc<S3Credentials> {
IntrusiveRc::new(S3Credentials {
ref_count: RefCount::init(),
access_key_id: dupe_slice(&self.access_key_id),
secret_access_key: dupe_slice(&self.secret_access_key),
region: dupe_slice(&self.region),
endpoint: dupe_slice(&self.endpoint),
bucket: dupe_slice(&self.bucket),
session_token: dupe_slice(&self.session_token),
storage_class: None,
insecure_http: self.insecure_http,
virtual_hosted_style: self.virtual_hosted_style,
})
}
pub fn sign_request<const ALLOW_EMPTY_PATH: bool>(
&self,
sign_options: &SignOptions<'_>,
sign_query_option: Option<SignQueryOptions>,
) -> Result<SignResult, SignError> {
let method = sign_options.method;
let request_path = sign_options.path;
let content_hash = sign_options.content_hash;
let mut content_md5: Option<Box<[u8]>> = None;
if let Some(content_md5_val) = sign_options.content_md5 {
let len = bun_base64::encode_len(content_md5_val);
let mut content_md5_as_base64 = vec![0u8; len];
let n = bun_base64::encode(&mut content_md5_as_base64, content_md5_val);
content_md5_as_base64.truncate(n);
content_md5 = Some(content_md5_as_base64.into_boxed_slice());
}
let search_params = sign_options.search_params;
let mut content_disposition = sign_options.content_disposition;
if matches!(content_disposition, Some(s) if s.is_empty()) {
content_disposition = None;
}
let mut content_type = sign_options.content_type;
if matches!(content_type, Some(s) if s.is_empty()) {
content_type = None;
}
let mut content_encoding = sign_options.content_encoding;
if matches!(content_encoding, Some(s) if s.is_empty()) {
content_encoding = None;
}
let session_token: Option<&[u8]> = if self.session_token.is_empty() {
None
} else {
Some(&self.session_token)
};
let acl: Option<&'static [u8]> = sign_options.acl.map(|a| a.to_string());
let storage_class: Option<&'static [u8]> =
sign_options.storage_class.map(|s| s.to_string());
if self.access_key_id.is_empty() || self.secret_access_key.is_empty() {
return Err(SignError::MissingCredentials);
}
let sign_query = sign_query_option.is_some();
let expires = sign_query_option.map(|o| o.expires).unwrap_or(0);
let method_name: &'static str = match method {
Method::GET => "GET",
Method::POST => "POST",
Method::PUT => "PUT",
Method::DELETE => "DELETE",
Method::HEAD => "HEAD",
_ => return Err(SignError::InvalidMethod),
};
let region: &[u8] = if !self.region.is_empty() {
&self.region
} else {
guess_region(&self.endpoint)
};
let mut full_path = request_path;
if strings::starts_with(full_path, b"/") || strings::starts_with(full_path, b"\\") {
full_path = &full_path[1..];
}
let mut path: &[u8] = full_path;
let mut bucket: &[u8] = &self.bucket;
if !self.virtual_hosted_style {
if bucket.is_empty() {
if let Some(end) = strings::index_of(full_path, b"/") {
bucket = &full_path[..end];
path = &full_path[end + 1..];
} else if let Some(backslash_index) = strings::index_of(full_path, b"\\") {
bucket = &full_path[..backslash_index];
path = &full_path[backslash_index + 1..];
} else {
return Err(SignError::InvalidPath);
}
}
}
let path = normalize_name(path);
let bucket = normalize_name(bucket);
if !ALLOW_EMPTY_PATH && path.is_empty() {
return Err(SignError::InvalidPath);
}
let mut normalized_path_buffer = [0u8; 1024 + 63 + 2];
let mut path_buffer = [0u8; 1024];
let mut bucket_buffer = [0u8; 63];
let bucket = encode_uri_component::<false>(bucket, &mut bucket_buffer)
.map_err(|_| SignError::InvalidPath)?;
let path = encode_uri_component::<false>(path, &mut path_buffer)
.map_err(|_| SignError::InvalidPath)?;
let protocol: &str = if self.insecure_http { "http" } else { "https" };
let mut endpoint_owned: Option<Vec<u8>> = None;
let mut extra_path: &[u8] = b"";
let host: Box<[u8]> = 'brk_host: {
if !self.endpoint.is_empty() {
if self.endpoint.len() >= 2048 {
return Err(SignError::InvalidEndpoint);
}
let mut host: &[u8] = &self.endpoint;
if let Some(index) = strings::index_of(&self.endpoint, b"/") {
host = &self.endpoint[..index];
extra_path = &self.endpoint[index..];
}
break 'brk_host Box::<[u8]>::from(host);
} else {
if self.virtual_hosted_style {
if bucket.is_empty() {
return Err(SignError::InvalidEndpoint);
}
if bucket.contains(&b'/') {
return Err(SignError::InvalidEndpoint);
}
let mut v = Vec::new();
write!(
&mut v,
"{}.s3.{}.amazonaws.com",
BStr::new(bucket),
BStr::new(region)
)
.unwrap();
endpoint_owned = Some(v.clone());
break 'brk_host v.into_boxed_slice();
}
let mut v = Vec::new();
write!(&mut v, "s3.{}.amazonaws.com", BStr::new(region))
.expect("infallible: in-memory write");
endpoint_owned = Some(v.clone());
break 'brk_host v.into_boxed_slice();
}
};
let _ = endpoint_owned;
let normalized_path: &[u8] = 'brk: {
if self.virtual_hosted_style {
break 'brk buf_print(
&mut normalized_path_buffer,
format_args!("{}/{}", BStr::new(extra_path), BStr::new(path)),
)
.map_err(|_| SignError::InvalidPath)?;
} else {
break 'brk buf_print(
&mut normalized_path_buffer,
format_args!(
"{}/{}/{}",
BStr::new(extra_path),
BStr::new(bucket),
BStr::new(path)
),
)
.map_err(|_| SignError::InvalidPath)?;
}
};
let date_result = get_amz_date();
let amz_date: Box<[u8]> = date_result.date;
let amz_day = &amz_date[0..8];
let request_payer = sign_options.request_payer;
let header_key = SignedHeadersKey {
content_disposition: content_disposition.is_some(),
content_encoding: content_encoding.is_some(),
content_md5: content_md5.is_some(),
acl: acl.is_some(),
request_payer,
session_token: session_token.is_some(),
storage_class: storage_class.is_some(),
};
let mut signed_headers_buf = [0u8; 256];
let signed_headers: &[u8] = if sign_query {
b"host"
} else {
SignedHeaders::get(header_key, &mut signed_headers_buf)
};
let service_name: &str = "s3";
let aws_content_hash: &[u8] = content_hash.unwrap_or(b"UNSIGNED-PAYLOAD");
let mut tmp_buffer = [0u8; 4096];
let authorization: Box<[u8]> = 'brk: {
let mut hmac_sig_service = [0u8; bun_sha_hmac::hmac::EVP_MAX_MD_SIZE];
let mut hmac_sig_service2 = [0u8; bun_sha_hmac::hmac::EVP_MAX_MD_SIZE];
let sig_date_region_service_req: [u8; DIGESTED_HMAC_256_LEN] = 'brk_sign: {
let key = buf_print(
&mut tmp_buffer,
format_args!(
"{}{}{}",
BStr::new(region),
service_name,
BStr::new(&self.secret_access_key)
),
)
.map_err(|_| SignError::NoSpaceLeft)?;
if let Some(cached) = aws_cache_get(date_result.numeric_day, key) {
break 'brk_sign cached;
}
let aws4_key = buf_print(
&mut tmp_buffer,
format_args!("AWS4{}", BStr::new(&self.secret_access_key)),
)
.map_err(|_| SignError::NoSpaceLeft)?;
let sig_date = bun_sha_hmac::generate(
aws4_key,
amz_day,
bun_sha_hmac::Algorithm::Sha256,
&mut hmac_sig_service,
)
.ok_or(SignError::FailedToGenerateSignature)?;
let sig_date_region = bun_sha_hmac::generate(
sig_date,
region,
bun_sha_hmac::Algorithm::Sha256,
&mut hmac_sig_service2,
)
.ok_or(SignError::FailedToGenerateSignature)?;
let sig_date_region_service = bun_sha_hmac::generate(
sig_date_region,
service_name.as_bytes(),
bun_sha_hmac::Algorithm::Sha256,
&mut hmac_sig_service,
)
.ok_or(SignError::FailedToGenerateSignature)?;
let _result = bun_sha_hmac::generate(
sig_date_region_service,
b"aws4_request",
bun_sha_hmac::Algorithm::Sha256,
&mut hmac_sig_service2,
)
.ok_or(SignError::FailedToGenerateSignature)?;
let digest: [u8; DIGESTED_HMAC_256_LEN] = hmac_sig_service2
[0..DIGESTED_HMAC_256_LEN]
.try_into()
.expect("infallible: size matches");
let key = buf_print(
&mut tmp_buffer,
format_args!(
"{}{}{}",
BStr::new(region),
service_name,
BStr::new(&self.secret_access_key)
),
)
.map_err(|_| SignError::NoSpaceLeft)?;
aws_cache_set(date_result.numeric_day, key, digest);
break 'brk_sign digest;
};
if sign_query {
let mut token_encoded_buffer = [0u8; 2048]; let mut encoded_session_token: Option<&[u8]> = None;
if let Some(token) = session_token {
encoded_session_token = Some(
encode_uri_component::<true>(token, &mut token_encoded_buffer)
.map_err(|_| SignError::InvalidSessionToken)?,
);
}
let mut content_md5_encoded_buffer = [0u8; 128];
let mut encoded_content_md5: Option<&[u8]> = None;
if let Some(content_md5_value) = content_md5.as_deref() {
encoded_content_md5 = Some(
encode_uri_component::<true>(
content_md5_value,
&mut content_md5_encoded_buffer,
)
.map_err(|_| SignError::FailedToGenerateSignature)?,
);
}
let mut content_disposition_encoded_buffer = [0u8; 512];
let mut encoded_content_disposition: Option<&[u8]> = None;
if let Some(cd) = content_disposition {
encoded_content_disposition = Some(
encode_uri_component::<true>(cd, &mut content_disposition_encoded_buffer)
.map_err(|_| SignError::FailedToGenerateSignature)?,
);
}
let mut content_type_encoded_buffer = [0u8; 256];
let mut encoded_content_type: Option<&[u8]> = None;
if let Some(ct) = content_type {
encoded_content_type = Some(
encode_uri_component::<true>(ct, &mut content_type_encoded_buffer)
.map_err(|_| SignError::FailedToGenerateSignature)?,
);
}
let canonical: &[u8] = 'brk_canonical: {
let mut query_parts: Vec<Vec<u8>> = Vec::with_capacity(13);
if let Some(v) = encoded_content_md5 {
query_parts.push(alloc_print!("Content-MD5={}", BStr::new(v)));
}
if let Some(v) = acl {
query_parts.push(alloc_print!("X-Amz-Acl={}", BStr::new(v)));
}
query_parts.push(alloc_print!("X-Amz-Algorithm=AWS4-HMAC-SHA256"));
query_parts.push(alloc_print!(
"X-Amz-Credential={}%2F{}%2F{}%2F{}%2Faws4_request",
BStr::new(&self.access_key_id),
BStr::new(amz_day),
BStr::new(region),
service_name
));
query_parts.push(alloc_print!("X-Amz-Date={}", BStr::new(&amz_date)));
query_parts.push(alloc_print!("X-Amz-Expires={}", expires));
if let Some(token) = encoded_session_token {
query_parts.push(alloc_print!("X-Amz-Security-Token={}", BStr::new(token)));
}
query_parts.push(alloc_print!("X-Amz-SignedHeaders=host"));
if let Some(cd) = encoded_content_disposition {
query_parts.push(alloc_print!(
"response-content-disposition={}",
BStr::new(cd)
));
}
if let Some(ct) = encoded_content_type {
query_parts.push(alloc_print!("response-content-type={}", BStr::new(ct)));
}
if request_payer {
query_parts.push(alloc_print!("x-amz-request-payer=requester"));
}
if let Some(v) = storage_class {
query_parts.push(alloc_print!("x-amz-storage-class={}", BStr::new(v)));
}
let mut query_string: Vec<u8> = Vec::new();
for (i, part) in query_parts.iter().enumerate() {
if i > 0 {
query_string.push(b'&');
}
query_string.extend_from_slice(part);
}
break 'brk_canonical buf_print(
&mut tmp_buffer,
format_args!(
"{}\n{}\n{}\nhost:{}\n\nhost\n{}",
method_name,
BStr::new(normalized_path),
BStr::new(&query_string),
BStr::new(&host),
BStr::new(aws_content_hash)
),
)
.map_err(|_| SignError::NoSpaceLeft)?;
};
let mut sha_digest = [0u8; bun_sha_hmac::SHA256::DIGEST];
unsafe { bun_sha_hmac::SHA256::hash(canonical, &mut sha_digest, boring_engine()) };
let sign_value = buf_print(
&mut tmp_buffer,
format_args!(
"AWS4-HMAC-SHA256\n{}\n{}/{}/{}/aws4_request\n{}",
BStr::new(&amz_date),
BStr::new(amz_day),
BStr::new(region),
service_name,
HexLower(&sha_digest)
),
)
.map_err(|_| SignError::NoSpaceLeft)?;
let signature = bun_sha_hmac::generate(
&sig_date_region_service_req,
sign_value,
bun_sha_hmac::Algorithm::Sha256,
&mut hmac_sig_service,
)
.ok_or(SignError::FailedToGenerateSignature)?;
let mut url_query_parts: Vec<Vec<u8>> = Vec::with_capacity(14);
if let Some(v) = encoded_content_md5 {
url_query_parts.push(alloc_print!("Content-MD5={}", BStr::new(v)));
}
if let Some(v) = acl {
url_query_parts.push(alloc_print!("X-Amz-Acl={}", BStr::new(v)));
}
url_query_parts.push(alloc_print!("X-Amz-Algorithm=AWS4-HMAC-SHA256"));
url_query_parts.push(alloc_print!(
"X-Amz-Credential={}%2F{}%2F{}%2F{}%2Faws4_request",
BStr::new(&self.access_key_id),
BStr::new(amz_day),
BStr::new(region),
service_name
));
url_query_parts.push(alloc_print!("X-Amz-Date={}", BStr::new(&amz_date)));
url_query_parts.push(alloc_print!("X-Amz-Expires={}", expires));
if let Some(token) = encoded_session_token {
url_query_parts.push(alloc_print!("X-Amz-Security-Token={}", BStr::new(token)));
}
url_query_parts.push(alloc_print!(
"X-Amz-Signature={}",
HexLower(&signature[0..DIGESTED_HMAC_256_LEN])
));
url_query_parts.push(alloc_print!("X-Amz-SignedHeaders=host"));
if let Some(cd) = encoded_content_disposition {
url_query_parts.push(alloc_print!(
"response-content-disposition={}",
BStr::new(cd)
));
}
if let Some(ct) = encoded_content_type {
url_query_parts.push(alloc_print!("response-content-type={}", BStr::new(ct)));
}
if request_payer {
url_query_parts.push(alloc_print!("x-amz-request-payer=requester"));
}
if let Some(v) = storage_class {
url_query_parts.push(alloc_print!("x-amz-storage-class={}", BStr::new(v)));
}
let mut url_query_string: Vec<u8> = Vec::new();
for (i, part) in url_query_parts.iter().enumerate() {
if i > 0 {
url_query_string.push(b'&');
}
url_query_string.extend_from_slice(part);
}
break 'brk alloc_print!(
"{}://{}{}?{}",
protocol,
BStr::new(&host),
BStr::new(normalized_path),
BStr::new(&url_query_string)
)
.into_boxed_slice();
} else {
let canonical = CanonicalRequest::format(
&mut tmp_buffer,
header_key,
method_name.as_bytes(),
normalized_path,
search_params.map(|p| &p[1..]).unwrap_or(b""),
content_disposition,
content_encoding,
content_md5.as_deref(),
&host,
acl,
aws_content_hash,
&amz_date,
session_token,
storage_class,
signed_headers,
)
.map_err(|_| SignError::NoSpaceLeft)?;
let mut sha_digest = [0u8; bun_sha_hmac::SHA256::DIGEST];
unsafe { bun_sha_hmac::SHA256::hash(canonical, &mut sha_digest, boring_engine()) };
let sign_value = buf_print(
&mut tmp_buffer,
format_args!(
"AWS4-HMAC-SHA256\n{}\n{}/{}/{}/aws4_request\n{}",
BStr::new(&amz_date),
BStr::new(amz_day),
BStr::new(region),
service_name,
HexLower(&sha_digest)
),
)
.map_err(|_| SignError::NoSpaceLeft)?;
let signature = bun_sha_hmac::generate(
&sig_date_region_service_req,
sign_value,
bun_sha_hmac::Algorithm::Sha256,
&mut hmac_sig_service,
)
.ok_or(SignError::FailedToGenerateSignature)?;
break 'brk alloc_print!(
"AWS4-HMAC-SHA256 Credential={}/{}/{}/{}/aws4_request, SignedHeaders={}, Signature={}",
BStr::new(&self.access_key_id),
BStr::new(amz_day),
BStr::new(region),
service_name,
BStr::new(signed_headers),
HexLower(&signature[0..DIGESTED_HMAC_256_LEN])
)
.into_boxed_slice();
}
};
if sign_query {
let mut r = SignResult::default();
r.acl = sign_options.acl;
r.url = authorization;
r.storage_class = sign_options.storage_class;
return Ok(r);
}
if contains_newline_or_cr(aws_content_hash)
|| search_params.is_some_and(contains_newline_or_cr)
|| acl.is_some_and(contains_newline_or_cr)
|| storage_class.is_some_and(contains_newline_or_cr)
|| content_md5.as_deref().is_some_and(contains_newline_or_cr)
|| content_disposition.is_some_and(contains_newline_or_cr)
|| content_encoding.is_some_and(contains_newline_or_cr)
|| session_token.is_some_and(contains_newline_or_cr)
|| contains_newline_or_cr(region)
|| contains_newline_or_cr(&self.access_key_id)
|| contains_newline_or_cr(&host)
{
return Err(SignError::InvalidHeaderValue);
}
let url = alloc_print!(
"{}://{}{}{}",
protocol,
BStr::new(&host),
BStr::new(normalized_path),
BStr::new(search_params.unwrap_or(b""))
)
.into_boxed_slice();
let mut result = SignResult::default();
result.amz_date = amz_date;
result.host = host;
result.authorization = authorization;
result.acl = sign_options.acl;
result.storage_class = sign_options.storage_class;
result.request_payer = request_payer;
result.url = url;
result._headers_len = 4;
result._headers[0] = pico_header_new(b"x-amz-content-sha256", aws_content_hash);
result._headers[1] = pico_header_new(b"x-amz-date", &result.amz_date);
result._headers[2] = pico_header_new(b"Host", &result.host);
result._headers[3] = pico_header_new(b"Authorization", &result.authorization);
if let Some(acl_value) = acl {
result._headers[result._headers_len as usize] =
pico_header_new(b"x-amz-acl", acl_value);
result._headers_len += 1;
}
if let Some(token) = session_token {
let session_token_value = Box::<[u8]>::from(token);
result._headers[result._headers_len as usize] =
pico_header_new(b"x-amz-security-token", &session_token_value);
result.session_token = session_token_value;
result._headers_len += 1;
}
if let Some(storage_class_value) = storage_class {
result._headers[result._headers_len as usize] =
pico_header_new(b"x-amz-storage-class", storage_class_value);
result._headers_len += 1;
}
if let Some(cd) = content_disposition {
let content_disposition_value = Box::<[u8]>::from(cd);
result._headers[result._headers_len as usize] =
pico_header_new(b"content-disposition", &content_disposition_value);
result.content_disposition = content_disposition_value;
result._headers_len += 1;
}
if let Some(ce) = content_encoding {
let content_encoding_value = Box::<[u8]>::from(ce);
result._headers[result._headers_len as usize] =
pico_header_new(b"content-encoding", &content_encoding_value);
result.content_encoding = content_encoding_value;
result._headers_len += 1;
}
if let Some(c_md5) = content_md5.as_deref() {
let content_md5_value = Box::<[u8]>::from(c_md5);
result._headers[result._headers_len as usize] =
pico_header_new(b"content-md5", &content_md5_value);
result.content_md5 = content_md5_value;
result._headers_len += 1;
}
if request_payer {
result._headers[result._headers_len as usize] =
pico_header_new(b"x-amz-request-payer", b"requester");
result._headers_len += 1;
}
Ok(result)
}
}
use bun_ptr::owned::alloc_dupe_slice as dupe_slice;
struct DateResult {
numeric_day: u64,
date: Box<[u8]>,
}
fn get_amz_date() -> DateResult {
let secs: u64 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let (year, month, day, hours, minutes, seconds, day_seconds) = epoch_to_utc_components(secs);
DateResult {
numeric_day: secs - day_seconds,
date: alloc_print!(
"{:04}{:02}{:02}T{:02}{:02}{:02}Z",
year,
month,
day,
hours,
minutes,
seconds
)
.into_boxed_slice(),
}
}
fn epoch_to_utc_components(secs: u64) -> (u32, u32, u32, u32, u32, u32, u64) {
let day_seconds = secs % 86_400;
let hours = u32::try_from(day_seconds / 3600).expect("int cast");
let minutes = u32::try_from((day_seconds % 3600) / 60).expect("int cast");
let seconds = u32::try_from(day_seconds % 60).expect("int cast");
let z: i64 = i64::try_from(secs / 86_400).expect("int cast") + 719_468; let era: i64 = z.div_euclid(146_097);
let doe: u64 = u64::try_from(z - era * 146_097).expect("int cast"); let yoe: u64 = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y: i64 = i64::try_from(yoe).expect("int cast") + era * 400;
let doy: u64 = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp: u64 = (5 * doy + 2) / 153; let day: u32 = u32::try_from(doy - (153 * mp + 2) / 5 + 1).expect("int cast"); let month: u32 = u32::try_from(if mp < 10 { mp + 3 } else { mp - 9 }).expect("int cast"); let year: u32 = u32::try_from(y + i64::from(month <= 2)).expect("int cast");
(year, month, day, hours, minutes, seconds, day_seconds)
}
pub(crate) const DIGESTED_HMAC_256_LEN: usize = 32;
pub struct SignResult {
pub amz_date: Box<[u8]>,
pub host: Box<[u8]>,
pub authorization: Box<[u8]>,
pub url: Box<[u8]>,
pub content_disposition: Box<[u8]>,
pub content_encoding: Box<[u8]>,
pub content_md5: Box<[u8]>,
pub session_token: Box<[u8]>,
pub acl: Option<ACL>,
pub storage_class: Option<StorageClass>,
pub request_payer: bool,
pub _headers: [PicoHeader; Self::MAX_HEADERS],
pub _headers_len: u8,
}
impl SignResult {
pub const MAX_HEADERS: usize = 11;
pub fn headers(&self) -> &[PicoHeader] {
&self._headers[0..self._headers_len as usize]
}
pub fn mix_with_header<'b>(
&self,
headers_buffer: &'b mut [PicoHeader],
header: PicoHeader,
) -> &'b [PicoHeader] {
let len = self._headers_len as usize;
for (i, existing_header) in self._headers[0..len].iter().enumerate() {
headers_buffer[i] = *existing_header;
}
headers_buffer[len] = header;
&headers_buffer[0..len + 1]
}
}
impl Default for SignResult {
fn default() -> Self {
Self {
amz_date: Box::default(),
host: Box::default(),
authorization: Box::default(),
url: Box::default(),
content_disposition: Box::default(),
content_encoding: Box::default(),
content_md5: Box::default(),
session_token: Box::default(),
acl: None,
storage_class: None,
request_payer: false,
_headers: [pico_header_empty(); Self::MAX_HEADERS],
_headers_len: 0,
}
}
}
impl Drop for SignResult {
fn drop(&mut self) {
zero_sensitive(&mut self.amz_date);
zero_sensitive(&mut self.session_token);
zero_sensitive(&mut self.content_disposition);
zero_sensitive(&mut self.content_encoding);
zero_sensitive(&mut self.host);
zero_sensitive(&mut self.authorization);
zero_sensitive(&mut self.url);
}
}
#[inline]
fn zero_sensitive(b: &mut Box<[u8]>) {
unsafe { bun_core::secure_zero(b.as_mut_ptr(), b.len()) };
}
#[derive(Clone, Copy)]
pub struct SignQueryOptions {
pub expires: usize,
}
impl Default for SignQueryOptions {
fn default() -> Self {
Self { expires: 86400 }
}
}
#[derive(Clone, Copy)]
pub struct SignOptions<'a> {
pub path: &'a [u8],
pub method: Method,
pub content_hash: Option<&'a [u8]>,
pub content_md5: Option<&'a [u8]>,
pub search_params: Option<&'a [u8]>,
pub content_disposition: Option<&'a [u8]>,
pub content_type: Option<&'a [u8]>,
pub content_encoding: Option<&'a [u8]>,
pub acl: Option<ACL>,
pub storage_class: Option<StorageClass>,
pub request_payer: bool,
}
pub fn guess_bucket(endpoint: &[u8]) -> Option<&[u8]> {
if strings::index_of(endpoint, b".amazonaws.com").is_some() {
if let Some(end) = strings::index_of(endpoint, b".s3.") {
let Some(start) = strings::index_of(endpoint, b"/") else {
return Some(&endpoint[0..end]);
};
return Some(&endpoint[start + 1..end]);
}
} else if let Some(r2_start) = strings::index_of(endpoint, b".r2.cloudflarestorage.com") {
let end = strings::index_of(endpoint, b".")?; if end > 0 && r2_start == end {
return None;
}
let Some(start) = strings::index_of(endpoint, b"/") else {
return Some(&endpoint[0..end]);
};
return Some(&endpoint[start + 1..end]);
}
None
}
pub fn guess_region(endpoint: &[u8]) -> &[u8] {
if !endpoint.is_empty() {
if strings::ends_with(endpoint, b".r2.cloudflarestorage.com") {
return b"auto";
}
if let Some(end) = strings::index_of(endpoint, b".amazonaws.com") {
if let Some(start) = strings::index_of(endpoint, b"s3.") {
return &endpoint[start + 3..end];
}
}
return b"auto";
}
b"us-east-1"
}
#[derive(Debug, thiserror::Error, strum::IntoStaticStr)]
pub enum EncodeError {
#[error("BufferTooSmall")]
BufferTooSmall,
}
pub fn encode_uri_component<'b, const ENCODE_SLASH: bool>(
input: &[u8],
buffer: &'b mut [u8],
) -> Result<&'b [u8], EncodeError> {
let mut written: usize = 0;
for &c in input {
match c {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
if written >= buffer.len() {
return Err(EncodeError::BufferTooSmall);
}
buffer[written] = c;
written += 1;
}
_ => {
if !ENCODE_SLASH && (c == b'/' || c == b'\\') {
if written >= buffer.len() {
return Err(EncodeError::BufferTooSmall);
}
buffer[written] = if c == b'\\' { b'/' } else { c };
written += 1;
continue;
}
if written + 3 > buffer.len() {
return Err(EncodeError::BufferTooSmall);
}
buffer[written] = b'%';
buffer[written + 1] = bun_core::fmt::hex_char_upper(c >> 4);
buffer[written + 2] = bun_core::fmt::hex_char_upper(c);
written += 3;
}
}
}
Ok(&buffer[..written])
}
fn normalize_name(name: &[u8]) -> &[u8] {
if name.is_empty() {
return name;
}
strings::trim(name, b"/\\")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error, strum::IntoStaticStr)]
pub enum SignError {
#[error("MissingCredentials")]
MissingCredentials,
#[error("InvalidMethod")]
InvalidMethod,
#[error("InvalidPath")]
InvalidPath,
#[error("InvalidEndpoint")]
InvalidEndpoint,
#[error("InvalidSessionToken")]
InvalidSessionToken,
#[error("InvalidHeaderValue")]
InvalidHeaderValue,
#[error("FailedToGenerateSignature")]
FailedToGenerateSignature,
#[error("NoSpaceLeft")]
NoSpaceLeft,
}
bun_core::named_error_set!(SignError);
impl<'a> Default for SignOptions<'a> {
fn default() -> Self {
Self {
path: b"",
method: Method::GET,
content_hash: None,
content_md5: None,
search_params: None,
content_disposition: None,
content_type: None,
content_encoding: None,
acl: None,
storage_class: None,
request_payer: false,
}
}
}
#[derive(Default)]
pub struct S3CredentialsWithOptions {
pub credentials: S3Credentials,
pub options: MultiPartUploadOptions,
pub acl: Option<ACL>,
pub storage_class: Option<StorageClass>,
pub content_disposition: Option<RawSlice<u8>>,
pub content_type: Option<RawSlice<u8>>,
pub content_encoding: Option<RawSlice<u8>>,
pub request_payer: bool,
pub changed_credentials: bool,
pub virtual_hosted_style: bool,
pub _access_key_id_slice: Option<bun_core::ZigStringSlice>,
pub _secret_access_key_slice: Option<bun_core::ZigStringSlice>,
pub _region_slice: Option<bun_core::ZigStringSlice>,
pub _endpoint_slice: Option<bun_core::ZigStringSlice>,
pub _bucket_slice: Option<bun_core::ZigStringSlice>,
pub _session_token_slice: Option<bun_core::ZigStringSlice>,
pub _content_disposition_slice: Option<bun_core::ZigStringSlice>,
pub _content_type_slice: Option<bun_core::ZigStringSlice>,
pub _content_encoding_slice: Option<bun_core::ZigStringSlice>,
}
#[derive(Clone, Copy, Default)]
pub(crate) struct SignedHeadersKey {
pub content_disposition: bool,
pub content_encoding: bool,
pub content_md5: bool,
pub acl: bool,
pub request_payer: bool,
pub session_token: bool,
pub storage_class: bool,
}
struct SignedHeaders;
impl SignedHeaders {
fn get(key: SignedHeadersKey, buf: &mut [u8; 256]) -> &[u8] {
let mut n = 0usize;
macro_rules! push {
($s:expr) => {{
let s: &[u8] = $s;
buf[n..n + s.len()].copy_from_slice(s);
n += s.len();
}};
}
if key.content_disposition {
push!(b"content-disposition;");
}
if key.content_encoding {
push!(b"content-encoding;");
}
if key.content_md5 {
push!(b"content-md5;");
}
push!(b"host;");
if key.acl {
push!(b"x-amz-acl;");
}
push!(b"x-amz-content-sha256;x-amz-date");
if key.request_payer {
push!(b";x-amz-request-payer");
}
if key.session_token {
push!(b";x-amz-security-token");
}
if key.storage_class {
push!(b";x-amz-storage-class");
}
unsafe { core::slice::from_raw_parts(buf.as_ptr(), n) }
}
}
struct CanonicalRequest;
impl CanonicalRequest {
pub(crate) fn format<'b>(
buf: &'b mut [u8],
key: SignedHeadersKey,
method: &[u8],
path: &[u8],
query: &[u8],
content_disposition: Option<&[u8]>,
content_encoding: Option<&[u8]>,
content_md5: Option<&[u8]>,
host: &[u8],
acl: Option<&[u8]>,
hash: &[u8],
date: &[u8],
session_token: Option<&[u8]>,
storage_class: Option<&[u8]>,
signed_headers: &[u8],
) -> Result<&'b [u8], core::fmt::Error> {
let mut c = bun_core::fmt::SliceCursor::new(buf);
macro_rules! w {
($($arg:tt)*) => { core::fmt::Write::write_fmt(&mut c, format_args!($($arg)*))? };
}
w!(
"{}\n{}\n{}\n",
BStr::new(method),
BStr::new(path),
BStr::new(query)
);
if key.content_disposition {
w!(
"content-disposition:{}\n",
BStr::new(content_disposition.unwrap())
);
}
if key.content_encoding {
w!(
"content-encoding:{}\n",
BStr::new(content_encoding.unwrap())
);
}
if key.content_md5 {
w!("content-md5:{}\n", BStr::new(content_md5.unwrap()));
}
w!("host:{}\n", BStr::new(host));
if key.acl {
w!("x-amz-acl:{}\n", BStr::new(acl.unwrap()));
}
w!(
"x-amz-content-sha256:{}\nx-amz-date:{}\n",
BStr::new(hash),
BStr::new(date)
);
if key.request_payer {
w!("x-amz-request-payer:requester\n");
}
if key.session_token {
w!(
"x-amz-security-token:{}\n",
BStr::new(session_token.unwrap())
);
}
if key.storage_class {
w!(
"x-amz-storage-class:{}\n",
BStr::new(storage_class.unwrap())
);
}
w!("\n{}\n{}", BStr::new(signed_headers), BStr::new(hash));
let len = c.at;
Ok(&c.buf[..len])
}
}
fn contains_newline_or_cr(value: &[u8]) -> bool {
strings::index_of_any(value, b"\r\n").is_some()
}