#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(crate) struct ValidatedWriteChecksums {
content_md5: Option<String>,
checksum_sha256: Option<String>,
}
pub(crate) fn validate_write_checksums(
headers: &BTreeMap<String, String>,
body: &[u8],
) -> Result<ValidatedWriteChecksums, RuntimeError> {
let content_md5 = if let Some(value) = header(headers, "content-md5") {
let expected = base64_decode(value).ok_or_else(|| RuntimeError::InvalidDigest {
header: "Content-MD5".to_string(),
value: value.to_string(),
})?;
let actual = md5_digest(body);
if expected.as_slice() != actual.as_slice() {
return Err(RuntimeError::BadDigest {
header: "Content-MD5".to_string(),
});
}
Some(value.to_string())
} else {
None
};
let checksum_sha256 = if let Some(value) = header(headers, "x-amz-checksum-sha256") {
let expected = base64_decode(value).ok_or_else(|| RuntimeError::InvalidDigest {
header: "x-amz-checksum-sha256".to_string(),
value: value.to_string(),
})?;
let actual = hex_to_bytes(&sha256_hex(body)).expect("sha256 hex is valid");
if expected != actual {
return Err(RuntimeError::BadDigest {
header: "x-amz-checksum-sha256".to_string(),
});
}
Some(value.to_string())
} else {
None
};
Ok(ValidatedWriteChecksums {
content_md5,
checksum_sha256,
})
}
pub(crate) fn conditional_response(
headers: &BTreeMap<String, String>,
etag: &str,
last_modified_epoch_seconds: u64,
) -> Result<Option<S3HttpResponse>, RuntimeError> {
if let Some(value) = header(headers, "if-match") {
if !etag_condition_matches(value, etag) {
return Ok(Some(precondition_failed()));
}
}
if let Some(value) = header(headers, "if-unmodified-since") {
let threshold = parse_epoch_condition("if-unmodified-since", value)?;
if last_modified_epoch_seconds > threshold {
return Ok(Some(precondition_failed()));
}
}
if let Some(value) = header(headers, "if-none-match") {
if etag_condition_matches(value, etag) {
return Ok(Some(not_modified(etag, last_modified_epoch_seconds)));
}
}
if let Some(value) = header(headers, "if-modified-since") {
let threshold = parse_epoch_condition("if-modified-since", value)?;
if last_modified_epoch_seconds <= threshold {
return Ok(Some(not_modified(etag, last_modified_epoch_seconds)));
}
}
Ok(None)
}
pub(crate) fn conditional_write_response(
headers: &BTreeMap<String, String>,
current_etag: Option<&str>,
) -> Option<S3HttpResponse> {
if let Some(value) = header(headers, "if-match") {
let matches_current = current_etag
.map(|etag| etag_condition_matches(value, etag))
.unwrap_or(false);
if !matches_current {
return Some(precondition_failed());
}
}
if let Some(value) = header(headers, "if-none-match") {
if current_etag
.map(|etag| etag_condition_matches(value, etag))
.unwrap_or(false)
{
return Some(precondition_failed());
}
}
None
}
pub(crate) fn precondition_failed() -> S3HttpResponse {
general_error_response(
"PreconditionFailed",
412,
"At least one of the preconditions you specified did not hold.",
)
}
pub(crate) fn not_modified(etag: &str, last_modified_epoch_seconds: u64) -> S3HttpResponse {
S3HttpResponse::new(304)
.with_header("etag", quote_etag(etag))
.with_header("last-modified", last_modified_epoch_seconds.to_string())
}
pub(crate) fn parse_epoch_condition(name: &str, value: &str) -> Result<u64, RuntimeError> {
value
.trim()
.parse()
.map_err(|_| RuntimeError::InvalidConditionalHeader {
name: name.to_string(),
value: value.to_string(),
})
}
pub(crate) fn etag_condition_matches(value: &str, etag: &str) -> bool {
value.split(',').any(|candidate| {
let candidate = candidate.trim();
candidate == "*" || candidate.trim_matches('"') == etag
})
}