use super::*;
pub(super) fn copy_metadata_directive(
headers: &BTreeMap<String, String>,
encryption: ServerSideEncryption,
) -> Result<MetadataDirective, RuntimeError> {
match header(headers, "x-amz-metadata-directive") {
None => Ok(MetadataDirective::Copy),
Some(value) if value.eq_ignore_ascii_case("COPY") => Ok(MetadataDirective::Copy),
Some(value) if value.eq_ignore_ascii_case("REPLACE") => {
let mut metadata = metadata_from_headers(headers);
metadata.encryption = Some(encryption);
Ok(MetadataDirective::Replace(metadata))
}
Some(value) => Err(RuntimeError::InvalidMetadataDirective(value.to_string())),
}
}
pub(super) fn dispatch_put_object_s3_http(
runtime: &mut BucketWarden,
request: S3HttpRequest,
bucket: &str,
key: &str,
) -> Result<S3HttpResponse, RuntimeError> {
validate_acl_write(&request.headers, &[])?;
if let Some(copy_source) = header(&request.headers, "x-amz-copy-source") {
let (source_bucket, source_key, source_version_id) = parse_copy_source(copy_source)?;
let encryption = runtime.encryption_from_headers(bucket, &request.headers)?;
let metadata_directive = copy_metadata_directive(&request.headers, encryption.clone())?;
let result = runtime.copy_object(
&request.principal,
CopyObjectRequest {
source_bucket,
source_key,
source_version_id,
destination_bucket: bucket.to_string(),
destination_key: key.to_string(),
metadata_directive,
destination_encryption: Some(encryption),
},
)?;
return Ok(xml_response(200, copy_object_xml(&result)));
}
let current_etag = runtime
.buckets
.get(bucket)
.and_then(|bucket_state| bucket_state.objects.get(key))
.and_then(ObjectState::current_version)
.map(|version| version.etag.as_str());
if let Some(response) = conditional_write_response(&request.headers, current_etag) {
return Ok(response);
}
let checksums = validate_write_checksums(&request.headers, &request.body)?;
let lock = runtime.object_lock_from_headers(bucket, &request.headers)?;
let encryption = runtime.encryption_from_headers(bucket, &request.headers)?;
let mut metadata = metadata_from_headers(&request.headers);
metadata.encryption = Some(encryption.clone());
let result = runtime.put_object(
&request.principal,
PutObjectRequest {
bucket: bucket.to_string(),
key: key.to_string(),
body: request.body,
metadata,
},
lock,
)?;
let mut response = S3HttpResponse::new(200)
.with_header("etag", quote_etag(&result.etag))
.with_header("x-amz-version-id", result.version_id);
response = apply_encryption_response_headers(response, Some(&encryption));
response = apply_checksum_response_headers(response, checksums);
Ok(response)
}
pub(super) fn parse_requested_object_attributes(
headers: &BTreeMap<String, String>,
) -> Result<std::collections::BTreeSet<String>, RuntimeError> {
let raw = header(headers, "x-amz-object-attributes").ok_or_else(|| {
RuntimeError::InvalidObjectAttributesRequest(
"missing x-amz-object-attributes header".to_string(),
)
})?;
let mut requested = std::collections::BTreeSet::new();
for attribute in raw
.split(',')
.map(str::trim)
.filter(|value| !value.is_empty())
{
match attribute {
"ETag" | "Checksum" | "ObjectParts" | "StorageClass" | "ObjectSize" => {
requested.insert(attribute.to_string());
}
other => {
return Err(RuntimeError::InvalidObjectAttributesRequest(format!(
"unsupported object attribute {other}"
)));
}
}
}
if requested.is_empty() {
return Err(RuntimeError::InvalidObjectAttributesRequest(
"at least one object attribute must be requested".to_string(),
));
}
Ok(requested)
}
pub(super) fn parse_rename_source(
bucket: &str,
headers: &BTreeMap<String, String>,
) -> Result<String, RuntimeError> {
let rename_source = header(headers, "x-amz-rename-source").ok_or_else(|| {
RuntimeError::InvalidObjectKey("RenameObject requires x-amz-rename-source".to_string())
})?;
let rename_source = percent_decode(rename_source.trim());
let rename_source = rename_source.trim_start_matches('/');
let (source_bucket, source_key) = rename_source
.split_once('/')
.map(|(source_bucket, source_key)| (Some(source_bucket), source_key))
.unwrap_or((None, rename_source));
if let Some(source_bucket) = source_bucket {
if source_bucket != bucket {
return Err(RuntimeError::InvalidObjectKey(
"RenameObject source bucket must match destination bucket".to_string(),
));
}
}
if !validate_object_key(source_key) {
return Err(RuntimeError::InvalidObjectKey(source_key.to_string()));
}
Ok(source_key.to_string())
}
pub(super) fn parse_update_object_encryption_request(
bucket: &str,
key: &str,
query: &BTreeMap<String, String>,
body: &[u8],
) -> Result<UpdateObjectEncryptionRequest, RuntimeError> {
let xml = String::from_utf8_lossy(body);
if !xml.contains("<ObjectEncryption") {
return Err(RuntimeError::InvalidEncryption(
"missing ObjectEncryption root".to_string(),
));
}
let sse_kms = text_between(&xml, "<SSE-KMS>", "</SSE-KMS>")
.or_else(|| text_between(&xml, "<SSEKMS>", "</SSEKMS>"))
.ok_or_else(|| {
RuntimeError::InvalidEncryption(
"UpdateObjectEncryption requires an SSE-KMS block".to_string(),
)
})?;
let kms_key_id = text_between(sse_kms, "<KMSKeyArn>", "</KMSKeyArn>")
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
RuntimeError::InvalidEncryption("UpdateObjectEncryption requires KMSKeyArn".to_string())
})?;
let bucket_key_enabled = text_between(sse_kms, "<BucketKeyEnabled>", "</BucketKeyEnabled>")
.map(str::trim)
.map(|value| match value {
"true" | "TRUE" => Ok(true),
"false" | "FALSE" => Ok(false),
other => Err(RuntimeError::InvalidEncryption(format!(
"invalid BucketKeyEnabled value: {other}"
))),
})
.transpose()?;
Ok(UpdateObjectEncryptionRequest {
bucket: bucket.to_string(),
key: key.to_string(),
version_id: query.get("versionId").cloned(),
encryption: ServerSideEncryption {
algorithm: "aws:kms".to_string(),
kms_key_id: Some(kms_key_id.to_string()),
},
bucket_key_enabled,
})
}
pub(super) fn get_object_attributes_xml(
result: &GetObjectAttributesResult,
requested: &std::collections::BTreeSet<String>,
) -> String {
let etag = requested
.contains("ETag")
.then(|| format!("<ETag>{}</ETag>", xml_escape("e_etag(&result.etag))))
.unwrap_or_default();
let checksum = requested
.contains("Checksum")
.then(|| {
format!(
"<Checksum><ChecksumSHA256>{}</ChecksumSHA256></Checksum>",
xml_escape(&result.checksum.checksum_sha256)
)
})
.unwrap_or_default();
let object_parts = requested
.contains("ObjectParts")
.then(|| {
format!(
"<ObjectParts><IsTruncated>{}</IsTruncated><MaxParts>{}</MaxParts><PartNumberMarker>{}</PartNumberMarker><TotalPartsCount>{}</TotalPartsCount></ObjectParts>",
result.object_parts.is_truncated,
result.object_parts.max_parts,
result.object_parts.part_number_marker,
result.object_parts.total_parts_count
)
})
.unwrap_or_default();
let storage_class = requested
.contains("StorageClass")
.then(|| {
format!(
"<StorageClass>{}</StorageClass>",
xml_escape(&result.storage_class)
)
})
.unwrap_or_default();
let object_size = requested
.contains("ObjectSize")
.then(|| format!("<ObjectSize>{}</ObjectSize>", result.object_size))
.unwrap_or_default();
format!(
"<GetObjectAttributesOutput><VersionId>{}</VersionId><LastModified>{}</LastModified>{etag}{checksum}{object_parts}{storage_class}{object_size}</GetObjectAttributesOutput>",
xml_escape(&result.version_id),
result.last_modified_epoch_seconds
)
}
pub(super) fn get_object_torrent_response(
result: &GetObjectResult,
host: Option<&str>,
) -> Result<S3HttpResponse, RuntimeError> {
let metainfo = single_file_torrent_metainfo(result, host);
Ok(S3HttpResponse::new(200)
.with_header("content-type", "application/x-bittorrent")
.with_header(
"content-disposition",
format!(
"attachment; filename=\"{}\"",
torrent_attachment_name(&result.key)
),
)
.with_header("content-length", metainfo.len().to_string())
.with_header("x-amz-version-id", result.version_id.clone())
.with_body(metainfo))
}
fn single_file_torrent_metainfo(result: &GetObjectResult, host: Option<&str>) -> Vec<u8> {
let piece_length = torrent_piece_length(result.body.len());
let pieces = torrent_piece_hashes(&result.body, piece_length);
let mut info = Vec::new();
bencode_dict_start(&mut info);
bencode_bytes(&mut info, b"length");
bencode_int(&mut info, result.body.len() as i64);
bencode_bytes(&mut info, b"name");
bencode_bytes(&mut info, torrent_file_name(&result.key).as_bytes());
bencode_bytes(&mut info, b"piece length");
bencode_int(&mut info, piece_length as i64);
bencode_bytes(&mut info, b"pieces");
bencode_bytes(&mut info, &pieces);
bencode_end(&mut info);
let mut metainfo = Vec::new();
bencode_dict_start(&mut metainfo);
if let Some(host) = host.filter(|value| !value.trim().is_empty()) {
bencode_bytes(&mut metainfo, b"url-list");
bencode_bytes(
&mut metainfo,
format!(
"http://{host}/{}",
torrent_object_path(&result.bucket, &result.key)
)
.as_bytes(),
);
}
bencode_bytes(&mut metainfo, b"creation date");
bencode_int(&mut metainfo, result.last_modified_epoch_seconds as i64);
bencode_bytes(&mut metainfo, b"comment");
bencode_bytes(
&mut metainfo,
format!(
"BucketWarden torrent for {}/{} version {}",
result.bucket, result.key, result.version_id
)
.as_bytes(),
);
bencode_bytes(&mut metainfo, b"info");
metainfo.extend_from_slice(&info);
bencode_end(&mut metainfo);
metainfo
}
fn torrent_piece_length(object_size: usize) -> usize {
match object_size {
0..=262_144 => 16_384,
262_145..=4_194_304 => 65_536,
_ => 262_144,
}
}
fn torrent_piece_hashes(body: &[u8], piece_length: usize) -> Vec<u8> {
if body.is_empty() {
return sha1_digest(&[]).to_vec();
}
let mut out = Vec::new();
for piece in body.chunks(piece_length) {
out.extend_from_slice(&sha1_digest(piece));
}
out
}
fn torrent_file_name(key: &str) -> String {
key.rsplit('/')
.next()
.filter(|value| !value.is_empty())
.unwrap_or("object")
.to_string()
}
fn torrent_attachment_name(key: &str) -> String {
format!("{}.torrent", torrent_file_name(key))
}
fn torrent_object_path(bucket: &str, key: &str) -> String {
format!("{}/{}", percent_encode(bucket), percent_encode(key))
}
fn bencode_dict_start(out: &mut Vec<u8>) {
out.push(b'd');
}
fn bencode_end(out: &mut Vec<u8>) {
out.push(b'e');
}
fn bencode_int(out: &mut Vec<u8>, value: i64) {
out.push(b'i');
out.extend_from_slice(value.to_string().as_bytes());
out.push(b'e');
}
fn bencode_bytes(out: &mut Vec<u8>, value: &[u8]) {
out.extend_from_slice(value.len().to_string().as_bytes());
out.push(b':');
out.extend_from_slice(value);
}
fn sha1_digest(input: &[u8]) -> [u8; 20] {
let mut data = input.to_vec();
let bit_len = (data.len() as u64) * 8;
data.push(0x80);
while data.len() % 64 != 56 {
data.push(0);
}
data.extend_from_slice(&bit_len.to_be_bytes());
let mut h0 = 0x67452301u32;
let mut h1 = 0xefcdab89u32;
let mut h2 = 0x98badcfeu32;
let mut h3 = 0x10325476u32;
let mut h4 = 0xc3d2e1f0u32;
for chunk in data.chunks(64) {
let mut w = [0u32; 80];
for (index, word) in w.iter_mut().take(16).enumerate() {
let start = index * 4;
*word = u32::from_be_bytes([
chunk[start],
chunk[start + 1],
chunk[start + 2],
chunk[start + 3],
]);
}
for index in 16..80 {
w[index] = (w[index - 3] ^ w[index - 8] ^ w[index - 14] ^ w[index - 16]).rotate_left(1);
}
let mut a = h0;
let mut b = h1;
let mut c = h2;
let mut d = h3;
let mut e = h4;
for (index, word) in w.iter().enumerate() {
let (f, k) = match index {
0..=19 => (((b & c) | ((!b) & d)), 0x5a827999),
20..=39 => (b ^ c ^ d, 0x6ed9eba1),
40..=59 => (((b & c) | (b & d) | (c & d)), 0x8f1bbcdc),
_ => (b ^ c ^ d, 0xca62c1d6),
};
let temp = a
.rotate_left(5)
.wrapping_add(f)
.wrapping_add(e)
.wrapping_add(k)
.wrapping_add(*word);
e = d;
d = c;
c = b.rotate_left(30);
b = a;
a = temp;
}
h0 = h0.wrapping_add(a);
h1 = h1.wrapping_add(b);
h2 = h2.wrapping_add(c);
h3 = h3.wrapping_add(d);
h4 = h4.wrapping_add(e);
}
let mut out = [0u8; 20];
out[..4].copy_from_slice(&h0.to_be_bytes());
out[4..8].copy_from_slice(&h1.to_be_bytes());
out[8..12].copy_from_slice(&h2.to_be_bytes());
out[12..16].copy_from_slice(&h3.to_be_bytes());
out[16..20].copy_from_slice(&h4.to_be_bytes());
out
}