s3-wire 0.2.0

An async, streaming S3-compatible client for Rust
Documentation
use http::header::{AUTHORIZATION, CONTENT_LENGTH, HOST};
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use secrecy::ExposeSecret;
use time::OffsetDateTime;

use super::super::S3Client;
use crate::endpoint::EndpointUrl;
use crate::error::{ErrorCategory, RetryClassification, S3Error};
use crate::signing::{
    Header, HeaderSigningRequest, QueryParam, SigningCredentials, SigningPath, SigningScope,
    sign_headers,
};
use crate::stream::{PreparedBody, TransportBody};

pub(super) struct SignedRequestInput<'a> {
    pub(super) method: Method,
    pub(super) target: &'a EndpointUrl,
    pub(super) query: &'a [QueryParam<'a>],
    pub(super) headers: HeaderMap,
    pub(super) body: Option<&'a PreparedBody>,
    pub(super) payload_hash: &'a str,
    pub(super) signing_region: &'a str,
}

impl S3Client {
    pub(super) async fn signed_request(
        &self,
        input: SignedRequestInput<'_>,
    ) -> Result<http::Request<TransportBody>, S3Error> {
        let SignedRequestInput {
            method,
            target,
            query,
            mut headers,
            body,
            payload_hash,
            signing_region,
        } = input;
        headers.insert(
            HOST,
            HeaderValue::from_str(target.authority())
                .map_err(|_| S3Error::configuration("endpoint authority is not a valid header"))?,
        );
        headers.insert(
            HeaderName::from_static("x-amz-content-sha256"),
            HeaderValue::from_str(payload_hash)
                .map_err(|_| S3Error::configuration("payload digest is not a valid header"))?,
        );
        if let Some(body) = body {
            headers.insert(CONTENT_LENGTH, HeaderValue::from(body.length()));
        }

        let credentials = self
            .inner
            .config
            .credentials_provider()
            .provide_credentials()
            .await?;
        if credentials.expires_by(OffsetDateTime::now_utc()) {
            return Err(S3Error::new(
                ErrorCategory::Authentication,
                "credential provider returned expired credentials",
                RetryClassification::Never,
            ));
        }

        let owned_headers = headers
            .iter()
            .map(|(name, value)| {
                value
                    .to_str()
                    .map(|value| (name.as_str().to_owned(), value.to_owned()))
                    .map_err(|_| S3Error::configuration("a signed header contains non-ASCII bytes"))
            })
            .collect::<Result<Vec<_>, _>>()?;
        let signing_headers = owned_headers
            .iter()
            .map(|(name, value)| Header::new(name, value))
            .collect::<Vec<_>>();
        let secret = credentials.secret_access_key().expose_secret();
        let session_token = credentials.session_token().map(ExposeSecret::expose_secret);
        let signing_credentials = SigningCredentials::new(
            credentials.access_key_id(),
            secret.as_bytes(),
            session_token,
        );
        let path = target
            .path_and_query()
            .split_once('?')
            .map_or(target.path_and_query(), |(path, _)| path);
        let signing_request = HeaderSigningRequest {
            method: method.as_str(),
            uri_path: SigningPath::encoded(path),
            query,
            headers: &signing_headers,
            payload_hash,
        };
        let signed: crate::signing::HeaderSigningOutput = sign_headers(
            &signing_credentials,
            SigningScope::new(signing_region, "s3"),
            &signing_request,
            OffsetDateTime::now_utc(),
        )
        .map_err(|error| {
            S3Error::new(
                ErrorCategory::Authentication,
                "request signing failed",
                RetryClassification::Never,
            )
            .with_source(error)
        })?;
        headers.insert(
            AUTHORIZATION,
            HeaderValue::from_str(signed.authorization())
                .map_err(|_| S3Error::configuration("generated authorization header is invalid"))?,
        );
        headers.insert(
            HeaderName::from_static("x-amz-date"),
            HeaderValue::from_str(signed.amz_date())
                .map_err(|_| S3Error::configuration("generated signing date is invalid"))?,
        );
        if let Some(token) = signed.security_token() {
            headers.insert(
                HeaderName::from_static("x-amz-security-token"),
                HeaderValue::from_str(token).map_err(|_| {
                    S3Error::configuration("generated session token header is invalid")
                })?,
            );
        }

        let request_body = match body {
            Some(body) => body.request_body().await?,
            None => TransportBody::empty(),
        };
        let mut request = http::Request::builder()
            .method(method)
            .uri(target.request_uri())
            .body(request_body)
            .map_err(|error| {
                S3Error::configuration("signed request could not be constructed").with_source(error)
            })?;
        *request.headers_mut() = headers;
        Ok(request)
    }
}