s3-wire 0.3.1

An async, streaming S3-compatible client for Rust
Documentation
use std::time::{Duration, SystemTime};

use http::header::RETRY_AFTER;
use http::{HeaderMap, StatusCode};

use super::super::S3Client;

impl S3Client {
    pub(super) fn redirected_aws_authority(&self, region: &str) -> Option<String> {
        let endpoint = self.inner.config.endpoint().url();
        if endpoint.scheme() != "https" {
            return None;
        }
        let authority = endpoint.authority();
        let host = authority
            .split_once(':')
            .map_or(authority, |(host, _)| host);
        let (name, suffix) = if let Some(name) = host.strip_suffix(".amazonaws.com.cn") {
            (name, "amazonaws.com.cn")
        } else {
            let name = host.strip_suffix(".amazonaws.com")?;
            (name, "amazonaws.com")
        };
        let prefix = if name == "s3" || is_regional_name(name, "s3") {
            "s3"
        } else if is_regional_name(name, "s3.dualstack") {
            "s3.dualstack"
        } else if is_regional_name(name, "s3-fips") {
            "s3-fips"
        } else if is_regional_name(name, "s3-fips.dualstack") {
            "s3-fips.dualstack"
        } else {
            return None;
        };
        Some(format!("{prefix}.{region}.{suffix}"))
    }
}

fn is_regional_name(name: &str, prefix: &str) -> bool {
    name.strip_prefix(prefix)
        .and_then(|rest| rest.strip_prefix('.'))
        .is_some_and(|region| {
            !region.is_empty()
                && region.len() <= 64
                && region
                    .bytes()
                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
        })
}

pub(super) fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
    headers
        .get(RETRY_AFTER)
        .and_then(parse_retry_after_value)
        .or_else(|| {
            headers
                .get("x-amz-retry-after")
                .and_then(parse_retry_after_value)
        })
}

fn parse_retry_after_value(value: &http::HeaderValue) -> Option<Duration> {
    let value = value.to_str().ok()?;
    if let Ok(seconds) = value.parse::<u64>() {
        return Some(Duration::from_secs(seconds));
    }
    let retry_at = httpdate::parse_http_date(value).ok()?;
    Some(
        retry_at
            .duration_since(SystemTime::now())
            .unwrap_or(Duration::ZERO),
    )
}

pub(super) fn controlled_region_redirect(status: StatusCode, headers: &HeaderMap) -> Option<&str> {
    if !matches!(
        status,
        StatusCode::MOVED_PERMANENTLY
            | StatusCode::TEMPORARY_REDIRECT
            | StatusCode::PERMANENT_REDIRECT
            | StatusCode::BAD_REQUEST
    ) {
        return None;
    }
    headers
        .get("x-amz-bucket-region")?
        .to_str()
        .ok()
        .filter(|region| {
            !region.is_empty()
                && region.len() <= 64
                && region
                    .bytes()
                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
        })
}

#[cfg(test)]
mod tests {
    use http::{HeaderName, HeaderValue};

    use super::*;
    use crate::config::S3Config;
    use crate::endpoint::Endpoint;

    fn client_for_endpoint(endpoint: &str) -> S3Client {
        let config = S3Config::builder()
            .endpoint(Endpoint::new(endpoint).unwrap())
            .bucket("bucket")
            .build()
            .unwrap();
        S3Client::new(config).unwrap()
    }

    #[test]
    fn region_redirect_authority_preserves_whitelisted_aws_endpoint_shapes() {
        for (endpoint, region, expected) in [
            (
                "https://s3.amazonaws.com",
                "eu-west-1",
                "s3.eu-west-1.amazonaws.com",
            ),
            (
                "https://s3.us-east-1.amazonaws.com",
                "us-west-2",
                "s3.us-west-2.amazonaws.com",
            ),
            (
                "https://s3.dualstack.us-east-1.amazonaws.com",
                "us-west-2",
                "s3.dualstack.us-west-2.amazonaws.com",
            ),
            (
                "https://s3-fips.us-east-1.amazonaws.com",
                "us-gov-west-1",
                "s3-fips.us-gov-west-1.amazonaws.com",
            ),
            (
                "https://s3-fips.dualstack.us-east-1.amazonaws.com",
                "us-gov-west-1",
                "s3-fips.dualstack.us-gov-west-1.amazonaws.com",
            ),
            (
                "https://s3.cn-north-1.amazonaws.com.cn",
                "cn-northwest-1",
                "s3.cn-northwest-1.amazonaws.com.cn",
            ),
        ] {
            assert_eq!(
                client_for_endpoint(endpoint).redirected_aws_authority(region),
                Some(expected.to_owned())
            );
        }
    }

    #[test]
    fn region_redirect_authority_rejects_non_aws_endpoints() {
        assert_eq!(
            client_for_endpoint("https://objects.example.com")
                .redirected_aws_authority("us-west-2"),
            None
        );
    }

    #[test]
    fn retry_after_in_the_past_means_no_delay() {
        let mut headers = HeaderMap::new();
        headers.insert(
            RETRY_AFTER,
            HeaderValue::from_static("Thu, 01 Jan 1970 00:00:00 GMT"),
        );
        assert_eq!(parse_retry_after(&headers), Some(Duration::ZERO));
    }

    #[test]
    fn accepts_amazon_retry_after_header() {
        let mut headers = HeaderMap::new();
        headers.insert(
            HeaderName::from_static("x-amz-retry-after"),
            HeaderValue::from_static("7"),
        );
        assert_eq!(parse_retry_after(&headers), Some(Duration::from_secs(7)));
    }

    #[test]
    fn region_redirects_require_a_valid_aws_region_header() {
        let mut headers = HeaderMap::new();
        headers.insert(
            HeaderName::from_static("x-amz-bucket-region"),
            HeaderValue::from_static("eu-west-1"),
        );
        assert_eq!(
            controlled_region_redirect(StatusCode::MOVED_PERMANENTLY, &headers),
            Some("eu-west-1")
        );
        headers.insert(
            HeaderName::from_static("x-amz-bucket-region"),
            HeaderValue::from_static("amazonaws.com"),
        );
        assert_eq!(
            controlled_region_redirect(StatusCode::MOVED_PERMANENTLY, &headers),
            None
        );
    }

    #[test]
    fn recognizes_regional_endpoint_shapes() {
        assert!(is_regional_name("s3.us-west-2", "s3"));
        assert!(is_regional_name(
            "s3-fips.dualstack.us-gov-west-1",
            "s3-fips.dualstack"
        ));
        assert!(!is_regional_name("s3.attacker.example", "s3"));
    }
}