bucketwarden-server 0.1.0

BucketWarden storage server runtime.
Documentation
#[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
    })
}