From 4c256ef0202122b501576e5cfa225cbebf3f009c Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 23 Jan 2024 14:10:34 -0800 Subject: [PATCH] Move `aws-http` types into `aws-runtime` (#3355) This issue addresses a semver compatibility problem similar to the one described in https://github.com/smithy-lang/smithy-rs/pull/3318, except for the storable types in the aws-http crate. I opted to move all of aws-http into aws-runtime since there was so little in there. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._ --- CHANGELOG.next.toml | 8 +- aws/rust-runtime/aws-config/Cargo.toml | 1 - .../aws-config/src/imds/client.rs | 3 +- aws/rust-runtime/aws-http/Cargo.toml | 17 +- aws/rust-runtime/aws-http/README.md | 5 +- aws/rust-runtime/aws-http/external-types.toml | 10 +- .../aws-http/src/content_encoding.rs | 625 +----------- aws/rust-runtime/aws-http/src/user_agent.rs | 781 +-------------- aws/rust-runtime/aws-inlineable/Cargo.toml | 3 +- .../src/http_request_checksum.rs | 6 +- aws/rust-runtime/aws-runtime/Cargo.toml | 9 +- .../aws-runtime/external-types.toml | 4 + .../aws-runtime/src/content_encoding.rs | 613 ++++++++++++ aws/rust-runtime/aws-runtime/src/lib.rs | 4 + .../aws-runtime/src/user_agent.rs | 886 +++++++++++++----- .../aws-runtime/src/user_agent/interceptor.rs | 277 ++++++ .../smithy/rustsdk/AwsCargoDependency.kt | 2 - .../amazon/smithy/rustsdk/AwsRuntimeType.kt | 2 - .../rustsdk/HttpRequestChecksumDecorator.kt | 1 + .../smithy/rustsdk/UserAgentDecorator.kt | 4 +- aws/sdk/integration-tests/dynamodb/Cargo.toml | 1 - aws/sdk/integration-tests/glacier/Cargo.toml | 1 - aws/sdk/integration-tests/iam/Cargo.toml | 1 - aws/sdk/integration-tests/kms/Cargo.toml | 1 - aws/sdk/integration-tests/lambda/Cargo.toml | 1 - aws/sdk/integration-tests/polly/Cargo.toml | 1 - .../integration-tests/qldbsession/Cargo.toml | 1 - aws/sdk/integration-tests/s3/Cargo.toml | 1 - .../integration-tests/s3/tests/checksums.rs | 2 +- .../s3/tests/request_information_headers.rs | 2 +- .../integration-tests/s3control/Cargo.toml | 1 - .../transcribestreaming/Cargo.toml | 1 - 32 files changed, 1672 insertions(+), 1603 deletions(-) create mode 100644 aws/rust-runtime/aws-runtime/src/content_encoding.rs create mode 100644 aws/rust-runtime/aws-runtime/src/user_agent/interceptor.rs diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index fc4c4c257..03b974105 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -9,4 +9,10 @@ # message = "Fix typos in module documentation for generated crates" # references = ["smithy-rs#920"] # meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"} -# author = "rcoh" \ No newline at end of file +# author = "rcoh" + +[[aws-sdk-rust]] +message = "The types in the aws-http crate were moved into aws-runtime. Deprecated type aliases were put in place to point to the new locations." +references = ["smithy-rs#3355"] +meta = { "breaking" = false, "tada" = false, "bug" = false } +author = "jdisanti" diff --git a/aws/rust-runtime/aws-config/Cargo.toml b/aws/rust-runtime/aws-config/Cargo.toml index b95524096..bd84fc4f9 100644 --- a/aws/rust-runtime/aws-config/Cargo.toml +++ b/aws/rust-runtime/aws-config/Cargo.toml @@ -23,7 +23,6 @@ allow-compilation = [] [dependencies] aws-credential-types = { path = "../../sdk/build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } -aws-http = { path = "../../sdk/build/aws-sdk/sdk/aws-http" } aws-sdk-sts = { path = "../../sdk/build/aws-sdk/sdk/sts", default-features = false } aws-smithy-async = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-async" } aws-smithy-http = { path = "../../sdk/build/aws-sdk/sdk/aws-smithy-http" } diff --git a/aws/rust-runtime/aws-config/src/imds/client.rs b/aws/rust-runtime/aws-config/src/imds/client.rs index 154225b29..a660369c5 100644 --- a/aws/rust-runtime/aws-config/src/imds/client.rs +++ b/aws/rust-runtime/aws-config/src/imds/client.rs @@ -11,8 +11,7 @@ use crate::imds::client::error::{BuildError, ImdsError, InnerImdsError, InvalidE use crate::imds::client::token::TokenRuntimePlugin; use crate::provider_config::ProviderConfig; use crate::PKG_VERSION; -use aws_http::user_agent::{ApiMetadata, AwsUserAgent}; -use aws_runtime::user_agent::UserAgentInterceptor; +use aws_runtime::user_agent::{ApiMetadata, AwsUserAgent, UserAgentInterceptor}; use aws_smithy_runtime::client::orchestrator::operation::Operation; use aws_smithy_runtime::client::retries::strategy::StandardRetryStrategy; use aws_smithy_runtime_api::box_error::BoxError; diff --git a/aws/rust-runtime/aws-http/Cargo.toml b/aws/rust-runtime/aws-http/Cargo.toml index e33b302d6..e01593ab5 100644 --- a/aws/rust-runtime/aws-http/Cargo.toml +++ b/aws/rust-runtime/aws-http/Cargo.toml @@ -1,25 +1,14 @@ [package] name = "aws-http" -version = "0.0.0-smithy-rs-head" +version = "0.60.5" authors = ["AWS Rust SDK Team ", "Russell Cohen "] -description = "HTTP specific AWS SDK behaviors." +description = "This crate is no longer used by the AWS SDK and is deprecated." edition = "2021" license = "Apache-2.0" repository = "https://github.com/smithy-lang/smithy-rs" [dependencies] -aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api", features = ["client"] } -aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types", features = ["http-body-0-4-x"] } -aws-types = { path = "../aws-types" } -bytes = "1.1" -http = "0.2.3" -http-body = "0.4.5" -tracing = "0.1" -pin-project-lite = "0.2.9" - -[dev-dependencies] -bytes-utils = "0.1.2" -tokio = { version = "1.23.1", features = ["macros", "rt", "time"] } +aws-runtime = { path = "../aws-runtime", features = ["http-02x"] } [package.metadata.docs.rs] all-features = true diff --git a/aws/rust-runtime/aws-http/README.md b/aws/rust-runtime/aws-http/README.md index b4d264c82..3e85fe868 100644 --- a/aws/rust-runtime/aws-http/README.md +++ b/aws/rust-runtime/aws-http/README.md @@ -1,9 +1,6 @@ # aws-http -This crate provides middleware for AWS SDKs using HTTP including: -* Generalized retry policy -* Middleware for setting `User-Agent` headers based on runtime configuration -* Credential loading async middleware +This crate is no longer used by the AWS SDK and is deprecated. This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/smithy-lang/smithy-rs) code generator. In most cases, it should not be used directly. diff --git a/aws/rust-runtime/aws-http/external-types.toml b/aws/rust-runtime/aws-http/external-types.toml index 429bf1a90..db178fc89 100644 --- a/aws/rust-runtime/aws-http/external-types.toml +++ b/aws/rust-runtime/aws-http/external-types.toml @@ -1,11 +1,3 @@ allowed_external_types = [ - "aws_smithy_runtime_api::http::headers::Headers", - "aws_smithy_types::body::Error", - "aws_smithy_types::config_bag::storable::Storable", - "aws_smithy_types::config_bag::storable::StoreReplace", - "aws_smithy_types::error::metadata::Builder", - "aws_types::app_name::AppName", - "aws_types::os_shim_internal::Env", - "bytes::bytes::Bytes", - "http_body::Body", + "aws_runtime::*", ] diff --git a/aws/rust-runtime/aws-http/src/content_encoding.rs b/aws/rust-runtime/aws-http/src/content_encoding.rs index 2c00cff9c..7e1becf64 100644 --- a/aws/rust-runtime/aws-http/src/content_encoding.rs +++ b/aws/rust-runtime/aws-http/src/content_encoding.rs @@ -3,611 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -use bytes::{Bytes, BytesMut}; -use http::{HeaderMap, HeaderValue}; -use http_body::{Body, SizeHint}; -use pin_project_lite::pin_project; - -use std::pin::Pin; -use std::task::{Context, Poll}; - -const CRLF: &str = "\r\n"; -const CHUNK_TERMINATOR: &str = "0\r\n"; -const TRAILER_SEPARATOR: &[u8] = b":"; - -/// Content encoding header value constants +/// Use aws_runtime::content_encoding::header_value instead. +#[deprecated( + since = "0.60.2", + note = "Use aws_runtime::content_encoding::header_value instead." +)] pub mod header_value { - /// Header value denoting "aws-chunked" encoding + /// Use aws_runtime::content_encoding::header_value::AWS_CHUNKED instead. + #[deprecated( + since = "0.60.2", + note = "Use aws_runtime::content_encoding::header_value::AWS_CHUNKED instead." + )] pub const AWS_CHUNKED: &str = "aws-chunked"; } -/// Options used when constructing an [`AwsChunkedBody`]. -#[derive(Debug, Default)] -#[non_exhaustive] -pub struct AwsChunkedBodyOptions { - /// The total size of the stream. Because we only support unsigned encoding - /// this implies that there will only be a single chunk containing the - /// underlying payload. - stream_length: u64, - /// The length of each trailer sent within an `AwsChunkedBody`. Necessary in - /// order to correctly calculate the total size of the body accurately. - trailer_lengths: Vec, -} - -impl AwsChunkedBodyOptions { - /// Create a new [`AwsChunkedBodyOptions`]. - pub fn new(stream_length: u64, trailer_lengths: Vec) -> Self { - Self { - stream_length, - trailer_lengths, - } - } - - fn total_trailer_length(&self) -> u64 { - self.trailer_lengths.iter().sum::() - // We need to account for a CRLF after each trailer name/value pair - + (self.trailer_lengths.len() * CRLF.len()) as u64 - } - - /// Set a trailer len - pub fn with_trailer_len(mut self, trailer_len: u64) -> Self { - self.trailer_lengths.push(trailer_len); - self - } -} - -#[derive(Debug, PartialEq, Eq)] -enum AwsChunkedBodyState { - /// Write out the size of the chunk that will follow. Then, transition into the - /// `WritingChunk` state. - WritingChunkSize, - /// Write out the next chunk of data. Multiple polls of the inner body may need to occur before - /// all data is written out. Once there is no more data to write, transition into the - /// `WritingTrailers` state. - WritingChunk, - /// Write out all trailers associated with this `AwsChunkedBody` and then transition into the - /// `Closed` state. - WritingTrailers, - /// This is the final state. Write out the body terminator and then remain in this state. - Closed, -} - -pin_project! { - /// A request body compatible with `Content-Encoding: aws-chunked`. This implementation is only - /// capable of writing a single chunk and does not support signed chunks. - /// - /// Chunked-Body grammar is defined in [ABNF] as: - /// - /// ```txt - /// Chunked-Body = *chunk - /// last-chunk - /// chunked-trailer - /// CRLF - /// - /// chunk = chunk-size CRLF chunk-data CRLF - /// chunk-size = 1*HEXDIG - /// last-chunk = 1*("0") CRLF - /// chunked-trailer = *( entity-header CRLF ) - /// entity-header = field-name ":" OWS field-value OWS - /// ``` - /// For more info on what the abbreviations mean, see https://datatracker.ietf.org/doc/html/rfc7230#section-1.2 - /// - /// [ABNF]:https://en.wikipedia.org/wiki/Augmented_Backus%E2%80%93Naur_form - #[derive(Debug)] - pub struct AwsChunkedBody { - #[pin] - inner: InnerBody, - #[pin] - state: AwsChunkedBodyState, - options: AwsChunkedBodyOptions, - inner_body_bytes_read_so_far: usize, - } -} - -impl AwsChunkedBody { - /// Wrap the given body in an outer body compatible with `Content-Encoding: aws-chunked` - pub fn new(body: Inner, options: AwsChunkedBodyOptions) -> Self { - Self { - inner: body, - state: AwsChunkedBodyState::WritingChunkSize, - options, - inner_body_bytes_read_so_far: 0, - } - } - - fn encoded_length(&self) -> u64 { - let mut length = 0; - if self.options.stream_length != 0 { - length += get_unsigned_chunk_bytes_length(self.options.stream_length); - } - - // End chunk - length += CHUNK_TERMINATOR.len() as u64; - - // Trailers - for len in self.options.trailer_lengths.iter() { - length += len + CRLF.len() as u64; - } - - // Encoding terminator - length += CRLF.len() as u64; - - length - } -} - -fn get_unsigned_chunk_bytes_length(payload_length: u64) -> u64 { - let hex_repr_len = int_log16(payload_length); - hex_repr_len + CRLF.len() as u64 + payload_length + CRLF.len() as u64 -} - -/// Writes trailers out into a `string` and then converts that `String` to a `Bytes` before -/// returning. -/// -/// - Trailer names are separated by a single colon only, no space. -/// - Trailer names with multiple values will be written out one line per value, with the name -/// appearing on each line. -fn trailers_as_aws_chunked_bytes( - trailer_map: Option, - estimated_length: u64, -) -> BytesMut { - if let Some(trailer_map) = trailer_map { - let mut current_header_name = None; - let mut trailers = BytesMut::with_capacity(estimated_length.try_into().unwrap_or_default()); - - for (header_name, header_value) in trailer_map.into_iter() { - // When a header has multiple values, the name only comes up in iteration the first time - // we see it. Therefore, we need to keep track of the last name we saw and fall back to - // it when `header_name == None`. - current_header_name = header_name.or(current_header_name); - - // In practice, this will always exist, but `if let` is nicer than unwrap - if let Some(header_name) = current_header_name.as_ref() { - trailers.extend_from_slice(header_name.as_ref()); - trailers.extend_from_slice(TRAILER_SEPARATOR); - trailers.extend_from_slice(header_value.as_bytes()); - trailers.extend_from_slice(CRLF.as_bytes()); - } - } - - trailers - } else { - BytesMut::new() - } -} - -/// Given an optional `HeaderMap`, calculate the total number of bytes required to represent the -/// `HeaderMap`. If no `HeaderMap` is given as input, return 0. -/// -/// - Trailer names are separated by a single colon only, no space. -/// - Trailer names with multiple values will be written out one line per value, with the name -/// appearing on each line. -fn total_rendered_length_of_trailers(trailer_map: Option<&HeaderMap>) -> u64 { - match trailer_map { - Some(trailer_map) => trailer_map - .iter() - .map(|(trailer_name, trailer_value)| { - trailer_name.as_str().len() - + TRAILER_SEPARATOR.len() - + trailer_value.len() - + CRLF.len() - }) - .sum::() as u64, - None => 0, - } -} - -impl Body for AwsChunkedBody -where - Inner: Body, -{ - type Data = Bytes; - type Error = aws_smithy_types::body::Error; - - fn poll_data( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - tracing::trace!(state = ?self.state, "polling AwsChunkedBody"); - let mut this = self.project(); - - match *this.state { - AwsChunkedBodyState::WritingChunkSize => { - if this.options.stream_length == 0 { - // If the stream is empty, we skip to writing trailers after writing the CHUNK_TERMINATOR. - *this.state = AwsChunkedBodyState::WritingTrailers; - tracing::trace!("stream is empty, writing chunk terminator"); - Poll::Ready(Some(Ok(Bytes::from([CHUNK_TERMINATOR].concat())))) - } else { - *this.state = AwsChunkedBodyState::WritingChunk; - // A chunk must be prefixed by chunk size in hexadecimal - let chunk_size = format!("{:X?}{CRLF}", this.options.stream_length); - tracing::trace!(%chunk_size, "writing chunk size"); - let chunk_size = Bytes::from(chunk_size); - Poll::Ready(Some(Ok(chunk_size))) - } - } - AwsChunkedBodyState::WritingChunk => match this.inner.poll_data(cx) { - Poll::Ready(Some(Ok(data))) => { - tracing::trace!(len = data.len(), "writing chunk data"); - *this.inner_body_bytes_read_so_far += data.len(); - Poll::Ready(Some(Ok(data))) - } - Poll::Ready(None) => { - let actual_stream_length = *this.inner_body_bytes_read_so_far as u64; - let expected_stream_length = this.options.stream_length; - if actual_stream_length != expected_stream_length { - let err = Box::new(AwsChunkedBodyError::StreamLengthMismatch { - actual: actual_stream_length, - expected: expected_stream_length, - }); - return Poll::Ready(Some(Err(err))); - }; - - tracing::trace!("no more chunk data, writing CRLF and chunk terminator"); - *this.state = AwsChunkedBodyState::WritingTrailers; - // Since we wrote chunk data, we end it with a CRLF and since we only write - // a single chunk, we write the CHUNK_TERMINATOR immediately after - Poll::Ready(Some(Ok(Bytes::from([CRLF, CHUNK_TERMINATOR].concat())))) - } - Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))), - Poll::Pending => Poll::Pending, - }, - AwsChunkedBodyState::WritingTrailers => { - return match this.inner.poll_trailers(cx) { - Poll::Ready(Ok(trailers)) => { - *this.state = AwsChunkedBodyState::Closed; - let expected_length = total_rendered_length_of_trailers(trailers.as_ref()); - let actual_length = this.options.total_trailer_length(); - - if expected_length != actual_length { - let err = - Box::new(AwsChunkedBodyError::ReportedTrailerLengthMismatch { - actual: actual_length, - expected: expected_length, - }); - return Poll::Ready(Some(Err(err))); - } - - let mut trailers = - trailers_as_aws_chunked_bytes(trailers, actual_length + 1); - // Insert the final CRLF to close the body - trailers.extend_from_slice(CRLF.as_bytes()); - - Poll::Ready(Some(Ok(trailers.into()))) - } - Poll::Pending => Poll::Pending, - Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))), - }; - } - AwsChunkedBodyState::Closed => Poll::Ready(None), - } - } - - fn poll_trailers( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll>, Self::Error>> { - // Trailers were already appended to the body because of the content encoding scheme - Poll::Ready(Ok(None)) - } - - fn is_end_stream(&self) -> bool { - self.state == AwsChunkedBodyState::Closed - } - - fn size_hint(&self) -> SizeHint { - SizeHint::with_exact(self.encoded_length()) - } -} - -/// Errors related to `AwsChunkedBody` -#[derive(Debug)] -enum AwsChunkedBodyError { - /// Error that occurs when the sum of `trailer_lengths` set when creating an `AwsChunkedBody` is - /// not equal to the actual length of the trailers returned by the inner `http_body::Body` - /// implementor. These trailer lengths are necessary in order to correctly calculate the total - /// size of the body for setting the content length header. - ReportedTrailerLengthMismatch { actual: u64, expected: u64 }, - /// Error that occurs when the `stream_length` set when creating an `AwsChunkedBody` is not - /// equal to the actual length of the body returned by the inner `http_body::Body` implementor. - /// `stream_length` must be correct in order to set an accurate content length header. - StreamLengthMismatch { actual: u64, expected: u64 }, -} - -impl std::fmt::Display for AwsChunkedBodyError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::ReportedTrailerLengthMismatch { actual, expected } => { - write!(f, "When creating this AwsChunkedBody, length of trailers was reported as {expected}. However, when double checking during trailer encoding, length was found to be {actual} instead.") - } - Self::StreamLengthMismatch { actual, expected } => { - write!(f, "When creating this AwsChunkedBody, stream length was reported as {expected}. However, when double checking during body encoding, length was found to be {actual} instead.") - } - } - } -} - -impl std::error::Error for AwsChunkedBodyError {} - -// Used for finding how many hexadecimal digits it takes to represent a base 10 integer -fn int_log16(mut i: T) -> u64 -where - T: std::ops::DivAssign + PartialOrd + From + Copy, -{ - let mut len = 0; - let zero = T::from(0); - let sixteen = T::from(16); - - while i > zero { - i /= sixteen; - len += 1; - } - - len -} - -#[cfg(test)] -mod tests { - use super::{ - total_rendered_length_of_trailers, trailers_as_aws_chunked_bytes, AwsChunkedBody, - AwsChunkedBodyOptions, CHUNK_TERMINATOR, CRLF, - }; - - use aws_smithy_types::body::SdkBody; - use bytes::{Buf, Bytes}; - use bytes_utils::SegmentedBuf; - use http::{HeaderMap, HeaderValue}; - use http_body::{Body, SizeHint}; - use pin_project_lite::pin_project; - - use std::io::Read; - use std::pin::Pin; - use std::task::{Context, Poll}; - use std::time::Duration; - - pin_project! { - struct SputteringBody { - parts: Vec>, - cursor: usize, - delay_in_millis: u64, - } - } - - impl SputteringBody { - fn len(&self) -> usize { - self.parts.iter().flatten().map(|b| b.len()).sum() - } - } - - impl Body for SputteringBody { - type Data = Bytes; - type Error = aws_smithy_types::body::Error; - - fn poll_data( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - if self.cursor == self.parts.len() { - return Poll::Ready(None); - } - - let this = self.project(); - let delay_in_millis = *this.delay_in_millis; - let next_part = this.parts.get_mut(*this.cursor).unwrap().take(); - - match next_part { - None => { - *this.cursor += 1; - let waker = cx.waker().clone(); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(delay_in_millis)).await; - waker.wake(); - }); - Poll::Pending - } - Some(data) => { - *this.cursor += 1; - Poll::Ready(Some(Ok(data))) - } - } - } - - fn poll_trailers( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll>, Self::Error>> { - Poll::Ready(Ok(None)) - } - - fn is_end_stream(&self) -> bool { - false - } - - fn size_hint(&self) -> SizeHint { - SizeHint::new() - } - } - - #[tokio::test] - async fn test_aws_chunked_encoding() { - let test_fut = async { - let input_str = "Hello world"; - let opts = AwsChunkedBodyOptions::new(input_str.len() as u64, Vec::new()); - let mut body = AwsChunkedBody::new(SdkBody::from(input_str), opts); - - let mut output = SegmentedBuf::new(); - while let Some(buf) = body.data().await { - output.push(buf.unwrap()); - } - - let mut actual_output = String::new(); - output - .reader() - .read_to_string(&mut actual_output) - .expect("Doesn't cause IO errors"); - - let expected_output = "B\r\nHello world\r\n0\r\n\r\n"; - - assert_eq!(expected_output, actual_output); - assert!( - body.trailers() - .await - .expect("no errors occurred during trailer polling") - .is_none(), - "aws-chunked encoded bodies don't have normal HTTP trailers" - ); - - // You can insert a `tokio::time::sleep` here to verify the timeout works as intended - }; - - let timeout_duration = Duration::from_secs(3); - if tokio::time::timeout(timeout_duration, test_fut) - .await - .is_err() - { - panic!("test_aws_chunked_encoding timed out after {timeout_duration:?}"); - } - } - - #[tokio::test] - async fn test_aws_chunked_encoding_sputtering_body() { - let test_fut = async { - let input = SputteringBody { - parts: vec![ - Some(Bytes::from_static(b"chunk 1, ")), - None, - Some(Bytes::from_static(b"chunk 2, ")), - Some(Bytes::from_static(b"chunk 3, ")), - None, - None, - Some(Bytes::from_static(b"chunk 4, ")), - Some(Bytes::from_static(b"chunk 5, ")), - Some(Bytes::from_static(b"chunk 6")), - ], - cursor: 0, - delay_in_millis: 500, - }; - let opts = AwsChunkedBodyOptions::new(input.len() as u64, Vec::new()); - let mut body = AwsChunkedBody::new(input, opts); +/// Use aws_runtime::content_encoding::AwsChunkedBodyOption instead. +#[deprecated( + since = "0.60.2", + note = "Use aws_runtime::content_encoding::AwsChunkedBodyOptions instead." +)] +pub type AwsChunkedBodyOptions = aws_runtime::content_encoding::AwsChunkedBodyOptions; - let mut output = SegmentedBuf::new(); - while let Some(buf) = body.data().await { - output.push(buf.unwrap()); - } - - let mut actual_output = String::new(); - output - .reader() - .read_to_string(&mut actual_output) - .expect("Doesn't cause IO errors"); - - let expected_output = - "34\r\nchunk 1, chunk 2, chunk 3, chunk 4, chunk 5, chunk 6\r\n0\r\n\r\n"; - - assert_eq!(expected_output, actual_output); - assert!( - body.trailers() - .await - .expect("no errors occurred during trailer polling") - .is_none(), - "aws-chunked encoded bodies don't have normal HTTP trailers" - ); - }; - - let timeout_duration = Duration::from_secs(3); - if tokio::time::timeout(timeout_duration, test_fut) - .await - .is_err() - { - panic!( - "test_aws_chunked_encoding_sputtering_body timed out after {timeout_duration:?}" - ); - } - } - - #[tokio::test] - #[should_panic = "called `Result::unwrap()` on an `Err` value: ReportedTrailerLengthMismatch { actual: 44, expected: 0 }"] - async fn test_aws_chunked_encoding_incorrect_trailer_length_panic() { - let input_str = "Hello world"; - // Test body has no trailers, so this length is incorrect and will trigger an assert panic - // When the panic occurs, it will actually expect a length of 44. This is because, when using - // aws-chunked encoding, each trailer will end with a CRLF which is 2 bytes long. - let wrong_trailer_len = 42; - let opts = AwsChunkedBodyOptions::new(input_str.len() as u64, vec![wrong_trailer_len]); - let mut body = AwsChunkedBody::new(SdkBody::from(input_str), opts); - - // We don't care about the body contents but we have to read it all before checking for trailers - while let Some(buf) = body.data().await { - drop(buf.unwrap()); - } - - assert!( - body.trailers() - .await - .expect("no errors occurred during trailer polling") - .is_none(), - "aws-chunked encoded bodies don't have normal HTTP trailers" - ); - } - - #[tokio::test] - async fn test_aws_chunked_encoding_empty_body() { - let input_str = ""; - let opts = AwsChunkedBodyOptions::new(input_str.len() as u64, Vec::new()); - let mut body = AwsChunkedBody::new(SdkBody::from(input_str), opts); - - let mut output = SegmentedBuf::new(); - while let Some(buf) = body.data().await { - output.push(buf.unwrap()); - } - - let mut actual_output = String::new(); - output - .reader() - .read_to_string(&mut actual_output) - .expect("Doesn't cause IO errors"); - - let expected_output = [CHUNK_TERMINATOR, CRLF].concat(); - - assert_eq!(expected_output, actual_output); - assert!( - body.trailers() - .await - .expect("no errors occurred during trailer polling") - .is_none(), - "aws-chunked encoded bodies don't have normal HTTP trailers" - ); - } - - #[tokio::test] - async fn test_total_rendered_length_of_trailers() { - let mut trailers = HeaderMap::new(); - - trailers.insert("empty_value", HeaderValue::from_static("")); - - trailers.insert("single_value", HeaderValue::from_static("value 1")); - - trailers.insert("two_values", HeaderValue::from_static("value 1")); - trailers.append("two_values", HeaderValue::from_static("value 2")); - - trailers.insert("three_values", HeaderValue::from_static("value 1")); - trailers.append("three_values", HeaderValue::from_static("value 2")); - trailers.append("three_values", HeaderValue::from_static("value 3")); - - let trailers = Some(trailers); - let actual_length = total_rendered_length_of_trailers(trailers.as_ref()); - let expected_length = (trailers_as_aws_chunked_bytes(trailers, actual_length).len()) as u64; - - assert_eq!(expected_length, actual_length); - } - - #[tokio::test] - async fn test_total_rendered_length_of_empty_trailers() { - let trailers = Some(HeaderMap::new()); - let actual_length = total_rendered_length_of_trailers(trailers.as_ref()); - let expected_length = (trailers_as_aws_chunked_bytes(trailers, actual_length).len()) as u64; - - assert_eq!(expected_length, actual_length); - } -} +/// Use aws_runtime::content_encoding::AwsChunkedBody instead. +#[deprecated( + since = "0.60.2", + note = "Use aws_runtime::content_encoding::AwsChunkedBody instead." +)] +pub type AwsChunkedBody = aws_runtime::content_encoding::AwsChunkedBody; diff --git a/aws/rust-runtime/aws-http/src/user_agent.rs b/aws/rust-runtime/aws-http/src/user_agent.rs index 9d9272372..f68ecf91b 100644 --- a/aws/rust-runtime/aws-http/src/user_agent.rs +++ b/aws/rust-runtime/aws-http/src/user_agent.rs @@ -3,736 +3,51 @@ * SPDX-License-Identifier: Apache-2.0 */ -use aws_smithy_types::config_bag::{Storable, StoreReplace}; -use aws_types::app_name::AppName; -use aws_types::build_metadata::{OsFamily, BUILD_METADATA}; -use aws_types::os_shim_internal::Env; -use std::borrow::Cow; -use std::error::Error; -use std::fmt; - -/// AWS User Agent -/// -/// Ths struct should be inserted into the [`ConfigBag`](aws_smithy_types::config_bag::ConfigBag) -/// during operation construction. The `UserAgentInterceptor` reads `AwsUserAgent` -/// from the config bag and sets the `User-Agent` and `x-amz-user-agent` headers. -#[derive(Clone, Debug)] -pub struct AwsUserAgent { - sdk_metadata: SdkMetadata, - api_metadata: ApiMetadata, - os_metadata: OsMetadata, - language_metadata: LanguageMetadata, - exec_env_metadata: Option, - feature_metadata: Vec, - config_metadata: Vec, - framework_metadata: Vec, - app_name: Option, - build_env_additional_metadata: Option, -} - -impl AwsUserAgent { - /// Load a User Agent configuration from the environment - /// - /// This utilizes [`BUILD_METADATA`](const@aws_types::build_metadata::BUILD_METADATA) from `aws_types` - /// to capture the Rust version & target platform. `ApiMetadata` provides - /// the version & name of the specific service. - pub fn new_from_environment(env: Env, api_metadata: ApiMetadata) -> Self { - let build_metadata = &BUILD_METADATA; - let sdk_metadata = SdkMetadata { - name: "rust", - version: build_metadata.core_pkg_version, - }; - let os_metadata = OsMetadata { - os_family: &build_metadata.os_family, - version: None, - }; - let exec_env_metadata = env - .get("AWS_EXECUTION_ENV") - .ok() - .map(|name| ExecEnvMetadata { name }); - - // Retrieve additional metadata at compile-time from the AWS_SDK_RUST_BUILD_UA_METADATA env var - let build_env_additional_metadata = option_env!("AWS_SDK_RUST_BUILD_UA_METADATA") - .and_then(|value| AdditionalMetadata::new(value).ok()); - - AwsUserAgent { - sdk_metadata, - api_metadata, - os_metadata, - language_metadata: LanguageMetadata { - lang: "rust", - version: BUILD_METADATA.rust_version, - extras: Default::default(), - }, - exec_env_metadata, - feature_metadata: Default::default(), - config_metadata: Default::default(), - framework_metadata: Default::default(), - app_name: Default::default(), - build_env_additional_metadata, - } - } - - /// For test purposes, construct an environment-independent User Agent - /// - /// Without this, running CI on a different platform would produce different user agent strings - pub fn for_tests() -> Self { - Self { - sdk_metadata: SdkMetadata { - name: "rust", - version: "0.123.test", - }, - api_metadata: ApiMetadata { - service_id: "test-service".into(), - version: "0.123", - }, - os_metadata: OsMetadata { - os_family: &OsFamily::Windows, - version: Some("XPSP3".to_string()), - }, - language_metadata: LanguageMetadata { - lang: "rust", - version: "1.50.0", - extras: Default::default(), - }, - exec_env_metadata: None, - feature_metadata: Vec::new(), - config_metadata: Vec::new(), - framework_metadata: Vec::new(), - app_name: None, - build_env_additional_metadata: None, - } - } - - #[doc(hidden)] - /// Adds feature metadata to the user agent. - pub fn with_feature_metadata(mut self, metadata: FeatureMetadata) -> Self { - self.feature_metadata.push(metadata); - self - } - - #[doc(hidden)] - /// Adds feature metadata to the user agent. - pub fn add_feature_metadata(&mut self, metadata: FeatureMetadata) -> &mut Self { - self.feature_metadata.push(metadata); - self - } - - #[doc(hidden)] - /// Adds config metadata to the user agent. - pub fn with_config_metadata(mut self, metadata: ConfigMetadata) -> Self { - self.config_metadata.push(metadata); - self - } - - #[doc(hidden)] - /// Adds config metadata to the user agent. - pub fn add_config_metadata(&mut self, metadata: ConfigMetadata) -> &mut Self { - self.config_metadata.push(metadata); - self - } - - #[doc(hidden)] - /// Adds framework metadata to the user agent. - pub fn with_framework_metadata(mut self, metadata: FrameworkMetadata) -> Self { - self.framework_metadata.push(metadata); - self - } - - #[doc(hidden)] - /// Adds framework metadata to the user agent. - pub fn add_framework_metadata(&mut self, metadata: FrameworkMetadata) -> &mut Self { - self.framework_metadata.push(metadata); - self - } - - /// Sets the app name for the user agent. - pub fn with_app_name(mut self, app_name: AppName) -> Self { - self.app_name = Some(app_name); - self - } - - /// Sets the app name for the user agent. - pub fn set_app_name(&mut self, app_name: AppName) -> &mut Self { - self.app_name = Some(app_name); - self - } - - /// Generate a new-style user agent style header - /// - /// This header should be set at `x-amz-user-agent` - pub fn aws_ua_header(&self) -> String { - /* - ABNF for the user agent (see the bottom of the file for complete ABNF): - ua-string = sdk-metadata RWS - [api-metadata RWS] - os-metadata RWS - language-metadata RWS - [env-metadata RWS] - *(feat-metadata RWS) - *(config-metadata RWS) - *(framework-metadata RWS) - [appId] - */ - let mut ua_value = String::new(); - use std::fmt::Write; - // unwrap calls should never fail because string formatting will always succeed. - write!(ua_value, "{} ", &self.sdk_metadata).unwrap(); - write!(ua_value, "{} ", &self.api_metadata).unwrap(); - write!(ua_value, "{} ", &self.os_metadata).unwrap(); - write!(ua_value, "{} ", &self.language_metadata).unwrap(); - if let Some(ref env_meta) = self.exec_env_metadata { - write!(ua_value, "{} ", env_meta).unwrap(); - } - for feature in &self.feature_metadata { - write!(ua_value, "{} ", feature).unwrap(); - } - for config in &self.config_metadata { - write!(ua_value, "{} ", config).unwrap(); - } - for framework in &self.framework_metadata { - write!(ua_value, "{} ", framework).unwrap(); - } - if let Some(app_name) = &self.app_name { - write!(ua_value, "app/{}", app_name).unwrap(); - } - if let Some(additional_metadata) = &self.build_env_additional_metadata { - write!(ua_value, "{}", additional_metadata).unwrap(); - } - if ua_value.ends_with(' ') { - ua_value.truncate(ua_value.len() - 1); - } - ua_value - } - - /// Generate an old-style User-Agent header for backward compatibility - /// - /// This header is intended to be set at `User-Agent` - pub fn ua_header(&self) -> String { - let mut ua_value = String::new(); - use std::fmt::Write; - write!(ua_value, "{} ", &self.sdk_metadata).unwrap(); - write!(ua_value, "{} ", &self.os_metadata).unwrap(); - write!(ua_value, "{}", &self.language_metadata).unwrap(); - ua_value - } -} - -impl Storable for AwsUserAgent { - type Storer = StoreReplace; -} - -#[derive(Clone, Copy, Debug)] -struct SdkMetadata { - name: &'static str, - version: &'static str, -} - -impl fmt::Display for SdkMetadata { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "aws-sdk-{}/{}", self.name, self.version) - } -} - -/// Metadata about the client that's making the call. -#[derive(Clone, Debug)] -pub struct ApiMetadata { - service_id: Cow<'static, str>, - version: &'static str, -} - -impl ApiMetadata { - /// Creates new `ApiMetadata`. - pub const fn new(service_id: &'static str, version: &'static str) -> Self { - Self { - service_id: Cow::Borrowed(service_id), - version, - } - } -} - -impl fmt::Display for ApiMetadata { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "api/{}/{}", self.service_id, self.version) - } -} - -impl Storable for ApiMetadata { - type Storer = StoreReplace; -} - -/// Error for when an user agent metadata doesn't meet character requirements. -/// -/// Metadata may only have alphanumeric characters and any of these characters: -/// ```text -/// !#$%&'*+-.^_`|~ -/// ``` -/// Spaces are not allowed. -#[derive(Debug)] -#[non_exhaustive] -pub struct InvalidMetadataValue; - -impl Error for InvalidMetadataValue {} - -impl fmt::Display for InvalidMetadataValue { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "User agent metadata can only have alphanumeric characters, or any of \ - '!' | '#' | '$' | '%' | '&' | '\\'' | '*' | '+' | '-' | \ - '.' | '^' | '_' | '`' | '|' | '~'" - ) - } -} - -fn validate_metadata(value: Cow<'static, str>) -> Result, InvalidMetadataValue> { - fn valid_character(c: char) -> bool { - match c { - _ if c.is_ascii_alphanumeric() => true, - '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|' - | '~' => true, - _ => false, - } - } - if !value.chars().all(valid_character) { - return Err(InvalidMetadataValue); - } - Ok(value) -} - -#[doc(hidden)] -/// Additional metadata that can be bundled with framework or feature metadata. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub struct AdditionalMetadata { - value: Cow<'static, str>, -} - -impl AdditionalMetadata { - /// Creates `AdditionalMetadata`. - /// - /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or - /// has characters other than the following: - /// ```text - /// !#$%&'*+-.^_`|~ - /// ``` - pub fn new(value: impl Into>) -> Result { - Ok(Self { - value: validate_metadata(value.into())?, - }) - } -} - -impl fmt::Display for AdditionalMetadata { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // additional-metadata = "md/" ua-pair - write!(f, "md/{}", self.value) - } -} - -#[derive(Clone, Debug, Default)] -struct AdditionalMetadataList(Vec); - -impl AdditionalMetadataList { - fn push(&mut self, metadata: AdditionalMetadata) { - self.0.push(metadata); - } -} - -impl fmt::Display for AdditionalMetadataList { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for metadata in &self.0 { - write!(f, " {}", metadata)?; - } - Ok(()) - } -} - -#[doc(hidden)] -/// Metadata about a feature that is being used in the SDK. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub struct FeatureMetadata { - name: Cow<'static, str>, - version: Option>, - additional: AdditionalMetadataList, -} - -impl FeatureMetadata { - /// Creates `FeatureMetadata`. - /// - /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or - /// has characters other than the following: - /// ```text - /// !#$%&'*+-.^_`|~ - /// ``` - pub fn new( - name: impl Into>, - version: Option>, - ) -> Result { - Ok(Self { - name: validate_metadata(name.into())?, - version: version.map(validate_metadata).transpose()?, - additional: Default::default(), - }) - } - - /// Bundles additional arbitrary metadata with this feature metadata. - pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self { - self.additional.push(metadata); - self - } -} - -impl fmt::Display for FeatureMetadata { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // feat-metadata = "ft/" name ["/" version] *(RWS additional-metadata) - if let Some(version) = &self.version { - write!(f, "ft/{}/{}{}", self.name, version, self.additional) - } else { - write!(f, "ft/{}{}", self.name, self.additional) - } - } -} - -#[doc(hidden)] -/// Metadata about a config value that is being used in the SDK. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub struct ConfigMetadata { - config: Cow<'static, str>, - value: Option>, -} - -impl ConfigMetadata { - /// Creates `ConfigMetadata`. - /// - /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or - /// has characters other than the following: - /// ```text - /// !#$%&'*+-.^_`|~ - /// ``` - pub fn new( - config: impl Into>, - value: Option>, - ) -> Result { - Ok(Self { - config: validate_metadata(config.into())?, - value: value.map(validate_metadata).transpose()?, - }) - } -} - -impl fmt::Display for ConfigMetadata { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // config-metadata = "cfg/" config ["/" value] - if let Some(value) = &self.value { - write!(f, "cfg/{}/{}", self.config, value) - } else { - write!(f, "cfg/{}", self.config) - } - } -} - -#[doc(hidden)] -/// Metadata about a software framework that is being used with the SDK. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub struct FrameworkMetadata { - name: Cow<'static, str>, - version: Option>, - additional: AdditionalMetadataList, -} - -impl FrameworkMetadata { - /// Creates `FrameworkMetadata`. - /// - /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or - /// has characters other than the following: - /// ```text - /// !#$%&'*+-.^_`|~ - /// ``` - pub fn new( - name: impl Into>, - version: Option>, - ) -> Result { - Ok(Self { - name: validate_metadata(name.into())?, - version: version.map(validate_metadata).transpose()?, - additional: Default::default(), - }) - } - - /// Bundles additional arbitrary metadata with this framework metadata. - pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self { - self.additional.push(metadata); - self - } -} - -impl fmt::Display for FrameworkMetadata { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // framework-metadata = "lib/" name ["/" version] *(RWS additional-metadata) - if let Some(version) = &self.version { - write!(f, "lib/{}/{}{}", self.name, version, self.additional) - } else { - write!(f, "lib/{}{}", self.name, self.additional) - } - } -} - -#[derive(Clone, Debug)] -struct OsMetadata { - os_family: &'static OsFamily, - version: Option, -} - -impl fmt::Display for OsMetadata { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let os_family = match self.os_family { - OsFamily::Windows => "windows", - OsFamily::Linux => "linux", - OsFamily::Macos => "macos", - OsFamily::Android => "android", - OsFamily::Ios => "ios", - OsFamily::Other => "other", - }; - write!(f, "os/{}", os_family)?; - if let Some(ref version) = self.version { - write!(f, "/{}", version)?; - } - Ok(()) - } -} - -#[derive(Clone, Debug)] -struct LanguageMetadata { - lang: &'static str, - version: &'static str, - extras: AdditionalMetadataList, -} -impl fmt::Display for LanguageMetadata { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // language-metadata = "lang/" language "/" version *(RWS additional-metadata) - write!(f, "lang/{}/{}{}", self.lang, self.version, self.extras) - } -} - -#[derive(Clone, Debug)] -struct ExecEnvMetadata { - name: String, -} -impl fmt::Display for ExecEnvMetadata { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "exec-env/{}", &self.name) - } -} - -#[cfg(test)] -mod test { - use super::*; - use aws_types::app_name::AppName; - use aws_types::build_metadata::OsFamily; - use aws_types::os_shim_internal::Env; - use std::borrow::Cow; - - fn make_deterministic(ua: &mut AwsUserAgent) { - // hard code some variable things for a deterministic test - ua.sdk_metadata.version = "0.1"; - ua.language_metadata.version = "1.50.0"; - ua.os_metadata.os_family = &OsFamily::Macos; - ua.os_metadata.version = Some("1.15".to_string()); - } - - #[test] - fn generate_a_valid_ua() { - let api_metadata = ApiMetadata { - service_id: "dynamodb".into(), - version: "123", - }; - let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata); - make_deterministic(&mut ua); - assert_eq!( - ua.aws_ua_header(), - "aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0" - ); - assert_eq!( - ua.ua_header(), - "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" - ); - } - - #[test] - fn generate_a_valid_ua_with_execution_env() { - let api_metadata = ApiMetadata { - service_id: "dynamodb".into(), - version: "123", - }; - let mut ua = AwsUserAgent::new_from_environment( - Env::from_slice(&[("AWS_EXECUTION_ENV", "lambda")]), - api_metadata, - ); - make_deterministic(&mut ua); - assert_eq!( - ua.aws_ua_header(), - "aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda" - ); - assert_eq!( - ua.ua_header(), - "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" - ); - } - - #[test] - fn generate_a_valid_ua_with_features() { - let api_metadata = ApiMetadata { - service_id: "dynamodb".into(), - version: "123", - }; - let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata) - .with_feature_metadata( - FeatureMetadata::new("test-feature", Some(Cow::Borrowed("1.0"))).unwrap(), - ) - .with_feature_metadata( - FeatureMetadata::new("other-feature", None) - .unwrap() - .with_additional(AdditionalMetadata::new("asdf").unwrap()), - ); - make_deterministic(&mut ua); - assert_eq!( - ua.aws_ua_header(), - "aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 ft/test-feature/1.0 ft/other-feature md/asdf" - ); - assert_eq!( - ua.ua_header(), - "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" - ); - } - - #[test] - fn generate_a_valid_ua_with_config() { - let api_metadata = ApiMetadata { - service_id: "dynamodb".into(), - version: "123", - }; - let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata) - .with_config_metadata( - ConfigMetadata::new("some-config", Some(Cow::Borrowed("5"))).unwrap(), - ) - .with_config_metadata(ConfigMetadata::new("other-config", None).unwrap()); - make_deterministic(&mut ua); - assert_eq!( - ua.aws_ua_header(), - "aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 cfg/some-config/5 cfg/other-config" - ); - assert_eq!( - ua.ua_header(), - "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" - ); - } - - #[test] - fn generate_a_valid_ua_with_frameworks() { - let api_metadata = ApiMetadata { - service_id: "dynamodb".into(), - version: "123", - }; - let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata) - .with_framework_metadata( - FrameworkMetadata::new("some-framework", Some(Cow::Borrowed("1.3"))) - .unwrap() - .with_additional(AdditionalMetadata::new("something").unwrap()), - ) - .with_framework_metadata(FrameworkMetadata::new("other", None).unwrap()); - make_deterministic(&mut ua); - assert_eq!( - ua.aws_ua_header(), - "aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 lib/some-framework/1.3 md/something lib/other" - ); - assert_eq!( - ua.ua_header(), - "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" - ); - } - - #[test] - fn generate_a_valid_ua_with_app_name() { - let api_metadata = ApiMetadata { - service_id: "dynamodb".into(), - version: "123", - }; - let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata) - .with_app_name(AppName::new("my_app").unwrap()); - make_deterministic(&mut ua); - assert_eq!( - ua.aws_ua_header(), - "aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app" - ); - assert_eq!( - ua.ua_header(), - "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" - ); - } - - #[test] - fn generate_a_valid_ua_with_build_env_additional_metadata() { - let mut ua = AwsUserAgent::for_tests(); - ua.build_env_additional_metadata = Some(AdditionalMetadata::new("asdf").unwrap()); - assert_eq!( - ua.aws_ua_header(), - "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf" - ); - assert_eq!( - ua.ua_header(), - "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0" - ); - } -} - -/* -Appendix: User Agent ABNF -sdk-ua-header = "x-amz-user-agent:" OWS ua-string OWS -ua-pair = ua-name ["/" ua-value] -ua-name = token -ua-value = token -version = token -name = token -service-id = token -sdk-name = java / ruby / php / dotnet / python / cli / kotlin / rust / js / cpp / go / go-v2 -os-family = windows / linux / macos / android / ios / other -config = retry-mode -additional-metadata = "md/" ua-pair -sdk-metadata = "aws-sdk-" sdk-name "/" version -api-metadata = "api/" service-id "/" version -os-metadata = "os/" os-family ["/" version] -language-metadata = "lang/" language "/" version *(RWS additional-metadata) -env-metadata = "exec-env/" name -feat-metadata = "ft/" name ["/" version] *(RWS additional-metadata) -config-metadata = "cfg/" config ["/" value] -framework-metadata = "lib/" name ["/" version] *(RWS additional-metadata) -app-id = "app/" name -build-env-additional-metadata = "md/" value -ua-string = sdk-metadata RWS - [api-metadata RWS] - os-metadata RWS - language-metadata RWS - [env-metadata RWS] - *(feat-metadata RWS) - *(config-metadata RWS) - *(framework-metadata RWS) - [app-id] - [build-env-additional-metadata] - -# New metadata field might be added in the future and they must follow this format -prefix = token -metadata = prefix "/" ua-pair - -# token, RWS and OWS are defined in [RFC 7230](https://tools.ietf.org/html/rfc7230) -OWS = *( SP / HTAB ) - ; optional whitespace -RWS = 1*( SP / HTAB ) - ; required whitespace -token = 1*tchar -tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / - "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA -*/ +/// Use aws_runtime::user_agent::AwsUserAgent instead. +#[deprecated( + since = "0.60.2", + note = "Use aws_runtime::user_agent::AwsUserAgent instead." +)] +pub type AwsUserAgent = aws_runtime::user_agent::AwsUserAgent; + +/// Use aws_runtime::user_agent::ApiMetadata instead. +#[deprecated( + since = "0.60.2", + note = "Use aws_runtime::user_agent::ApiMetadata instead." +)] +pub type ApiMetadata = aws_runtime::user_agent::ApiMetadata; + +/// Use aws_runtime::user_agent::InvalidMetadataValue instead. +#[deprecated( + since = "0.60.2", + note = "Use aws_runtime::user_agent::InvalidMetadataValue instead." +)] +pub type InvalidMetadataValue = aws_runtime::user_agent::InvalidMetadataValue; + +/// Use aws_runtime::user_agent::AdditionalMetadata instead. +#[deprecated( + since = "0.60.2", + note = "Use aws_runtime::user_agent::AdditionalMetadata instead." +)] +pub type AdditionalMetadata = aws_runtime::user_agent::AdditionalMetadata; + +/// Use aws_runtime::user_agent::FeatureMetadata instead. +#[deprecated( + since = "0.60.2", + note = "Use aws_runtime::user_agent::FeatureMetadata instead." +)] +pub type FeatureMetadata = aws_runtime::user_agent::FeatureMetadata; + +/// Use aws_runtime::user_agent::ConfigMetadata instead. +#[deprecated( + since = "0.60.2", + note = "Use aws_runtime::user_agent::ConfigMetadata instead." +)] +pub type ConfigMetadata = aws_runtime::user_agent::ConfigMetadata; + +/// Use aws_runtime::user_agent::FrameworkMetadata instead. +#[deprecated( + since = "0.60.2", + note = "Use aws_runtime::user_agent::FrameworkMetadata instead." +)] +pub type FrameworkMetadata = aws_runtime::user_agent::FrameworkMetadata; diff --git a/aws/rust-runtime/aws-inlineable/Cargo.toml b/aws/rust-runtime/aws-inlineable/Cargo.toml index 514aa0c2d..85b6c440c 100644 --- a/aws/rust-runtime/aws-inlineable/Cargo.toml +++ b/aws/rust-runtime/aws-inlineable/Cargo.toml @@ -13,8 +13,7 @@ repository = "https://github.com/smithy-lang/smithy-rs" [dependencies] aws-credential-types = { path = "../aws-credential-types" } -aws-http = { path = "../aws-http" } -aws-runtime = { path = "../aws-runtime" } +aws-runtime = { path = "../aws-runtime", features = ["http-02x"] } aws-sigv4 = { path = "../aws-sigv4" } aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["rt-tokio"] } aws-smithy-checksums = { path = "../../../rust-runtime/aws-smithy-checksums" } diff --git a/aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs b/aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs index ef07f2c34..2ccfc5d28 100644 --- a/aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs +++ b/aws/rust-runtime/aws-inlineable/src/http_request_checksum.rs @@ -7,8 +7,8 @@ //! Interceptor for handling Smithy `@httpChecksum` request checksumming with AWS SigV4 -use aws_http::content_encoding::{AwsChunkedBody, AwsChunkedBodyOptions}; -use aws_runtime::auth::SigV4OperationSigningConfig; +use aws_runtime::content_encoding::{AwsChunkedBody, AwsChunkedBodyOptions}; +use aws_runtime::{auth::SigV4OperationSigningConfig, content_encoding::header_value::AWS_CHUNKED}; use aws_sigv4::http_request::SignableBody; use aws_smithy_checksums::ChecksumAlgorithm; use aws_smithy_checksums::{body::calculate, http::HttpChecksum}; @@ -199,7 +199,7 @@ fn wrap_streaming_request_body_in_checksum_calculating_body( ); headers.insert( http::header::CONTENT_ENCODING, - HeaderValue::from_str(aws_http::content_encoding::header_value::AWS_CHUNKED) + HeaderValue::from_str(AWS_CHUNKED) .map_err(BuildError::other) .expect("\"aws-chunked\" will always be a valid HeaderValue"), ); diff --git a/aws/rust-runtime/aws-runtime/Cargo.toml b/aws/rust-runtime/aws-runtime/Cargo.toml index 4ba2423b9..a436ab872 100644 --- a/aws/rust-runtime/aws-runtime/Cargo.toml +++ b/aws/rust-runtime/aws-runtime/Cargo.toml @@ -9,12 +9,12 @@ repository = "https://github.com/smithy-lang/smithy-rs" [features] event-stream = ["dep:aws-smithy-eventstream", "aws-sigv4/sign-eventstream"] +http-02x = [] test-util = [] sigv4a = ["aws-sigv4/sigv4a"] [dependencies] aws-credential-types = { path = "../aws-credential-types" } -aws-http = { path = "../aws-http" } # TODO(httpRefactor): Remove the http0-compat feature aws-sigv4 = { path = "../aws-sigv4", features = ["http0-compat"] } aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async" } @@ -23,21 +23,26 @@ aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" } aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api", features = ["client"] } aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types" } aws-types = { path = "../aws-types" } +bytes = "1.1" fastrand = "2.0.0" http = "0.2.3" +http-body = "0.4.5" percent-encoding = "2.1.0" +pin-project-lite = "0.2.9" tracing = "0.1" uuid = { version = "1" } [dev-dependencies] aws-credential-types = { path = "../aws-credential-types", features = ["test-util"] } aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["test-util"] } -aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types", features = ["test-util"] } aws-smithy-protocol-test = { path = "../../../rust-runtime/aws-smithy-protocol-test" } aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api", features = ["test-util"] } +aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types", features = ["test-util"] } +bytes-utils = "0.1.2" proptest = "1.2" serde = { version = "1", features = ["derive"]} serde_json = "1" +tokio = { version = "1.23.1", features = ["macros", "rt", "time"] } tracing-test = "0.2.4" [package.metadata.docs.rs] diff --git a/aws/rust-runtime/aws-runtime/external-types.toml b/aws/rust-runtime/aws-runtime/external-types.toml index 941f09767..d42e6a361 100644 --- a/aws/rust-runtime/aws-runtime/external-types.toml +++ b/aws/rust-runtime/aws-runtime/external-types.toml @@ -3,4 +3,8 @@ allowed_external_types = [ "aws_smithy_types::*", "aws_smithy_runtime_api::*", "aws_types::*", + "bytes::bytes::Bytes", + + # Used by the aws-chunked implementation + "http_body::Body", ] diff --git a/aws/rust-runtime/aws-runtime/src/content_encoding.rs b/aws/rust-runtime/aws-runtime/src/content_encoding.rs new file mode 100644 index 000000000..2c00cff9c --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/content_encoding.rs @@ -0,0 +1,613 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use bytes::{Bytes, BytesMut}; +use http::{HeaderMap, HeaderValue}; +use http_body::{Body, SizeHint}; +use pin_project_lite::pin_project; + +use std::pin::Pin; +use std::task::{Context, Poll}; + +const CRLF: &str = "\r\n"; +const CHUNK_TERMINATOR: &str = "0\r\n"; +const TRAILER_SEPARATOR: &[u8] = b":"; + +/// Content encoding header value constants +pub mod header_value { + /// Header value denoting "aws-chunked" encoding + pub const AWS_CHUNKED: &str = "aws-chunked"; +} + +/// Options used when constructing an [`AwsChunkedBody`]. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct AwsChunkedBodyOptions { + /// The total size of the stream. Because we only support unsigned encoding + /// this implies that there will only be a single chunk containing the + /// underlying payload. + stream_length: u64, + /// The length of each trailer sent within an `AwsChunkedBody`. Necessary in + /// order to correctly calculate the total size of the body accurately. + trailer_lengths: Vec, +} + +impl AwsChunkedBodyOptions { + /// Create a new [`AwsChunkedBodyOptions`]. + pub fn new(stream_length: u64, trailer_lengths: Vec) -> Self { + Self { + stream_length, + trailer_lengths, + } + } + + fn total_trailer_length(&self) -> u64 { + self.trailer_lengths.iter().sum::() + // We need to account for a CRLF after each trailer name/value pair + + (self.trailer_lengths.len() * CRLF.len()) as u64 + } + + /// Set a trailer len + pub fn with_trailer_len(mut self, trailer_len: u64) -> Self { + self.trailer_lengths.push(trailer_len); + self + } +} + +#[derive(Debug, PartialEq, Eq)] +enum AwsChunkedBodyState { + /// Write out the size of the chunk that will follow. Then, transition into the + /// `WritingChunk` state. + WritingChunkSize, + /// Write out the next chunk of data. Multiple polls of the inner body may need to occur before + /// all data is written out. Once there is no more data to write, transition into the + /// `WritingTrailers` state. + WritingChunk, + /// Write out all trailers associated with this `AwsChunkedBody` and then transition into the + /// `Closed` state. + WritingTrailers, + /// This is the final state. Write out the body terminator and then remain in this state. + Closed, +} + +pin_project! { + /// A request body compatible with `Content-Encoding: aws-chunked`. This implementation is only + /// capable of writing a single chunk and does not support signed chunks. + /// + /// Chunked-Body grammar is defined in [ABNF] as: + /// + /// ```txt + /// Chunked-Body = *chunk + /// last-chunk + /// chunked-trailer + /// CRLF + /// + /// chunk = chunk-size CRLF chunk-data CRLF + /// chunk-size = 1*HEXDIG + /// last-chunk = 1*("0") CRLF + /// chunked-trailer = *( entity-header CRLF ) + /// entity-header = field-name ":" OWS field-value OWS + /// ``` + /// For more info on what the abbreviations mean, see https://datatracker.ietf.org/doc/html/rfc7230#section-1.2 + /// + /// [ABNF]:https://en.wikipedia.org/wiki/Augmented_Backus%E2%80%93Naur_form + #[derive(Debug)] + pub struct AwsChunkedBody { + #[pin] + inner: InnerBody, + #[pin] + state: AwsChunkedBodyState, + options: AwsChunkedBodyOptions, + inner_body_bytes_read_so_far: usize, + } +} + +impl AwsChunkedBody { + /// Wrap the given body in an outer body compatible with `Content-Encoding: aws-chunked` + pub fn new(body: Inner, options: AwsChunkedBodyOptions) -> Self { + Self { + inner: body, + state: AwsChunkedBodyState::WritingChunkSize, + options, + inner_body_bytes_read_so_far: 0, + } + } + + fn encoded_length(&self) -> u64 { + let mut length = 0; + if self.options.stream_length != 0 { + length += get_unsigned_chunk_bytes_length(self.options.stream_length); + } + + // End chunk + length += CHUNK_TERMINATOR.len() as u64; + + // Trailers + for len in self.options.trailer_lengths.iter() { + length += len + CRLF.len() as u64; + } + + // Encoding terminator + length += CRLF.len() as u64; + + length + } +} + +fn get_unsigned_chunk_bytes_length(payload_length: u64) -> u64 { + let hex_repr_len = int_log16(payload_length); + hex_repr_len + CRLF.len() as u64 + payload_length + CRLF.len() as u64 +} + +/// Writes trailers out into a `string` and then converts that `String` to a `Bytes` before +/// returning. +/// +/// - Trailer names are separated by a single colon only, no space. +/// - Trailer names with multiple values will be written out one line per value, with the name +/// appearing on each line. +fn trailers_as_aws_chunked_bytes( + trailer_map: Option, + estimated_length: u64, +) -> BytesMut { + if let Some(trailer_map) = trailer_map { + let mut current_header_name = None; + let mut trailers = BytesMut::with_capacity(estimated_length.try_into().unwrap_or_default()); + + for (header_name, header_value) in trailer_map.into_iter() { + // When a header has multiple values, the name only comes up in iteration the first time + // we see it. Therefore, we need to keep track of the last name we saw and fall back to + // it when `header_name == None`. + current_header_name = header_name.or(current_header_name); + + // In practice, this will always exist, but `if let` is nicer than unwrap + if let Some(header_name) = current_header_name.as_ref() { + trailers.extend_from_slice(header_name.as_ref()); + trailers.extend_from_slice(TRAILER_SEPARATOR); + trailers.extend_from_slice(header_value.as_bytes()); + trailers.extend_from_slice(CRLF.as_bytes()); + } + } + + trailers + } else { + BytesMut::new() + } +} + +/// Given an optional `HeaderMap`, calculate the total number of bytes required to represent the +/// `HeaderMap`. If no `HeaderMap` is given as input, return 0. +/// +/// - Trailer names are separated by a single colon only, no space. +/// - Trailer names with multiple values will be written out one line per value, with the name +/// appearing on each line. +fn total_rendered_length_of_trailers(trailer_map: Option<&HeaderMap>) -> u64 { + match trailer_map { + Some(trailer_map) => trailer_map + .iter() + .map(|(trailer_name, trailer_value)| { + trailer_name.as_str().len() + + TRAILER_SEPARATOR.len() + + trailer_value.len() + + CRLF.len() + }) + .sum::() as u64, + None => 0, + } +} + +impl Body for AwsChunkedBody +where + Inner: Body, +{ + type Data = Bytes; + type Error = aws_smithy_types::body::Error; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + tracing::trace!(state = ?self.state, "polling AwsChunkedBody"); + let mut this = self.project(); + + match *this.state { + AwsChunkedBodyState::WritingChunkSize => { + if this.options.stream_length == 0 { + // If the stream is empty, we skip to writing trailers after writing the CHUNK_TERMINATOR. + *this.state = AwsChunkedBodyState::WritingTrailers; + tracing::trace!("stream is empty, writing chunk terminator"); + Poll::Ready(Some(Ok(Bytes::from([CHUNK_TERMINATOR].concat())))) + } else { + *this.state = AwsChunkedBodyState::WritingChunk; + // A chunk must be prefixed by chunk size in hexadecimal + let chunk_size = format!("{:X?}{CRLF}", this.options.stream_length); + tracing::trace!(%chunk_size, "writing chunk size"); + let chunk_size = Bytes::from(chunk_size); + Poll::Ready(Some(Ok(chunk_size))) + } + } + AwsChunkedBodyState::WritingChunk => match this.inner.poll_data(cx) { + Poll::Ready(Some(Ok(data))) => { + tracing::trace!(len = data.len(), "writing chunk data"); + *this.inner_body_bytes_read_so_far += data.len(); + Poll::Ready(Some(Ok(data))) + } + Poll::Ready(None) => { + let actual_stream_length = *this.inner_body_bytes_read_so_far as u64; + let expected_stream_length = this.options.stream_length; + if actual_stream_length != expected_stream_length { + let err = Box::new(AwsChunkedBodyError::StreamLengthMismatch { + actual: actual_stream_length, + expected: expected_stream_length, + }); + return Poll::Ready(Some(Err(err))); + }; + + tracing::trace!("no more chunk data, writing CRLF and chunk terminator"); + *this.state = AwsChunkedBodyState::WritingTrailers; + // Since we wrote chunk data, we end it with a CRLF and since we only write + // a single chunk, we write the CHUNK_TERMINATOR immediately after + Poll::Ready(Some(Ok(Bytes::from([CRLF, CHUNK_TERMINATOR].concat())))) + } + Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))), + Poll::Pending => Poll::Pending, + }, + AwsChunkedBodyState::WritingTrailers => { + return match this.inner.poll_trailers(cx) { + Poll::Ready(Ok(trailers)) => { + *this.state = AwsChunkedBodyState::Closed; + let expected_length = total_rendered_length_of_trailers(trailers.as_ref()); + let actual_length = this.options.total_trailer_length(); + + if expected_length != actual_length { + let err = + Box::new(AwsChunkedBodyError::ReportedTrailerLengthMismatch { + actual: actual_length, + expected: expected_length, + }); + return Poll::Ready(Some(Err(err))); + } + + let mut trailers = + trailers_as_aws_chunked_bytes(trailers, actual_length + 1); + // Insert the final CRLF to close the body + trailers.extend_from_slice(CRLF.as_bytes()); + + Poll::Ready(Some(Ok(trailers.into()))) + } + Poll::Pending => Poll::Pending, + Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))), + }; + } + AwsChunkedBodyState::Closed => Poll::Ready(None), + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>, Self::Error>> { + // Trailers were already appended to the body because of the content encoding scheme + Poll::Ready(Ok(None)) + } + + fn is_end_stream(&self) -> bool { + self.state == AwsChunkedBodyState::Closed + } + + fn size_hint(&self) -> SizeHint { + SizeHint::with_exact(self.encoded_length()) + } +} + +/// Errors related to `AwsChunkedBody` +#[derive(Debug)] +enum AwsChunkedBodyError { + /// Error that occurs when the sum of `trailer_lengths` set when creating an `AwsChunkedBody` is + /// not equal to the actual length of the trailers returned by the inner `http_body::Body` + /// implementor. These trailer lengths are necessary in order to correctly calculate the total + /// size of the body for setting the content length header. + ReportedTrailerLengthMismatch { actual: u64, expected: u64 }, + /// Error that occurs when the `stream_length` set when creating an `AwsChunkedBody` is not + /// equal to the actual length of the body returned by the inner `http_body::Body` implementor. + /// `stream_length` must be correct in order to set an accurate content length header. + StreamLengthMismatch { actual: u64, expected: u64 }, +} + +impl std::fmt::Display for AwsChunkedBodyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ReportedTrailerLengthMismatch { actual, expected } => { + write!(f, "When creating this AwsChunkedBody, length of trailers was reported as {expected}. However, when double checking during trailer encoding, length was found to be {actual} instead.") + } + Self::StreamLengthMismatch { actual, expected } => { + write!(f, "When creating this AwsChunkedBody, stream length was reported as {expected}. However, when double checking during body encoding, length was found to be {actual} instead.") + } + } + } +} + +impl std::error::Error for AwsChunkedBodyError {} + +// Used for finding how many hexadecimal digits it takes to represent a base 10 integer +fn int_log16(mut i: T) -> u64 +where + T: std::ops::DivAssign + PartialOrd + From + Copy, +{ + let mut len = 0; + let zero = T::from(0); + let sixteen = T::from(16); + + while i > zero { + i /= sixteen; + len += 1; + } + + len +} + +#[cfg(test)] +mod tests { + use super::{ + total_rendered_length_of_trailers, trailers_as_aws_chunked_bytes, AwsChunkedBody, + AwsChunkedBodyOptions, CHUNK_TERMINATOR, CRLF, + }; + + use aws_smithy_types::body::SdkBody; + use bytes::{Buf, Bytes}; + use bytes_utils::SegmentedBuf; + use http::{HeaderMap, HeaderValue}; + use http_body::{Body, SizeHint}; + use pin_project_lite::pin_project; + + use std::io::Read; + use std::pin::Pin; + use std::task::{Context, Poll}; + use std::time::Duration; + + pin_project! { + struct SputteringBody { + parts: Vec>, + cursor: usize, + delay_in_millis: u64, + } + } + + impl SputteringBody { + fn len(&self) -> usize { + self.parts.iter().flatten().map(|b| b.len()).sum() + } + } + + impl Body for SputteringBody { + type Data = Bytes; + type Error = aws_smithy_types::body::Error; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + if self.cursor == self.parts.len() { + return Poll::Ready(None); + } + + let this = self.project(); + let delay_in_millis = *this.delay_in_millis; + let next_part = this.parts.get_mut(*this.cursor).unwrap().take(); + + match next_part { + None => { + *this.cursor += 1; + let waker = cx.waker().clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(delay_in_millis)).await; + waker.wake(); + }); + Poll::Pending + } + Some(data) => { + *this.cursor += 1; + Poll::Ready(Some(Ok(data))) + } + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>, Self::Error>> { + Poll::Ready(Ok(None)) + } + + fn is_end_stream(&self) -> bool { + false + } + + fn size_hint(&self) -> SizeHint { + SizeHint::new() + } + } + + #[tokio::test] + async fn test_aws_chunked_encoding() { + let test_fut = async { + let input_str = "Hello world"; + let opts = AwsChunkedBodyOptions::new(input_str.len() as u64, Vec::new()); + let mut body = AwsChunkedBody::new(SdkBody::from(input_str), opts); + + let mut output = SegmentedBuf::new(); + while let Some(buf) = body.data().await { + output.push(buf.unwrap()); + } + + let mut actual_output = String::new(); + output + .reader() + .read_to_string(&mut actual_output) + .expect("Doesn't cause IO errors"); + + let expected_output = "B\r\nHello world\r\n0\r\n\r\n"; + + assert_eq!(expected_output, actual_output); + assert!( + body.trailers() + .await + .expect("no errors occurred during trailer polling") + .is_none(), + "aws-chunked encoded bodies don't have normal HTTP trailers" + ); + + // You can insert a `tokio::time::sleep` here to verify the timeout works as intended + }; + + let timeout_duration = Duration::from_secs(3); + if tokio::time::timeout(timeout_duration, test_fut) + .await + .is_err() + { + panic!("test_aws_chunked_encoding timed out after {timeout_duration:?}"); + } + } + + #[tokio::test] + async fn test_aws_chunked_encoding_sputtering_body() { + let test_fut = async { + let input = SputteringBody { + parts: vec![ + Some(Bytes::from_static(b"chunk 1, ")), + None, + Some(Bytes::from_static(b"chunk 2, ")), + Some(Bytes::from_static(b"chunk 3, ")), + None, + None, + Some(Bytes::from_static(b"chunk 4, ")), + Some(Bytes::from_static(b"chunk 5, ")), + Some(Bytes::from_static(b"chunk 6")), + ], + cursor: 0, + delay_in_millis: 500, + }; + let opts = AwsChunkedBodyOptions::new(input.len() as u64, Vec::new()); + let mut body = AwsChunkedBody::new(input, opts); + + let mut output = SegmentedBuf::new(); + while let Some(buf) = body.data().await { + output.push(buf.unwrap()); + } + + let mut actual_output = String::new(); + output + .reader() + .read_to_string(&mut actual_output) + .expect("Doesn't cause IO errors"); + + let expected_output = + "34\r\nchunk 1, chunk 2, chunk 3, chunk 4, chunk 5, chunk 6\r\n0\r\n\r\n"; + + assert_eq!(expected_output, actual_output); + assert!( + body.trailers() + .await + .expect("no errors occurred during trailer polling") + .is_none(), + "aws-chunked encoded bodies don't have normal HTTP trailers" + ); + }; + + let timeout_duration = Duration::from_secs(3); + if tokio::time::timeout(timeout_duration, test_fut) + .await + .is_err() + { + panic!( + "test_aws_chunked_encoding_sputtering_body timed out after {timeout_duration:?}" + ); + } + } + + #[tokio::test] + #[should_panic = "called `Result::unwrap()` on an `Err` value: ReportedTrailerLengthMismatch { actual: 44, expected: 0 }"] + async fn test_aws_chunked_encoding_incorrect_trailer_length_panic() { + let input_str = "Hello world"; + // Test body has no trailers, so this length is incorrect and will trigger an assert panic + // When the panic occurs, it will actually expect a length of 44. This is because, when using + // aws-chunked encoding, each trailer will end with a CRLF which is 2 bytes long. + let wrong_trailer_len = 42; + let opts = AwsChunkedBodyOptions::new(input_str.len() as u64, vec![wrong_trailer_len]); + let mut body = AwsChunkedBody::new(SdkBody::from(input_str), opts); + + // We don't care about the body contents but we have to read it all before checking for trailers + while let Some(buf) = body.data().await { + drop(buf.unwrap()); + } + + assert!( + body.trailers() + .await + .expect("no errors occurred during trailer polling") + .is_none(), + "aws-chunked encoded bodies don't have normal HTTP trailers" + ); + } + + #[tokio::test] + async fn test_aws_chunked_encoding_empty_body() { + let input_str = ""; + let opts = AwsChunkedBodyOptions::new(input_str.len() as u64, Vec::new()); + let mut body = AwsChunkedBody::new(SdkBody::from(input_str), opts); + + let mut output = SegmentedBuf::new(); + while let Some(buf) = body.data().await { + output.push(buf.unwrap()); + } + + let mut actual_output = String::new(); + output + .reader() + .read_to_string(&mut actual_output) + .expect("Doesn't cause IO errors"); + + let expected_output = [CHUNK_TERMINATOR, CRLF].concat(); + + assert_eq!(expected_output, actual_output); + assert!( + body.trailers() + .await + .expect("no errors occurred during trailer polling") + .is_none(), + "aws-chunked encoded bodies don't have normal HTTP trailers" + ); + } + + #[tokio::test] + async fn test_total_rendered_length_of_trailers() { + let mut trailers = HeaderMap::new(); + + trailers.insert("empty_value", HeaderValue::from_static("")); + + trailers.insert("single_value", HeaderValue::from_static("value 1")); + + trailers.insert("two_values", HeaderValue::from_static("value 1")); + trailers.append("two_values", HeaderValue::from_static("value 2")); + + trailers.insert("three_values", HeaderValue::from_static("value 1")); + trailers.append("three_values", HeaderValue::from_static("value 2")); + trailers.append("three_values", HeaderValue::from_static("value 3")); + + let trailers = Some(trailers); + let actual_length = total_rendered_length_of_trailers(trailers.as_ref()); + let expected_length = (trailers_as_aws_chunked_bytes(trailers, actual_length).len()) as u64; + + assert_eq!(expected_length, actual_length); + } + + #[tokio::test] + async fn test_total_rendered_length_of_empty_trailers() { + let trailers = Some(HeaderMap::new()); + let actual_length = total_rendered_length_of_trailers(trailers.as_ref()); + let expected_length = (trailers_as_aws_chunked_bytes(trailers, actual_length).len()) as u64; + + assert_eq!(expected_length, actual_length); + } +} diff --git a/aws/rust-runtime/aws-runtime/src/lib.rs b/aws/rust-runtime/aws-runtime/src/lib.rs index df8415f20..8487640ad 100644 --- a/aws/rust-runtime/aws-runtime/src/lib.rs +++ b/aws/rust-runtime/aws-runtime/src/lib.rs @@ -19,6 +19,10 @@ /// Supporting code for authentication in the AWS SDK. pub mod auth; +/// AWS-specific content-encoding tools +#[cfg(feature = "http-02x")] +pub mod content_encoding; + /// Supporting code for recursion detection in the AWS SDK. pub mod recursion_detection; diff --git a/aws/rust-runtime/aws-runtime/src/user_agent.rs b/aws/rust-runtime/aws-runtime/src/user_agent.rs index bf4564ace..7c580d937 100644 --- a/aws/rust-runtime/aws-runtime/src/user_agent.rs +++ b/aws/rust-runtime/aws-runtime/src/user_agent.rs @@ -3,275 +3,739 @@ * SPDX-License-Identifier: Apache-2.0 */ -use aws_http::user_agent::{ApiMetadata, AwsUserAgent}; -use aws_smithy_runtime_api::box_error::BoxError; -use aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextMut; -use aws_smithy_runtime_api::client::interceptors::Intercept; -use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; -use aws_smithy_types::config_bag::ConfigBag; +use aws_smithy_types::config_bag::{Storable, StoreReplace}; use aws_types::app_name::AppName; +use aws_types::build_metadata::{OsFamily, BUILD_METADATA}; use aws_types::os_shim_internal::Env; -use http::header::{InvalidHeaderValue, USER_AGENT}; -use http::{HeaderName, HeaderValue}; use std::borrow::Cow; +use std::error::Error; use std::fmt; -#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this -const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent"); +mod interceptor; +pub use interceptor::UserAgentInterceptor; + +/// AWS User Agent +/// +/// Ths struct should be inserted into the [`ConfigBag`](aws_smithy_types::config_bag::ConfigBag) +/// during operation construction. The `UserAgentInterceptor` reads `AwsUserAgent` +/// from the config bag and sets the `User-Agent` and `x-amz-user-agent` headers. +#[derive(Clone, Debug)] +pub struct AwsUserAgent { + sdk_metadata: SdkMetadata, + api_metadata: ApiMetadata, + os_metadata: OsMetadata, + language_metadata: LanguageMetadata, + exec_env_metadata: Option, + feature_metadata: Vec, + config_metadata: Vec, + framework_metadata: Vec, + app_name: Option, + build_env_additional_metadata: Option, +} + +impl AwsUserAgent { + /// Load a User Agent configuration from the environment + /// + /// This utilizes [`BUILD_METADATA`](const@aws_types::build_metadata::BUILD_METADATA) from `aws_types` + /// to capture the Rust version & target platform. `ApiMetadata` provides + /// the version & name of the specific service. + pub fn new_from_environment(env: Env, api_metadata: ApiMetadata) -> Self { + let build_metadata = &BUILD_METADATA; + let sdk_metadata = SdkMetadata { + name: "rust", + version: build_metadata.core_pkg_version, + }; + let os_metadata = OsMetadata { + os_family: &build_metadata.os_family, + version: None, + }; + let exec_env_metadata = env + .get("AWS_EXECUTION_ENV") + .ok() + .map(|name| ExecEnvMetadata { name }); + + // Retrieve additional metadata at compile-time from the AWS_SDK_RUST_BUILD_UA_METADATA env var + let build_env_additional_metadata = option_env!("AWS_SDK_RUST_BUILD_UA_METADATA") + .and_then(|value| AdditionalMetadata::new(value).ok()); + + AwsUserAgent { + sdk_metadata, + api_metadata, + os_metadata, + language_metadata: LanguageMetadata { + lang: "rust", + version: BUILD_METADATA.rust_version, + extras: Default::default(), + }, + exec_env_metadata, + feature_metadata: Default::default(), + config_metadata: Default::default(), + framework_metadata: Default::default(), + app_name: Default::default(), + build_env_additional_metadata, + } + } + + /// For test purposes, construct an environment-independent User Agent + /// + /// Without this, running CI on a different platform would produce different user agent strings + pub fn for_tests() -> Self { + Self { + sdk_metadata: SdkMetadata { + name: "rust", + version: "0.123.test", + }, + api_metadata: ApiMetadata { + service_id: "test-service".into(), + version: "0.123", + }, + os_metadata: OsMetadata { + os_family: &OsFamily::Windows, + version: Some("XPSP3".to_string()), + }, + language_metadata: LanguageMetadata { + lang: "rust", + version: "1.50.0", + extras: Default::default(), + }, + exec_env_metadata: None, + feature_metadata: Vec::new(), + config_metadata: Vec::new(), + framework_metadata: Vec::new(), + app_name: None, + build_env_additional_metadata: None, + } + } + + #[doc(hidden)] + /// Adds feature metadata to the user agent. + pub fn with_feature_metadata(mut self, metadata: FeatureMetadata) -> Self { + self.feature_metadata.push(metadata); + self + } + + #[doc(hidden)] + /// Adds feature metadata to the user agent. + pub fn add_feature_metadata(&mut self, metadata: FeatureMetadata) -> &mut Self { + self.feature_metadata.push(metadata); + self + } + + #[doc(hidden)] + /// Adds config metadata to the user agent. + pub fn with_config_metadata(mut self, metadata: ConfigMetadata) -> Self { + self.config_metadata.push(metadata); + self + } + + #[doc(hidden)] + /// Adds config metadata to the user agent. + pub fn add_config_metadata(&mut self, metadata: ConfigMetadata) -> &mut Self { + self.config_metadata.push(metadata); + self + } + + #[doc(hidden)] + /// Adds framework metadata to the user agent. + pub fn with_framework_metadata(mut self, metadata: FrameworkMetadata) -> Self { + self.framework_metadata.push(metadata); + self + } + + #[doc(hidden)] + /// Adds framework metadata to the user agent. + pub fn add_framework_metadata(&mut self, metadata: FrameworkMetadata) -> &mut Self { + self.framework_metadata.push(metadata); + self + } + + /// Sets the app name for the user agent. + pub fn with_app_name(mut self, app_name: AppName) -> Self { + self.app_name = Some(app_name); + self + } + + /// Sets the app name for the user agent. + pub fn set_app_name(&mut self, app_name: AppName) -> &mut Self { + self.app_name = Some(app_name); + self + } + + /// Generate a new-style user agent style header + /// + /// This header should be set at `x-amz-user-agent` + pub fn aws_ua_header(&self) -> String { + /* + ABNF for the user agent (see the bottom of the file for complete ABNF): + ua-string = sdk-metadata RWS + [api-metadata RWS] + os-metadata RWS + language-metadata RWS + [env-metadata RWS] + *(feat-metadata RWS) + *(config-metadata RWS) + *(framework-metadata RWS) + [appId] + */ + let mut ua_value = String::new(); + use std::fmt::Write; + // unwrap calls should never fail because string formatting will always succeed. + write!(ua_value, "{} ", &self.sdk_metadata).unwrap(); + write!(ua_value, "{} ", &self.api_metadata).unwrap(); + write!(ua_value, "{} ", &self.os_metadata).unwrap(); + write!(ua_value, "{} ", &self.language_metadata).unwrap(); + if let Some(ref env_meta) = self.exec_env_metadata { + write!(ua_value, "{} ", env_meta).unwrap(); + } + for feature in &self.feature_metadata { + write!(ua_value, "{} ", feature).unwrap(); + } + for config in &self.config_metadata { + write!(ua_value, "{} ", config).unwrap(); + } + for framework in &self.framework_metadata { + write!(ua_value, "{} ", framework).unwrap(); + } + if let Some(app_name) = &self.app_name { + write!(ua_value, "app/{}", app_name).unwrap(); + } + if let Some(additional_metadata) = &self.build_env_additional_metadata { + write!(ua_value, "{}", additional_metadata).unwrap(); + } + if ua_value.ends_with(' ') { + ua_value.truncate(ua_value.len() - 1); + } + ua_value + } + + /// Generate an old-style User-Agent header for backward compatibility + /// + /// This header is intended to be set at `User-Agent` + pub fn ua_header(&self) -> String { + let mut ua_value = String::new(); + use std::fmt::Write; + write!(ua_value, "{} ", &self.sdk_metadata).unwrap(); + write!(ua_value, "{} ", &self.os_metadata).unwrap(); + write!(ua_value, "{}", &self.language_metadata).unwrap(); + ua_value + } +} + +impl Storable for AwsUserAgent { + type Storer = StoreReplace; +} + +#[derive(Clone, Copy, Debug)] +struct SdkMetadata { + name: &'static str, + version: &'static str, +} + +impl fmt::Display for SdkMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "aws-sdk-{}/{}", self.name, self.version) + } +} + +/// Metadata about the client that's making the call. +#[derive(Clone, Debug)] +pub struct ApiMetadata { + service_id: Cow<'static, str>, + version: &'static str, +} + +impl ApiMetadata { + /// Creates new `ApiMetadata`. + pub const fn new(service_id: &'static str, version: &'static str) -> Self { + Self { + service_id: Cow::Borrowed(service_id), + version, + } + } +} + +impl fmt::Display for ApiMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "api/{}/{}", self.service_id, self.version) + } +} + +impl Storable for ApiMetadata { + type Storer = StoreReplace; +} +/// Error for when an user agent metadata doesn't meet character requirements. +/// +/// Metadata may only have alphanumeric characters and any of these characters: +/// ```text +/// !#$%&'*+-.^_`|~ +/// ``` +/// Spaces are not allowed. #[derive(Debug)] -enum UserAgentInterceptorError { - MissingApiMetadata, - InvalidHeaderValue(InvalidHeaderValue), +#[non_exhaustive] +pub struct InvalidMetadataValue; + +impl Error for InvalidMetadataValue {} + +impl fmt::Display for InvalidMetadataValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "User agent metadata can only have alphanumeric characters, or any of \ + '!' | '#' | '$' | '%' | '&' | '\\'' | '*' | '+' | '-' | \ + '.' | '^' | '_' | '`' | '|' | '~'" + ) + } } -impl std::error::Error for UserAgentInterceptorError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::InvalidHeaderValue(source) => Some(source), - Self::MissingApiMetadata => None, +fn validate_metadata(value: Cow<'static, str>) -> Result, InvalidMetadataValue> { + fn valid_character(c: char) -> bool { + match c { + _ if c.is_ascii_alphanumeric() => true, + '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|' + | '~' => true, + _ => false, } } + if !value.chars().all(valid_character) { + return Err(InvalidMetadataValue); + } + Ok(value) +} + +#[doc(hidden)] +/// Additional metadata that can be bundled with framework or feature metadata. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct AdditionalMetadata { + value: Cow<'static, str>, +} + +impl AdditionalMetadata { + /// Creates `AdditionalMetadata`. + /// + /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or + /// has characters other than the following: + /// ```text + /// !#$%&'*+-.^_`|~ + /// ``` + pub fn new(value: impl Into>) -> Result { + Ok(Self { + value: validate_metadata(value.into())?, + }) + } +} + +impl fmt::Display for AdditionalMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // additional-metadata = "md/" ua-pair + write!(f, "md/{}", self.value) + } +} + +#[derive(Clone, Debug, Default)] +struct AdditionalMetadataList(Vec); + +impl AdditionalMetadataList { + fn push(&mut self, metadata: AdditionalMetadata) { + self.0.push(metadata); + } } -impl fmt::Display for UserAgentInterceptorError { +impl fmt::Display for AdditionalMetadataList { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::InvalidHeaderValue(_) => "AwsUserAgent generated an invalid HTTP header value. This is a bug. Please file an issue.", - Self::MissingApiMetadata => "The UserAgentInterceptor requires ApiMetadata to be set before the request is made. This is a bug. Please file an issue.", + for metadata in &self.0 { + write!(f, " {}", metadata)?; + } + Ok(()) + } +} + +#[doc(hidden)] +/// Metadata about a feature that is being used in the SDK. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct FeatureMetadata { + name: Cow<'static, str>, + version: Option>, + additional: AdditionalMetadataList, +} + +impl FeatureMetadata { + /// Creates `FeatureMetadata`. + /// + /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or + /// has characters other than the following: + /// ```text + /// !#$%&'*+-.^_`|~ + /// ``` + pub fn new( + name: impl Into>, + version: Option>, + ) -> Result { + Ok(Self { + name: validate_metadata(name.into())?, + version: version.map(validate_metadata).transpose()?, + additional: Default::default(), }) } + + /// Bundles additional arbitrary metadata with this feature metadata. + pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self { + self.additional.push(metadata); + self + } } -impl From for UserAgentInterceptorError { - fn from(err: InvalidHeaderValue) -> Self { - UserAgentInterceptorError::InvalidHeaderValue(err) +impl fmt::Display for FeatureMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // feat-metadata = "ft/" name ["/" version] *(RWS additional-metadata) + if let Some(version) = &self.version { + write!(f, "ft/{}/{}{}", self.name, version, self.additional) + } else { + write!(f, "ft/{}{}", self.name, self.additional) + } } } -/// Generates and attaches the AWS SDK's user agent to a HTTP request +#[doc(hidden)] +/// Metadata about a config value that is being used in the SDK. +#[derive(Clone, Debug)] #[non_exhaustive] -#[derive(Debug, Default)] -pub struct UserAgentInterceptor; - -impl UserAgentInterceptor { - /// Creates a new `UserAgentInterceptor` - pub fn new() -> Self { - UserAgentInterceptor - } -} - -fn header_values( - ua: &AwsUserAgent, -) -> Result<(HeaderValue, HeaderValue), UserAgentInterceptorError> { - // Pay attention to the extremely subtle difference between ua_header and aws_ua_header below... - Ok(( - HeaderValue::try_from(ua.ua_header())?, - HeaderValue::try_from(ua.aws_ua_header())?, - )) -} - -impl Intercept for UserAgentInterceptor { - fn name(&self) -> &'static str { - "UserAgentInterceptor" - } - - fn modify_before_signing( - &self, - context: &mut BeforeTransmitInterceptorContextMut<'_>, - _runtime_components: &RuntimeComponents, - cfg: &mut ConfigBag, - ) -> Result<(), BoxError> { - // Allow for overriding the user agent by an earlier interceptor (so, for example, - // tests can use `AwsUserAgent::for_tests()`) by attempting to grab one out of the - // config bag before creating one. - let ua: Cow<'_, AwsUserAgent> = cfg - .load::() - .map(Cow::Borrowed) - .map(Result::<_, UserAgentInterceptorError>::Ok) - .unwrap_or_else(|| { - let api_metadata = cfg - .load::() - .ok_or(UserAgentInterceptorError::MissingApiMetadata)?; - let mut ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata.clone()); - - let maybe_app_name = cfg.load::(); - if let Some(app_name) = maybe_app_name { - ua.set_app_name(app_name.clone()); - } - Ok(Cow::Owned(ua)) - })?; - - let headers = context.request_mut().headers_mut(); - let (user_agent, x_amz_user_agent) = header_values(&ua)?; - headers.append(USER_AGENT, user_agent); - headers.append(X_AMZ_USER_AGENT, x_amz_user_agent); - Ok(()) +pub struct ConfigMetadata { + config: Cow<'static, str>, + value: Option>, +} + +impl ConfigMetadata { + /// Creates `ConfigMetadata`. + /// + /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or + /// has characters other than the following: + /// ```text + /// !#$%&'*+-.^_`|~ + /// ``` + pub fn new( + config: impl Into>, + value: Option>, + ) -> Result { + Ok(Self { + config: validate_metadata(config.into())?, + value: value.map(validate_metadata).transpose()?, + }) } } -#[cfg(test)] -mod tests { - use super::*; - use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext}; - use aws_smithy_runtime_api::client::interceptors::Intercept; - use aws_smithy_runtime_api::client::orchestrator::HttpRequest; - use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder; - use aws_smithy_types::config_bag::{ConfigBag, Layer}; - use aws_smithy_types::error::display::DisplayErrorContext; - - fn expect_header<'a>(context: &'a InterceptorContext, header_name: &str) -> &'a str { - context - .request() - .expect("request is set") - .headers() - .get(header_name) - .unwrap() - } - - fn context() -> InterceptorContext { - let mut context = InterceptorContext::new(Input::doesnt_matter()); - context.enter_serialization_phase(); - context.set_request(HttpRequest::empty()); - let _ = context.take_input(); - context.enter_before_transmit_phase(); - context +impl fmt::Display for ConfigMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // config-metadata = "cfg/" config ["/" value] + if let Some(value) = &self.value { + write!(f, "cfg/{}/{}", self.config, value) + } else { + write!(f, "cfg/{}", self.config) + } } +} - #[test] - fn test_overridden_ua() { - let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); - let mut context = context(); +#[doc(hidden)] +/// Metadata about a software framework that is being used with the SDK. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct FrameworkMetadata { + name: Cow<'static, str>, + version: Option>, + additional: AdditionalMetadataList, +} - let mut layer = Layer::new("test"); - layer.store_put(AwsUserAgent::for_tests()); - layer.store_put(ApiMetadata::new("unused", "unused")); - let mut cfg = ConfigBag::of_layers(vec![layer]); +impl FrameworkMetadata { + /// Creates `FrameworkMetadata`. + /// + /// This will result in `InvalidMetadataValue` if the given value isn't alphanumeric or + /// has characters other than the following: + /// ```text + /// !#$%&'*+-.^_`|~ + /// ``` + pub fn new( + name: impl Into>, + version: Option>, + ) -> Result { + Ok(Self { + name: validate_metadata(name.into())?, + version: version.map(validate_metadata).transpose()?, + additional: Default::default(), + }) + } + + /// Bundles additional arbitrary metadata with this framework metadata. + pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self { + self.additional.push(metadata); + self + } +} + +impl fmt::Display for FrameworkMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // framework-metadata = "lib/" name ["/" version] *(RWS additional-metadata) + if let Some(version) = &self.version { + write!(f, "lib/{}/{}{}", self.name, version, self.additional) + } else { + write!(f, "lib/{}{}", self.name, self.additional) + } + } +} - let interceptor = UserAgentInterceptor::new(); - let mut ctx = Into::into(&mut context); - interceptor - .modify_before_signing(&mut ctx, &rc, &mut cfg) - .unwrap(); +#[derive(Clone, Debug)] +struct OsMetadata { + os_family: &'static OsFamily, + version: Option, +} - let header = expect_header(&context, "user-agent"); - assert_eq!(AwsUserAgent::for_tests().ua_header(), header); - assert!(!header.contains("unused")); +impl fmt::Display for OsMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let os_family = match self.os_family { + OsFamily::Windows => "windows", + OsFamily::Linux => "linux", + OsFamily::Macos => "macos", + OsFamily::Android => "android", + OsFamily::Ios => "ios", + OsFamily::Other => "other", + }; + write!(f, "os/{}", os_family)?; + if let Some(ref version) = self.version { + write!(f, "/{}", version)?; + } + Ok(()) + } +} +#[derive(Clone, Debug)] +struct LanguageMetadata { + lang: &'static str, + version: &'static str, + extras: AdditionalMetadataList, +} +impl fmt::Display for LanguageMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // language-metadata = "lang/" language "/" version *(RWS additional-metadata) + write!(f, "lang/{}/{}{}", self.lang, self.version, self.extras) + } +} + +#[derive(Clone, Debug)] +struct ExecEnvMetadata { + name: String, +} +impl fmt::Display for ExecEnvMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "exec-env/{}", &self.name) + } +} + +#[cfg(test)] +mod test { + use super::*; + use aws_types::app_name::AppName; + use aws_types::build_metadata::OsFamily; + use aws_types::os_shim_internal::Env; + use std::borrow::Cow; + + fn make_deterministic(ua: &mut AwsUserAgent) { + // hard code some variable things for a deterministic test + ua.sdk_metadata.version = "0.1"; + ua.language_metadata.version = "1.50.0"; + ua.os_metadata.os_family = &OsFamily::Macos; + ua.os_metadata.version = Some("1.15".to_string()); + } + + #[test] + fn generate_a_valid_ua() { + let api_metadata = ApiMetadata { + service_id: "dynamodb".into(), + version: "123", + }; + let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata); + make_deterministic(&mut ua); + assert_eq!( + ua.aws_ua_header(), + "aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0" + ); assert_eq!( - AwsUserAgent::for_tests().aws_ua_header(), - expect_header(&context, "x-amz-user-agent") + ua.ua_header(), + "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" ); } #[test] - fn test_default_ua() { - let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); - let mut context = context(); - - let api_metadata = ApiMetadata::new("some-service", "some-version"); - let mut layer = Layer::new("test"); - layer.store_put(api_metadata.clone()); - let mut config = ConfigBag::of_layers(vec![layer]); - - let interceptor = UserAgentInterceptor::new(); - let mut ctx = Into::into(&mut context); - interceptor - .modify_before_signing(&mut ctx, &rc, &mut config) - .unwrap(); - - let expected_ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata); - assert!( - expected_ua.aws_ua_header().contains("some-service"), - "precondition" + fn generate_a_valid_ua_with_execution_env() { + let api_metadata = ApiMetadata { + service_id: "dynamodb".into(), + version: "123", + }; + let mut ua = AwsUserAgent::new_from_environment( + Env::from_slice(&[("AWS_EXECUTION_ENV", "lambda")]), + api_metadata, ); + make_deterministic(&mut ua); assert_eq!( - expected_ua.ua_header(), - expect_header(&context, "user-agent") + ua.aws_ua_header(), + "aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda" ); assert_eq!( - expected_ua.aws_ua_header(), - expect_header(&context, "x-amz-user-agent") + ua.ua_header(), + "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" ); } #[test] - fn test_app_name() { - let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); - let mut context = context(); - - let api_metadata = ApiMetadata::new("some-service", "some-version"); - let mut layer = Layer::new("test"); - layer.store_put(api_metadata); - layer.store_put(AppName::new("my_awesome_app").unwrap()); - let mut config = ConfigBag::of_layers(vec![layer]); - - let interceptor = UserAgentInterceptor::new(); - let mut ctx = Into::into(&mut context); - interceptor - .modify_before_signing(&mut ctx, &rc, &mut config) - .unwrap(); - - let app_value = "app/my_awesome_app"; - let header = expect_header(&context, "user-agent"); - assert!( - !header.contains(app_value), - "expected `{header}` to not contain `{app_value}`" + fn generate_a_valid_ua_with_features() { + let api_metadata = ApiMetadata { + service_id: "dynamodb".into(), + version: "123", + }; + let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata) + .with_feature_metadata( + FeatureMetadata::new("test-feature", Some(Cow::Borrowed("1.0"))).unwrap(), + ) + .with_feature_metadata( + FeatureMetadata::new("other-feature", None) + .unwrap() + .with_additional(AdditionalMetadata::new("asdf").unwrap()), + ); + make_deterministic(&mut ua); + assert_eq!( + ua.aws_ua_header(), + "aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 ft/test-feature/1.0 ft/other-feature md/asdf" ); - - let header = expect_header(&context, "x-amz-user-agent"); - assert!( - header.contains(app_value), - "expected `{header}` to contain `{app_value}`" + assert_eq!( + ua.ua_header(), + "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" ); } #[test] - fn test_api_metadata_missing() { - let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); - let mut context = context(); - let mut config = ConfigBag::base(); - - let interceptor = UserAgentInterceptor::new(); - let mut ctx = Into::into(&mut context); - - let error = format!( - "{}", - DisplayErrorContext( - &*interceptor - .modify_before_signing(&mut ctx, &rc, &mut config) - .expect_err("it should error") + fn generate_a_valid_ua_with_config() { + let api_metadata = ApiMetadata { + service_id: "dynamodb".into(), + version: "123", + }; + let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata) + .with_config_metadata( + ConfigMetadata::new("some-config", Some(Cow::Borrowed("5"))).unwrap(), ) + .with_config_metadata(ConfigMetadata::new("other-config", None).unwrap()); + make_deterministic(&mut ua); + assert_eq!( + ua.aws_ua_header(), + "aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 cfg/some-config/5 cfg/other-config" ); - assert!( - error.contains("This is a bug"), - "`{error}` should contain message `This is a bug`" + assert_eq!( + ua.ua_header(), + "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" ); } #[test] - fn test_api_metadata_missing_with_ua_override() { - let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); - let mut context = context(); - - let mut layer = Layer::new("test"); - layer.store_put(AwsUserAgent::for_tests()); - let mut config = ConfigBag::of_layers(vec![layer]); - - let interceptor = UserAgentInterceptor::new(); - let mut ctx = Into::into(&mut context); - - interceptor - .modify_before_signing(&mut ctx, &rc, &mut config) - .expect("it should succeed"); + fn generate_a_valid_ua_with_frameworks() { + let api_metadata = ApiMetadata { + service_id: "dynamodb".into(), + version: "123", + }; + let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata) + .with_framework_metadata( + FrameworkMetadata::new("some-framework", Some(Cow::Borrowed("1.3"))) + .unwrap() + .with_additional(AdditionalMetadata::new("something").unwrap()), + ) + .with_framework_metadata(FrameworkMetadata::new("other", None).unwrap()); + make_deterministic(&mut ua); + assert_eq!( + ua.aws_ua_header(), + "aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 lib/some-framework/1.3 md/something lib/other" + ); + assert_eq!( + ua.ua_header(), + "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" + ); + } - let header = expect_header(&context, "user-agent"); - assert_eq!(AwsUserAgent::for_tests().ua_header(), header); - assert!(!header.contains("unused")); + #[test] + fn generate_a_valid_ua_with_app_name() { + let api_metadata = ApiMetadata { + service_id: "dynamodb".into(), + version: "123", + }; + let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata) + .with_app_name(AppName::new("my_app").unwrap()); + make_deterministic(&mut ua); + assert_eq!( + ua.aws_ua_header(), + "aws-sdk-rust/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app" + ); + assert_eq!( + ua.ua_header(), + "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" + ); + } + #[test] + fn generate_a_valid_ua_with_build_env_additional_metadata() { + let mut ua = AwsUserAgent::for_tests(); + ua.build_env_additional_metadata = Some(AdditionalMetadata::new("asdf").unwrap()); assert_eq!( - AwsUserAgent::for_tests().aws_ua_header(), - expect_header(&context, "x-amz-user-agent") + ua.aws_ua_header(), + "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf" + ); + assert_eq!( + ua.ua_header(), + "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0" ); } } + +/* +Appendix: User Agent ABNF +sdk-ua-header = "x-amz-user-agent:" OWS ua-string OWS +ua-pair = ua-name ["/" ua-value] +ua-name = token +ua-value = token +version = token +name = token +service-id = token +sdk-name = java / ruby / php / dotnet / python / cli / kotlin / rust / js / cpp / go / go-v2 +os-family = windows / linux / macos / android / ios / other +config = retry-mode +additional-metadata = "md/" ua-pair +sdk-metadata = "aws-sdk-" sdk-name "/" version +api-metadata = "api/" service-id "/" version +os-metadata = "os/" os-family ["/" version] +language-metadata = "lang/" language "/" version *(RWS additional-metadata) +env-metadata = "exec-env/" name +feat-metadata = "ft/" name ["/" version] *(RWS additional-metadata) +config-metadata = "cfg/" config ["/" value] +framework-metadata = "lib/" name ["/" version] *(RWS additional-metadata) +app-id = "app/" name +build-env-additional-metadata = "md/" value +ua-string = sdk-metadata RWS + [api-metadata RWS] + os-metadata RWS + language-metadata RWS + [env-metadata RWS] + *(feat-metadata RWS) + *(config-metadata RWS) + *(framework-metadata RWS) + [app-id] + [build-env-additional-metadata] + +# New metadata field might be added in the future and they must follow this format +prefix = token +metadata = prefix "/" ua-pair + +# token, RWS and OWS are defined in [RFC 7230](https://tools.ietf.org/html/rfc7230) +OWS = *( SP / HTAB ) + ; optional whitespace +RWS = 1*( SP / HTAB ) + ; required whitespace +token = 1*tchar +tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / + "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA +*/ diff --git a/aws/rust-runtime/aws-runtime/src/user_agent/interceptor.rs b/aws/rust-runtime/aws-runtime/src/user_agent/interceptor.rs new file mode 100644 index 000000000..dadf5ec28 --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/user_agent/interceptor.rs @@ -0,0 +1,277 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use crate::user_agent::{ApiMetadata, AwsUserAgent}; +use aws_smithy_runtime_api::box_error::BoxError; +use aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextMut; +use aws_smithy_runtime_api::client::interceptors::Intercept; +use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; +use aws_smithy_types::config_bag::ConfigBag; +use aws_types::app_name::AppName; +use aws_types::os_shim_internal::Env; +use http::header::{InvalidHeaderValue, USER_AGENT}; +use http::{HeaderName, HeaderValue}; +use std::borrow::Cow; +use std::fmt; + +#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this +const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent"); + +#[derive(Debug)] +enum UserAgentInterceptorError { + MissingApiMetadata, + InvalidHeaderValue(InvalidHeaderValue), +} + +impl std::error::Error for UserAgentInterceptorError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::InvalidHeaderValue(source) => Some(source), + Self::MissingApiMetadata => None, + } + } +} + +impl fmt::Display for UserAgentInterceptorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::InvalidHeaderValue(_) => "AwsUserAgent generated an invalid HTTP header value. This is a bug. Please file an issue.", + Self::MissingApiMetadata => "The UserAgentInterceptor requires ApiMetadata to be set before the request is made. This is a bug. Please file an issue.", + }) + } +} + +impl From for UserAgentInterceptorError { + fn from(err: InvalidHeaderValue) -> Self { + UserAgentInterceptorError::InvalidHeaderValue(err) + } +} + +/// Generates and attaches the AWS SDK's user agent to a HTTP request +#[non_exhaustive] +#[derive(Debug, Default)] +pub struct UserAgentInterceptor; + +impl UserAgentInterceptor { + /// Creates a new `UserAgentInterceptor` + pub fn new() -> Self { + UserAgentInterceptor + } +} + +fn header_values( + ua: &AwsUserAgent, +) -> Result<(HeaderValue, HeaderValue), UserAgentInterceptorError> { + // Pay attention to the extremely subtle difference between ua_header and aws_ua_header below... + Ok(( + HeaderValue::try_from(ua.ua_header())?, + HeaderValue::try_from(ua.aws_ua_header())?, + )) +} + +impl Intercept for UserAgentInterceptor { + fn name(&self) -> &'static str { + "UserAgentInterceptor" + } + + fn modify_before_signing( + &self, + context: &mut BeforeTransmitInterceptorContextMut<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + // Allow for overriding the user agent by an earlier interceptor (so, for example, + // tests can use `AwsUserAgent::for_tests()`) by attempting to grab one out of the + // config bag before creating one. + let ua: Cow<'_, AwsUserAgent> = cfg + .load::() + .map(Cow::Borrowed) + .map(Result::<_, UserAgentInterceptorError>::Ok) + .unwrap_or_else(|| { + let api_metadata = cfg + .load::() + .ok_or(UserAgentInterceptorError::MissingApiMetadata)?; + let mut ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata.clone()); + + let maybe_app_name = cfg.load::(); + if let Some(app_name) = maybe_app_name { + ua.set_app_name(app_name.clone()); + } + Ok(Cow::Owned(ua)) + })?; + + let headers = context.request_mut().headers_mut(); + let (user_agent, x_amz_user_agent) = header_values(&ua)?; + headers.append(USER_AGENT, user_agent); + headers.append(X_AMZ_USER_AGENT, x_amz_user_agent); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext}; + use aws_smithy_runtime_api::client::interceptors::Intercept; + use aws_smithy_runtime_api::client::orchestrator::HttpRequest; + use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder; + use aws_smithy_types::config_bag::{ConfigBag, Layer}; + use aws_smithy_types::error::display::DisplayErrorContext; + + fn expect_header<'a>(context: &'a InterceptorContext, header_name: &str) -> &'a str { + context + .request() + .expect("request is set") + .headers() + .get(header_name) + .unwrap() + } + + fn context() -> InterceptorContext { + let mut context = InterceptorContext::new(Input::doesnt_matter()); + context.enter_serialization_phase(); + context.set_request(HttpRequest::empty()); + let _ = context.take_input(); + context.enter_before_transmit_phase(); + context + } + + #[test] + fn test_overridden_ua() { + let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); + let mut context = context(); + + let mut layer = Layer::new("test"); + layer.store_put(AwsUserAgent::for_tests()); + layer.store_put(ApiMetadata::new("unused", "unused")); + let mut cfg = ConfigBag::of_layers(vec![layer]); + + let interceptor = UserAgentInterceptor::new(); + let mut ctx = Into::into(&mut context); + interceptor + .modify_before_signing(&mut ctx, &rc, &mut cfg) + .unwrap(); + + let header = expect_header(&context, "user-agent"); + assert_eq!(AwsUserAgent::for_tests().ua_header(), header); + assert!(!header.contains("unused")); + + assert_eq!( + AwsUserAgent::for_tests().aws_ua_header(), + expect_header(&context, "x-amz-user-agent") + ); + } + + #[test] + fn test_default_ua() { + let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); + let mut context = context(); + + let api_metadata = ApiMetadata::new("some-service", "some-version"); + let mut layer = Layer::new("test"); + layer.store_put(api_metadata.clone()); + let mut config = ConfigBag::of_layers(vec![layer]); + + let interceptor = UserAgentInterceptor::new(); + let mut ctx = Into::into(&mut context); + interceptor + .modify_before_signing(&mut ctx, &rc, &mut config) + .unwrap(); + + let expected_ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata); + assert!( + expected_ua.aws_ua_header().contains("some-service"), + "precondition" + ); + assert_eq!( + expected_ua.ua_header(), + expect_header(&context, "user-agent") + ); + assert_eq!( + expected_ua.aws_ua_header(), + expect_header(&context, "x-amz-user-agent") + ); + } + + #[test] + fn test_app_name() { + let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); + let mut context = context(); + + let api_metadata = ApiMetadata::new("some-service", "some-version"); + let mut layer = Layer::new("test"); + layer.store_put(api_metadata); + layer.store_put(AppName::new("my_awesome_app").unwrap()); + let mut config = ConfigBag::of_layers(vec![layer]); + + let interceptor = UserAgentInterceptor::new(); + let mut ctx = Into::into(&mut context); + interceptor + .modify_before_signing(&mut ctx, &rc, &mut config) + .unwrap(); + + let app_value = "app/my_awesome_app"; + let header = expect_header(&context, "user-agent"); + assert!( + !header.contains(app_value), + "expected `{header}` to not contain `{app_value}`" + ); + + let header = expect_header(&context, "x-amz-user-agent"); + assert!( + header.contains(app_value), + "expected `{header}` to contain `{app_value}`" + ); + } + + #[test] + fn test_api_metadata_missing() { + let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); + let mut context = context(); + let mut config = ConfigBag::base(); + + let interceptor = UserAgentInterceptor::new(); + let mut ctx = Into::into(&mut context); + + let error = format!( + "{}", + DisplayErrorContext( + &*interceptor + .modify_before_signing(&mut ctx, &rc, &mut config) + .expect_err("it should error") + ) + ); + assert!( + error.contains("This is a bug"), + "`{error}` should contain message `This is a bug`" + ); + } + + #[test] + fn test_api_metadata_missing_with_ua_override() { + let rc = RuntimeComponentsBuilder::for_tests().build().unwrap(); + let mut context = context(); + + let mut layer = Layer::new("test"); + layer.store_put(AwsUserAgent::for_tests()); + let mut config = ConfigBag::of_layers(vec![layer]); + + let interceptor = UserAgentInterceptor::new(); + let mut ctx = Into::into(&mut context); + + interceptor + .modify_before_signing(&mut ctx, &rc, &mut config) + .expect("it should succeed"); + + let header = expect_header(&context, "user-agent"); + assert_eq!(AwsUserAgent::for_tests().ua_header(), header); + assert!(!header.contains("unused")); + + assert_eq!( + AwsUserAgent::for_tests().aws_ua_header(), + expect_header(&context, "x-amz-user-agent") + ); + } +} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCargoDependency.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCargoDependency.kt index 001f17984..ae93a61e5 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCargoDependency.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCargoDependency.kt @@ -19,8 +19,6 @@ object AwsCargoDependency { fun awsCredentialTypes(runtimeConfig: RuntimeConfig) = runtimeConfig.awsRuntimeCrate("aws-credential-types") - fun awsHttp(runtimeConfig: RuntimeConfig) = runtimeConfig.awsRuntimeCrate("aws-http") - fun awsRuntime(runtimeConfig: RuntimeConfig) = runtimeConfig.awsRuntimeCrate("aws-runtime") fun awsRuntimeApi(runtimeConfig: RuntimeConfig) = runtimeConfig.awsRuntimeCrate("aws-runtime-api") diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeType.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeType.kt index b7042a90e..3ab7f571d 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeType.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsRuntimeType.kt @@ -50,8 +50,6 @@ object AwsRuntimeType { fun awsCredentialTypesTestUtil(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsCredentialTypes(runtimeConfig).toDevDependency().withFeature("test-util").toType() - fun awsHttp(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsHttp(runtimeConfig).toType() - fun awsSigv4(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsSigv4(runtimeConfig).toType() fun awsTypes(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsTypes(runtimeConfig).toType() diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt index 37bff546c..2500712e8 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/HttpRequestChecksumDecorator.kt @@ -34,6 +34,7 @@ private fun RuntimeConfig.awsInlineableHttpRequestChecksum() = CargoDependency.Http, CargoDependency.HttpBody, CargoDependency.Tracing, + AwsCargoDependency.awsRuntime(this).withFeature("http-02x"), CargoDependency.smithyChecksums(this), CargoDependency.smithyHttp(this), CargoDependency.smithyRuntimeApiClient(this), diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/UserAgentDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/UserAgentDecorator.kt index b8b69f275..927a4c8dd 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/UserAgentDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/UserAgentDecorator.kt @@ -71,7 +71,7 @@ class UserAgentDecorator : ClientCodegenDecorator { pub(crate) static API_METADATA: #{user_agent}::ApiMetadata = #{user_agent}::ApiMetadata::new(${serviceId.dq()}, #{PKG_VERSION}); """, - "user_agent" to AwsRuntimeType.awsHttp(runtimeConfig).resolve("user_agent"), + "user_agent" to AwsRuntimeType.awsRuntime(runtimeConfig).resolve("user_agent"), "PKG_VERSION" to CrateVersionCustomization.pkgVersion(ClientRustModule.Meta), ) } @@ -109,7 +109,7 @@ class UserAgentDecorator : ClientCodegenDecorator { arrayOf( *preludeScope, "AppName" to AwsRuntimeType.awsTypes(runtimeConfig).resolve("app_name::AppName"), - "AwsUserAgent" to AwsRuntimeType.awsHttp(runtimeConfig).resolve("user_agent::AwsUserAgent"), + "AwsUserAgent" to AwsRuntimeType.awsRuntime(runtimeConfig).resolve("user_agent::AwsUserAgent"), ) override fun section(section: ServiceConfig): Writable = diff --git a/aws/sdk/integration-tests/dynamodb/Cargo.toml b/aws/sdk/integration-tests/dynamodb/Cargo.toml index 57eadecc0..c6b383774 100644 --- a/aws/sdk/integration-tests/dynamodb/Cargo.toml +++ b/aws/sdk/integration-tests/dynamodb/Cargo.toml @@ -14,7 +14,6 @@ publish = false approx = "0.5.1" aws-config = { path = "../../build/aws-sdk/sdk/aws-config" } aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } -aws-http = { path = "../../build/aws-sdk/sdk/aws-http" } aws-sdk-dynamodb = { path = "../../build/aws-sdk/sdk/dynamodb", features = ["behavior-version-latest"] } aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async", features = ["test-util"] } aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" } diff --git a/aws/sdk/integration-tests/glacier/Cargo.toml b/aws/sdk/integration-tests/glacier/Cargo.toml index 418cbb6ba..6674d35d4 100644 --- a/aws/sdk/integration-tests/glacier/Cargo.toml +++ b/aws/sdk/integration-tests/glacier/Cargo.toml @@ -12,7 +12,6 @@ publish = false [dev-dependencies] aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } -aws-http = { path = "../../build/aws-sdk/sdk/aws-http"} aws-sdk-glacier = { path = "../../build/aws-sdk/sdk/glacier", features = ["behavior-version-latest"] } aws-smithy-protocol-test = { path = "../../build/aws-sdk/sdk/aws-smithy-protocol-test"} aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] } diff --git a/aws/sdk/integration-tests/iam/Cargo.toml b/aws/sdk/integration-tests/iam/Cargo.toml index 45235fa71..0c1a02e8b 100644 --- a/aws/sdk/integration-tests/iam/Cargo.toml +++ b/aws/sdk/integration-tests/iam/Cargo.toml @@ -12,7 +12,6 @@ publish = false [dev-dependencies] aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } -aws-http = { path = "../../build/aws-sdk/sdk/aws-http"} aws-sdk-iam = { path = "../../build/aws-sdk/sdk/iam", features = ["behavior-version-latest"] } aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] } aws-smithy-types = { path = "../../build/aws-sdk/sdk/aws-smithy-types" } diff --git a/aws/sdk/integration-tests/kms/Cargo.toml b/aws/sdk/integration-tests/kms/Cargo.toml index d29e5b215..86e314597 100644 --- a/aws/sdk/integration-tests/kms/Cargo.toml +++ b/aws/sdk/integration-tests/kms/Cargo.toml @@ -16,7 +16,6 @@ test-util = [] [dev-dependencies] aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } -aws-http = { path = "../../build/aws-sdk/sdk/aws-http" } aws-runtime = { path = "../../build/aws-sdk/sdk/aws-runtime" } aws-sdk-kms = { path = "../../build/aws-sdk/sdk/kms", features = ["test-util", "behavior-version-latest"] } aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async", features = ["test-util"] } diff --git a/aws/sdk/integration-tests/lambda/Cargo.toml b/aws/sdk/integration-tests/lambda/Cargo.toml index ec891d010..45ff53adf 100644 --- a/aws/sdk/integration-tests/lambda/Cargo.toml +++ b/aws/sdk/integration-tests/lambda/Cargo.toml @@ -10,7 +10,6 @@ publish = false [dev-dependencies] async-stream = "0.3.0" aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } -aws-http = { path = "../../build/aws-sdk/sdk/aws-http" } aws-sdk-lambda = { path = "../../build/aws-sdk/sdk/lambda", features = ["behavior-version-latest"] } aws-smithy-eventstream = { path = "../../build/aws-sdk/sdk/aws-smithy-eventstream" } aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" } diff --git a/aws/sdk/integration-tests/polly/Cargo.toml b/aws/sdk/integration-tests/polly/Cargo.toml index 9e7e83a51..c68b4e69c 100644 --- a/aws/sdk/integration-tests/polly/Cargo.toml +++ b/aws/sdk/integration-tests/polly/Cargo.toml @@ -12,7 +12,6 @@ publish = false [dev-dependencies] aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } -aws-http = { path = "../../build/aws-sdk/sdk/aws-http"} aws-sdk-polly = { path = "../../build/aws-sdk/sdk/polly", features = ["behavior-version-latest"] } aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" } bytes = "1.0.0" diff --git a/aws/sdk/integration-tests/qldbsession/Cargo.toml b/aws/sdk/integration-tests/qldbsession/Cargo.toml index 06673d843..d235881dd 100644 --- a/aws/sdk/integration-tests/qldbsession/Cargo.toml +++ b/aws/sdk/integration-tests/qldbsession/Cargo.toml @@ -16,7 +16,6 @@ test-util = [] [dev-dependencies] aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } -aws-http = { path = "../../build/aws-sdk/sdk/aws-http" } aws-sdk-qldbsession = { path = "../../build/aws-sdk/sdk/qldbsession", features = ["test-util", "behavior-version-latest"] } aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async" } aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" } diff --git a/aws/sdk/integration-tests/s3/Cargo.toml b/aws/sdk/integration-tests/s3/Cargo.toml index cdb22592b..8ba7542e6 100644 --- a/aws/sdk/integration-tests/s3/Cargo.toml +++ b/aws/sdk/integration-tests/s3/Cargo.toml @@ -18,7 +18,6 @@ test-util = [] async-std = "1.12.0" aws-config = { path = "../../build/aws-sdk/sdk/aws-config", features = ["behavior-version-latest"] } aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } -aws-http = { path = "../../build/aws-sdk/sdk/aws-http" } aws-runtime = { path = "../../build/aws-sdk/sdk/aws-runtime", features = ["test-util"] } aws-sdk-s3 = { path = "../../build/aws-sdk/sdk/s3", features = ["test-util", "behavior-version-latest"] } aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async", features = ["test-util", "rt-tokio"] } diff --git a/aws/sdk/integration-tests/s3/tests/checksums.rs b/aws/sdk/integration-tests/s3/tests/checksums.rs index a61f9026e..175f7c5f0 100644 --- a/aws/sdk/integration-tests/s3/tests/checksums.rs +++ b/aws/sdk/integration-tests/s3/tests/checksums.rs @@ -226,7 +226,7 @@ async fn test_checksum_on_streaming_request<'a>( "x-amz-trailer is incorrect" ); assert_eq!( - HeaderValue::from_static(aws_http::content_encoding::header_value::AWS_CHUNKED), + HeaderValue::from_static(aws_runtime::content_encoding::header_value::AWS_CHUNKED), content_encoding, "content-encoding wasn't set to aws-chunked" ); diff --git a/aws/sdk/integration-tests/s3/tests/request_information_headers.rs b/aws/sdk/integration-tests/s3/tests/request_information_headers.rs index e12a4317a..47de27c43 100644 --- a/aws/sdk/integration-tests/s3/tests/request_information_headers.rs +++ b/aws/sdk/integration-tests/s3/tests/request_information_headers.rs @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -use aws_http::user_agent::AwsUserAgent; use aws_runtime::invocation_id::{InvocationId, PredefinedInvocationIdGenerator}; +use aws_runtime::user_agent::AwsUserAgent; use aws_sdk_s3::config::interceptors::BeforeSerializationInterceptorContextMut; use aws_sdk_s3::config::interceptors::FinalizerInterceptorContextRef; use aws_sdk_s3::config::retry::RetryConfig; diff --git a/aws/sdk/integration-tests/s3control/Cargo.toml b/aws/sdk/integration-tests/s3control/Cargo.toml index 9cad807f1..fc059b283 100644 --- a/aws/sdk/integration-tests/s3control/Cargo.toml +++ b/aws/sdk/integration-tests/s3control/Cargo.toml @@ -16,7 +16,6 @@ test-util = [] [dev-dependencies] aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } -aws-http = { path = "../../build/aws-sdk/sdk/aws-http" } aws-sdk-s3control = { path = "../../build/aws-sdk/sdk/s3control", features = ["test-util", "behavior-version-latest"] } aws-smithy-async = { path = "../../build/aws-sdk/sdk/aws-smithy-async" } aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] } diff --git a/aws/sdk/integration-tests/transcribestreaming/Cargo.toml b/aws/sdk/integration-tests/transcribestreaming/Cargo.toml index 0624ef567..221d7b4b7 100644 --- a/aws/sdk/integration-tests/transcribestreaming/Cargo.toml +++ b/aws/sdk/integration-tests/transcribestreaming/Cargo.toml @@ -11,7 +11,6 @@ publish = false [dev-dependencies] async-stream = "0.3.0" aws-credential-types = { path = "../../build/aws-sdk/sdk/aws-credential-types", features = ["test-util"] } -aws-http = { path = "../../build/aws-sdk/sdk/aws-http" } aws-sdk-transcribestreaming = { path = "../../build/aws-sdk/sdk/transcribestreaming", features = ["behavior-version-latest"] } aws-smithy-eventstream = { path = "../../build/aws-sdk/sdk/aws-smithy-eventstream" } aws-smithy-http = { path = "../../build/aws-sdk/sdk/aws-smithy-http" } -- GitLab