diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index 53ee76961a..afe79f7a13 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -17,6 +17,12 @@ references = ["smithy-rs#3011"] meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" } author = "jdisanti" +[[smithy-rs]] +message = "The `enableNewSmithyRuntime: middleware` opt-out flag in smithy-build.json has been removed and no longer opts out of the client orchestrator implementation. Middleware is no longer supported. If you haven't already upgraded to the orchestrator, see [the guide](https://github.com/awslabs/smithy-rs/discussions/2887)." +references = ["smithy-rs#3038"] +meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" } +author = "jdisanti" + [[aws-sdk-rust]] message = "HTTP connector configuration has changed significantly. See the [upgrade guidance](https://github.com/awslabs/smithy-rs/discussions/3022) for details." references = ["smithy-rs#3011"] @@ -279,3 +285,9 @@ message = "STS and SSO-based credential providers will now respect both `use_fip references = ["aws-sdk-rust#882", "smithy-rs#3007"] meta = { "breaking" = true, "tada" = true, "bug" = true } author = "Velfi" + +[[smithy-rs]] +message = "`SdkError` is no longer re-exported in generated server crates." +references = ["smithy-rs#3038"] +meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "server" } +author = "jdisanti" diff --git a/aws/rust-runtime/aws-endpoint/Cargo.toml b/aws/rust-runtime/aws-endpoint/Cargo.toml index 5a6846d2d6..75d1f7a1c1 100644 --- a/aws/rust-runtime/aws-endpoint/Cargo.toml +++ b/aws/rust-runtime/aws-endpoint/Cargo.toml @@ -2,18 +2,11 @@ name = "aws-endpoint" version = "0.0.0-smithy-rs-head" authors = ["AWS Rust SDK Team ", "Russell Cohen "] -description = "AWS SDK endpoint support." +description = "This crate is no longer used by the AWS SDK and is deprecated." edition = "2021" license = "Apache-2.0" repository = "https://github.com/awslabs/smithy-rs" -[dependencies] -aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" } -aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types"} -aws-types = { path = "../aws-types" } -http = "0.2.3" -tracing = "0.1" - [package.metadata.docs.rs] all-features = true targets = ["x86_64-unknown-linux-gnu"] diff --git a/aws/rust-runtime/aws-endpoint/README.md b/aws/rust-runtime/aws-endpoint/README.md index d4405ac7fa..ac8e3940e9 100644 --- a/aws/rust-runtime/aws-endpoint/README.md +++ b/aws/rust-runtime/aws-endpoint/README.md @@ -1,5 +1,6 @@ # aws-endpoint -This crate defines endpoint resolution logic specific to AWS services. + +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/awslabs/smithy-rs) code generator. In most cases, it should not be used directly. diff --git a/aws/rust-runtime/aws-endpoint/external-types.toml b/aws/rust-runtime/aws-endpoint/external-types.toml index a121d9e123..7fa182b39d 100644 --- a/aws/rust-runtime/aws-endpoint/external-types.toml +++ b/aws/rust-runtime/aws-endpoint/external-types.toml @@ -1,5 +1 @@ -allowed_external_types = [ - "aws_types::*", - "aws_smithy_http::property_bag::PropertyBag", - "aws_smithy_http::middleware::MapRequest", -] +allowed_external_types = [] diff --git a/aws/rust-runtime/aws-endpoint/src/lib.rs b/aws/rust-runtime/aws-endpoint/src/lib.rs index 640e4a8787..2819d85118 100644 --- a/aws/rust-runtime/aws-endpoint/src/lib.rs +++ b/aws/rust-runtime/aws-endpoint/src/lib.rs @@ -3,240 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -#![allow(clippy::derive_partial_eq_without_eq)] - -use std::error::Error; -use std::fmt; - -use aws_smithy_http::middleware::MapRequest; -use aws_smithy_http::operation::Request; -use aws_smithy_types::endpoint::Endpoint as SmithyEndpoint; -use aws_smithy_types::Document; - -use aws_types::region::{Region, SigningRegion}; -use aws_types::SigningName; - -/// Middleware Stage to add authentication information from a Smithy endpoint into the property bag -/// -/// AwsAuthStage implements [`MapRequest`](MapRequest). It will: -/// 1. Load an endpoint from the property bag -/// 2. Set the `SigningRegion` and `SigningName` in the property bag to drive downstream -/// signing middleware. -#[derive(Clone, Debug)] -pub struct AwsAuthStage; - -#[derive(Debug)] -enum AwsAuthStageErrorKind { - NoEndpointResolver, - EndpointResolutionError(Box), -} - -#[derive(Debug)] -pub struct AwsAuthStageError { - kind: AwsAuthStageErrorKind, -} - -impl fmt::Display for AwsAuthStageError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use AwsAuthStageErrorKind::*; - match &self.kind { - NoEndpointResolver => write!(f, "endpoint resolution failed: no endpoint present"), - EndpointResolutionError(_) => write!(f, "endpoint resolution failed"), - } - } -} - -impl Error for AwsAuthStageError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - use AwsAuthStageErrorKind::*; - match &self.kind { - EndpointResolutionError(source) => Some(source.as_ref() as _), - NoEndpointResolver => None, - } - } -} - -impl From for AwsAuthStageError { - fn from(kind: AwsAuthStageErrorKind) -> Self { - Self { kind } - } -} - -impl MapRequest for AwsAuthStage { - type Error = AwsAuthStageError; - - fn name(&self) -> &'static str { - "resolve_endpoint" - } - - fn apply(&self, request: Request) -> Result { - request.augment(|http_req, props| { - let endpoint = props - .get::() - .ok_or(AwsAuthStageErrorKind::NoEndpointResolver)?; - let (signing_region_override, signing_name_override) = smithy_to_aws(endpoint) - .map_err(|err| AwsAuthStageErrorKind::EndpointResolutionError(err))?; - - if let Some(signing_region) = signing_region_override { - props.insert(signing_region); - } - if let Some(signing_name) = signing_name_override { - props.insert(signing_name); - } - Ok(http_req) - }) - } -} - -type EndpointMetadata = (Option, Option); - -fn smithy_to_aws(value: &SmithyEndpoint) -> Result> { - // look for v4 as an auth scheme - let auth_schemes = match value.properties().get("authSchemes") { - Some(Document::Array(schemes)) => schemes, - // no auth schemes: - None => return Ok((None, None)), - _other => return Err("expected an array for authSchemes".into()), - }; - let auth_schemes = auth_schemes - .iter() - .flat_map(|doc| match doc { - Document::Object(map) => Some(map), - _ => None, - }) - .map(|it| { - let name = match it.get("name") { - Some(Document::String(s)) => Some(s.as_str()), - _ => None, - }; - (name, it) - }); - let (_, v4) = auth_schemes - .clone() - .find(|(name, _doc)| name.as_deref() == Some("sigv4")) - .ok_or_else(|| { - format!( - "No auth schemes were supported. The Rust SDK only supports sigv4. \ - The authentication schemes supported by this endpoint were: {:?}", - auth_schemes.flat_map(|(name, _)| name).collect::>() - ) - })?; - - let signing_scope = match v4.get("signingRegion") { - Some(Document::String(s)) => Some(SigningRegion::from(Region::new(s.clone()))), - None => None, - _ => return Err("unexpected type".into()), - }; - let signing_name = match v4.get("signingName") { - Some(Document::String(s)) => Some(SigningName::from(s.to_string())), - None => None, - _ => return Err("unexpected type".into()), - }; - Ok((signing_scope, signing_name)) -} - -#[cfg(test)] -mod test { - use std::collections::HashMap; - - use aws_smithy_http::body::SdkBody; - use aws_smithy_http::middleware::MapRequest; - use aws_smithy_http::operation; - use aws_smithy_types::endpoint::Endpoint; - use aws_smithy_types::Document; - use http::header::HOST; - - use aws_types::region::{Region, SigningRegion}; - use aws_types::SigningName; - - use crate::AwsAuthStage; - - #[test] - fn default_endpoint_updates_request() { - let endpoint = Endpoint::builder() - .url("kinesis.us-east-1.amazon.com") - .build(); - let req = http::Request::new(SdkBody::from("")); - let region = Region::new("us-east-1"); - let mut req = operation::Request::new(req); - { - let mut props = req.properties_mut(); - props.insert(SigningRegion::from(region.clone())); - props.insert(SigningName::from_static("kinesis")); - props.insert(endpoint); - }; - let req = AwsAuthStage.apply(req).expect("should succeed"); - assert_eq!(req.properties().get(), Some(&SigningRegion::from(region))); - assert_eq!( - req.properties().get(), - Some(&SigningName::from_static("kinesis")) - ); - - assert!(req.http().headers().get(HOST).is_none()); - assert!( - req.properties().get::().is_some(), - "Endpoint middleware MUST leave the result in the bag" - ); - } - - #[test] - fn sets_service_override_when_set() { - let endpoint = Endpoint::builder() - .url("kinesis.us-east-override.amazon.com") - .property( - "authSchemes", - vec![Document::Object({ - let mut out = HashMap::new(); - out.insert("name".to_string(), "sigv4".to_string().into()); - out.insert( - "signingName".to_string(), - "qldb-override".to_string().into(), - ); - out.insert( - "signingRegion".to_string(), - "us-east-override".to_string().into(), - ); - out - })], - ) - .build(); - let req = http::Request::new(SdkBody::from("")); - let region = Region::new("us-east-1"); - let mut req = operation::Request::new(req); - { - let mut props = req.properties_mut(); - props.insert(region); - props.insert(SigningName::from_static("qldb")); - props.insert(endpoint); - }; - let req = AwsAuthStage.apply(req).expect("should succeed"); - assert_eq!( - req.properties().get(), - Some(&SigningRegion::from_static("us-east-override")) - ); - assert_eq!( - req.properties().get(), - Some(&SigningName::from_static("qldb-override")) - ); - } - - #[test] - fn supports_fallback_when_scope_is_unset() { - let endpoint = Endpoint::builder().url("www.service.com").build(); - let req = http::Request::new(SdkBody::from("")); - let region = SigningRegion::from_static("us-east-1"); - let mut req = operation::Request::new(req); - { - let mut props = req.properties_mut(); - props.insert(region.clone()); - props.insert(SigningName::from_static("qldb")); - props.insert(endpoint); - }; - let req = AwsAuthStage.apply(req).expect("should succeed"); - assert_eq!(req.properties().get(), Some(®ion)); - assert_eq!( - req.properties().get(), - Some(&SigningName::from_static("qldb")) - ); - } -} +//! This crate is no longer used by the AWS SDK and is deprecated. diff --git a/aws/rust-runtime/aws-http/Cargo.toml b/aws/rust-runtime/aws-http/Cargo.toml index fc25360575..9c6e1eb37e 100644 --- a/aws/rust-runtime/aws-http/Cargo.toml +++ b/aws/rust-runtime/aws-http/Cargo.toml @@ -8,7 +8,6 @@ license = "Apache-2.0" repository = "https://github.com/awslabs/smithy-rs" [dependencies] -aws-credential-types = { path = "../aws-credential-types" } aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" } aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types" } aws-types = { path = "../aws-types" } @@ -16,22 +15,11 @@ bytes = "1.1" http = "0.2.3" http-body = "0.4.5" tracing = "0.1" -percent-encoding = "2.1.0" pin-project-lite = "0.2.9" [dev-dependencies] -async-trait = "0.1.50" -aws-credential-types = { path = "../aws-credential-types", features = ["test-util"] } -aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["rt-tokio"] } -aws-smithy-checksums = { path = "../../../rust-runtime/aws-smithy-checksums" } -aws-smithy-protocol-test = { path = "../../../rust-runtime/aws-smithy-protocol-test" } bytes-utils = "0.1.2" -env_logger = "0.9" -tokio = { version = "1.23.1", features = ["macros", "rt", "rt-multi-thread", "test-util", "time"] } -tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -proptest = "1.2" -serde = { version = "1", features = ["derive"]} -serde_json = "1" +tokio = { version = "1.23.1", features = ["macros", "rt", "time"] } [package.metadata.docs.rs] all-features = true diff --git a/aws/rust-runtime/aws-http/external-types.toml b/aws/rust-runtime/aws-http/external-types.toml index d2e362f515..88240dcad0 100644 --- a/aws/rust-runtime/aws-http/external-types.toml +++ b/aws/rust-runtime/aws-http/external-types.toml @@ -1,8 +1,9 @@ allowed_external_types = [ - "aws_credential_types::*", - "aws_smithy_http::*", - "aws_smithy_types::*", - "aws_types::*", + "aws_smithy_http::body::Error", + "aws_smithy_types::config_bag::storable::Storable", + "aws_smithy_types::config_bag::storable::StoreReplace", + "aws_types::app_name::AppName", + "aws_types::os_shim_internal::Env", "bytes::bytes::Bytes", "http_body::Body", ] diff --git a/aws/rust-runtime/aws-http/src/auth.rs b/aws/rust-runtime/aws-http/src/auth.rs deleted file mode 100644 index 98e0e219bb..0000000000 --- a/aws/rust-runtime/aws-http/src/auth.rs +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use aws_credential_types::cache::{ProvideCachedCredentials, SharedCredentialsCache}; -use aws_credential_types::provider::error::CredentialsError; -use aws_smithy_http::middleware::AsyncMapRequest; -use aws_smithy_http::operation::Request; -use aws_smithy_http::property_bag::PropertyBag; -use std::future::Future; -use std::pin::Pin; - -/// Sets the credentials cache in the given property bag. -pub fn set_credentials_cache(bag: &mut PropertyBag, cache: SharedCredentialsCache) { - bag.insert(cache); -} - -/// Middleware stage that loads credentials from a [SharedCredentialsCache](aws_credential_types::cache::SharedCredentialsCache) -/// and places them in the property bag of the request. -/// -/// [CredentialsStage] implements [`AsyncMapRequest`](aws_smithy_http::middleware::AsyncMapRequest), and: -/// 1. Retrieves a `SharedCredentialsCache` from the property bag. -/// 2. Calls the credential cache's `provide_cached_credentials` and awaits its result. -/// 3. Places returned `Credentials` into the property bag to drive downstream signing middleware. -#[derive(Clone, Debug, Default)] -#[non_exhaustive] -pub struct CredentialsStage; - -impl CredentialsStage { - /// Creates a new credentials stage. - pub fn new() -> Self { - CredentialsStage - } - - async fn load_creds(mut request: Request) -> Result { - let credentials_cache = request - .properties() - .get::() - .cloned(); - let credentials_cache = match credentials_cache { - Some(credentials_cache) => credentials_cache, - None => { - tracing::info!("no credentials cache for request"); - return Ok(request); - } - }; - match credentials_cache.provide_cached_credentials().await { - Ok(creds) => { - request.properties_mut().insert(creds); - } - // ignore the case where there is no credentials cache wired up - Err(CredentialsError::CredentialsNotLoaded { .. }) => { - tracing::info!("credentials cache returned CredentialsNotLoaded, ignoring") - } - // if we get another error class, there is probably something actually wrong that the user will - // want to know about - Err(other) => return Err(other.into()), - } - Ok(request) - } -} - -mod error { - use aws_credential_types::provider::error::CredentialsError; - use std::error::Error as StdError; - use std::fmt; - - /// Failures that can occur in the credentials middleware. - #[derive(Debug)] - pub struct CredentialsStageError { - source: CredentialsError, - } - - impl StdError for CredentialsStageError { - fn source(&self) -> Option<&(dyn StdError + 'static)> { - Some(&self.source as _) - } - } - - impl fmt::Display for CredentialsStageError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "failed to load credentials from the credentials cache") - } - } - - impl From for CredentialsStageError { - fn from(source: CredentialsError) -> Self { - CredentialsStageError { source } - } - } -} - -pub use error::*; - -type BoxFuture = Pin + Send>>; - -impl AsyncMapRequest for CredentialsStage { - type Error = CredentialsStageError; - type Future = Pin> + Send + 'static>>; - - fn name(&self) -> &'static str { - "retrieve_credentials" - } - - fn apply(&self, request: Request) -> BoxFuture> { - Box::pin(Self::load_creds(request)) - } -} - -#[cfg(test)] -mod tests { - use super::set_credentials_cache; - use super::CredentialsStage; - use aws_credential_types::cache::{ - CredentialsCache, ProvideCachedCredentials, SharedCredentialsCache, - }; - use aws_credential_types::credential_fn::provide_credentials_fn; - use aws_credential_types::provider::SharedCredentialsProvider; - use aws_credential_types::provider::{error::CredentialsError, future}; - use aws_credential_types::Credentials; - use aws_smithy_http::body::SdkBody; - use aws_smithy_http::middleware::AsyncMapRequest; - use aws_smithy_http::operation; - - #[derive(Debug)] - struct Unhandled; - impl ProvideCachedCredentials for Unhandled { - fn provide_cached_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> - where - Self: 'a, - { - future::ProvideCredentials::ready(Err(CredentialsError::unhandled("whoops"))) - } - } - - #[derive(Debug)] - struct NoCreds; - impl ProvideCachedCredentials for NoCreds { - fn provide_cached_credentials<'a>(&'a self) -> future::ProvideCredentials<'a> - where - Self: 'a, - { - future::ProvideCredentials::ready(Err(CredentialsError::not_loaded("no creds"))) - } - } - - #[tokio::test] - async fn no_credential_cache_is_ok() { - let req = operation::Request::new(http::Request::new(SdkBody::from("some body"))); - CredentialsStage::new() - .apply(req) - .await - .expect("no credentials cache should not populate credentials"); - } - - #[tokio::test] - async fn credentials_cache_failure_is_failure() { - let mut req = operation::Request::new(http::Request::new(SdkBody::from("some body"))); - set_credentials_cache( - &mut req.properties_mut(), - SharedCredentialsCache::new(Unhandled), - ); - CredentialsStage::new() - .apply(req) - .await - .expect_err("no credentials cache should not populate credentials"); - } - - #[tokio::test] - async fn credentials_not_loaded_is_ok() { - let mut req = operation::Request::new(http::Request::new(SdkBody::from("some body"))); - set_credentials_cache( - &mut req.properties_mut(), - SharedCredentialsCache::new(NoCreds), - ); - CredentialsStage::new() - .apply(req) - .await - .expect("credentials not loaded is OK"); - } - - #[tokio::test] - async fn async_map_request_apply_populates_credentials() { - let mut req = operation::Request::new(http::Request::new(SdkBody::from("some body"))); - let credentials_cache = CredentialsCache::lazy_builder() - .into_credentials_cache() - .create_cache(SharedCredentialsProvider::new(provide_credentials_fn( - || async { Ok(Credentials::for_tests()) }, - ))); - set_credentials_cache(&mut req.properties_mut(), credentials_cache); - let req = CredentialsStage::new() - .apply(req) - .await - .expect("credentials cache is in the bag; should succeed"); - assert!( - req.properties().get::().is_some(), - "it should set credentials on the request config" - ); - } -} diff --git a/aws/rust-runtime/aws-http/src/lib.rs b/aws/rust-runtime/aws-http/src/lib.rs index d5307bcba3..4c0f72080f 100644 --- a/aws/rust-runtime/aws-http/src/lib.rs +++ b/aws/rust-runtime/aws-http/src/lib.rs @@ -14,15 +14,6 @@ unreachable_pub )] -/// Credentials middleware -pub mod auth; - -/// Recursion Detection middleware -pub mod recursion_detection; - -/// AWS-specific retry logic -pub mod retry; - /// User agent middleware pub mod user_agent; diff --git a/aws/rust-runtime/aws-http/src/recursion_detection.rs b/aws/rust-runtime/aws-http/src/recursion_detection.rs deleted file mode 100644 index 3cc27615d3..0000000000 --- a/aws/rust-runtime/aws-http/src/recursion_detection.rs +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use crate::recursion_detection::env::TRACE_ID; -use aws_smithy_http::middleware::MapRequest; -use aws_smithy_http::operation::Request; -use aws_types::os_shim_internal::Env; -use http::HeaderValue; -use percent_encoding::{percent_encode, CONTROLS}; -use std::borrow::Cow; - -// TODO(enableNewSmithyRuntimeCleanup): Delete this module - -/// Recursion Detection Middleware -/// -/// This middleware inspects the value of the `AWS_LAMBDA_FUNCTION_NAME` and `_X_AMZN_TRACE_ID` environment -/// variables to detect if the request is being invoked in a lambda function. If it is, the `X-Amzn-Trace-Id` header -/// will be set. This enables downstream services to prevent accidentally infinitely recursive invocations spawned -/// from lambda. -#[non_exhaustive] -#[derive(Default, Debug, Clone)] -pub struct RecursionDetectionStage { - env: Env, -} - -impl RecursionDetectionStage { - /// Creates a new `RecursionDetectionStage` - pub fn new() -> Self { - Self::default() - } -} - -impl MapRequest for RecursionDetectionStage { - type Error = std::convert::Infallible; - - fn name(&self) -> &'static str { - "recursion_detection" - } - - fn apply(&self, request: Request) -> Result { - request.augment(|mut req, _conf| { - augument_request(&mut req, &self.env); - Ok(req) - }) - } -} - -const TRACE_ID_HEADER: &str = "x-amzn-trace-id"; - -mod env { - pub(super) const LAMBDA_FUNCTION_NAME: &str = "AWS_LAMBDA_FUNCTION_NAME"; - pub(super) const TRACE_ID: &str = "_X_AMZN_TRACE_ID"; -} - -/// Set the trace id header from the request -fn augument_request(req: &mut http::Request, env: &Env) { - if req.headers().contains_key(TRACE_ID_HEADER) { - return; - } - if let (Ok(_function_name), Ok(trace_id)) = - (env.get(env::LAMBDA_FUNCTION_NAME), env.get(TRACE_ID)) - { - req.headers_mut() - .insert(TRACE_ID_HEADER, encode_header(trace_id.as_bytes())); - } -} - -/// Encodes a byte slice as a header. -/// -/// ASCII control characters are percent encoded which ensures that all byte sequences are valid headers -fn encode_header(value: &[u8]) -> HeaderValue { - let value: Cow<'_, str> = percent_encode(value, CONTROLS).into(); - HeaderValue::from_bytes(value.as_bytes()).expect("header is encoded, header must be valid") -} - -#[cfg(test)] -mod test { - use crate::recursion_detection::{encode_header, RecursionDetectionStage}; - use aws_smithy_http::body::SdkBody; - use aws_smithy_http::middleware::MapRequest; - use aws_smithy_http::operation; - use aws_smithy_protocol_test::{assert_ok, validate_headers}; - use aws_types::os_shim_internal::Env; - use http::HeaderValue; - use proptest::{prelude::*, proptest}; - use serde::Deserialize; - use std::collections::HashMap; - - proptest! { - #[test] - fn header_encoding_never_panics(s in any::>()) { - encode_header(&s); - } - } - - #[test] - fn every_char() { - let buff = (0..=255).collect::>(); - assert_eq!( - encode_header(&buff), - HeaderValue::from_static( - r##"%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF"## - ) - ); - } - - #[test] - fn run_tests() { - let test_cases: Vec = - serde_json::from_str(include_str!("../test-data/recursion-detection.json")) - .expect("invalid test case"); - for test_case in test_cases { - check(test_case) - } - } - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct TestCase { - env: HashMap, - request_headers_before: Vec, - request_headers_after: Vec, - } - - impl TestCase { - fn env(&self) -> Env { - Env::from(self.env.clone()) - } - - /// Headers on the input request - fn request_headers_before(&self) -> impl Iterator { - Self::split_headers(&self.request_headers_before) - } - - /// Headers on the output request - fn request_headers_after(&self) -> impl Iterator { - Self::split_headers(&self.request_headers_after) - } - - /// Split text headers on `: ` - fn split_headers(headers: &[String]) -> impl Iterator { - headers - .iter() - .map(|header| header.split_once(": ").expect("header must contain :")) - } - } - - fn check(test_case: TestCase) { - let env = test_case.env(); - let mut req = http::Request::builder(); - for (k, v) in test_case.request_headers_before() { - req = req.header(k, v); - } - let req = req.body(SdkBody::empty()).expect("must be valid"); - let req = operation::Request::new(req); - let augmented_req = RecursionDetectionStage { env } - .apply(req) - .expect("stage must succeed"); - for k in augmented_req.http().headers().keys() { - assert_eq!( - augmented_req.http().headers().get_all(k).iter().count(), - 1, - "No duplicated headers" - ) - } - assert_ok(validate_headers( - augmented_req.http().headers(), - test_case.request_headers_after(), - )) - } -} diff --git a/aws/rust-runtime/aws-http/src/request_id.rs b/aws/rust-runtime/aws-http/src/request_id.rs index 7713328f7c..692e9cc86f 100644 --- a/aws/rust-runtime/aws-http/src/request_id.rs +++ b/aws/rust-runtime/aws-http/src/request_id.rs @@ -4,7 +4,6 @@ */ use aws_smithy_http::http::HttpHeaders; -use aws_smithy_http::operation; use aws_smithy_http::result::SdkError; use aws_smithy_types::error::metadata::{ Builder as ErrorMetadataBuilder, ErrorMetadata, ProvideErrorMetadata, @@ -46,12 +45,6 @@ impl RequestId for Unhandled { } } -impl RequestId for operation::Response { - fn request_id(&self) -> Option<&str> { - extract_request_id(self.http().headers()) - } -} - impl RequestId for http::Response { fn request_id(&self) -> Option<&str> { extract_request_id(self.headers()) @@ -106,18 +99,15 @@ mod tests { #[test] fn test_request_id_sdk_error() { - let without_request_id = - || operation::Response::new(Response::builder().body(SdkBody::empty()).unwrap()); + let without_request_id = || Response::builder().body(SdkBody::empty()).unwrap(); let with_request_id = || { - operation::Response::new( - Response::builder() - .header( - "x-amzn-requestid", - HeaderValue::from_static("some-request-id"), - ) - .body(SdkBody::empty()) - .unwrap(), - ) + Response::builder() + .header( + "x-amzn-requestid", + HeaderValue::from_static("some-request-id"), + ) + .body(SdkBody::empty()) + .unwrap() }; assert_eq!( None, diff --git a/aws/rust-runtime/aws-http/src/retry.rs b/aws/rust-runtime/aws-http/src/retry.rs deleted file mode 100644 index 8e7d0fcbb5..0000000000 --- a/aws/rust-runtime/aws-http/src/retry.rs +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -//! AWS-specific retry logic - -use aws_smithy_http::result::SdkError; -use aws_smithy_http::retry::{ClassifyRetry, DefaultResponseRetryClassifier}; -use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind, RetryKind}; -use std::time::Duration; - -const TRANSIENT_ERROR_STATUS_CODES: &[u16] = &[500, 502, 503, 504]; -const THROTTLING_ERRORS: &[&str] = &[ - "Throttling", - "ThrottlingException", - "ThrottledException", - "RequestThrottledException", - "TooManyRequestsException", - "ProvisionedThroughputExceededException", - "TransactionInProgressException", - "RequestLimitExceeded", - "BandwidthLimitExceeded", - "LimitExceededException", - "RequestThrottled", - "SlowDown", - "PriorRequestNotComplete", - "EC2ThrottledException", -]; -const TRANSIENT_ERRORS: &[&str] = &["RequestTimeout", "RequestTimeoutException"]; - -/// Implementation of [`ClassifyRetry`] that classifies AWS error codes. -/// -/// In order of priority: -/// 1. The `x-amz-retry-after` header is checked -/// 2. The modeled error retry mode is checked -/// 3. The code is checked against a predetermined list of throttling errors & transient error codes -/// 4. The status code is checked against a predetermined list of status codes -#[non_exhaustive] -#[derive(Clone, Debug)] -pub struct AwsResponseRetryClassifier; - -impl AwsResponseRetryClassifier { - /// Create an `AwsResponseRetryClassifier` with the default set of known error & status codes - pub fn new() -> Self { - Self - } -} - -impl Default for AwsResponseRetryClassifier { - fn default() -> Self { - Self::new() - } -} - -impl ClassifyRetry> for AwsResponseRetryClassifier -where - E: ProvideErrorKind, -{ - fn classify_retry(&self, result: Result<&T, &SdkError>) -> RetryKind { - // Run common retry classification logic from aws-smithy-http, and if it yields - // a `RetryKind`, then return that immediately. Otherwise, continue on to run some - // AWS SDK specific classification logic. - let (err, response) = match DefaultResponseRetryClassifier::try_extract_err_response(result) - { - Ok(extracted) => extracted, - Err(retry_kind) => return retry_kind, - }; - if let Some(retry_after_delay) = response - .http() - .headers() - .get("x-amz-retry-after") - .and_then(|header| header.to_str().ok()) - .and_then(|header| header.parse::().ok()) - { - return RetryKind::Explicit(Duration::from_millis(retry_after_delay)); - } - if let Some(kind) = err.retryable_error_kind() { - return RetryKind::Error(kind); - }; - if let Some(code) = err.code() { - if THROTTLING_ERRORS.contains(&code) { - return RetryKind::Error(ErrorKind::ThrottlingError); - } - if TRANSIENT_ERRORS.contains(&code) { - return RetryKind::Error(ErrorKind::TransientError); - } - }; - if TRANSIENT_ERROR_STATUS_CODES.contains(&response.http().status().as_u16()) { - return RetryKind::Error(ErrorKind::TransientError); - }; - // TODO(https://github.com/awslabs/smithy-rs/issues/966): IDPCommuncation error needs to be retried - RetryKind::UnretryableFailure - } -} - -#[cfg(test)] -mod test { - use crate::retry::AwsResponseRetryClassifier; - use aws_smithy_http::body::SdkBody; - use aws_smithy_http::operation; - use aws_smithy_http::result::{SdkError, SdkSuccess}; - use aws_smithy_http::retry::ClassifyRetry; - use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind, RetryKind}; - use std::fmt; - use std::time::Duration; - - #[derive(Debug)] - struct UnmodeledError; - impl fmt::Display for UnmodeledError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "UnmodeledError") - } - } - impl std::error::Error for UnmodeledError {} - - struct CodedError { - code: &'static str, - } - - impl ProvideErrorKind for UnmodeledError { - fn retryable_error_kind(&self) -> Option { - None - } - - fn code(&self) -> Option<&str> { - None - } - } - - impl ProvideErrorKind for CodedError { - fn retryable_error_kind(&self) -> Option { - None - } - - fn code(&self) -> Option<&str> { - Some(self.code) - } - } - - fn make_err( - err: E, - raw: http::Response<&'static str>, - ) -> Result, SdkError> { - Err(SdkError::service_error( - err, - operation::Response::new(raw.map(SdkBody::from)), - )) - } - - #[test] - fn not_an_error() { - let policy = AwsResponseRetryClassifier::new(); - let test_response = http::Response::new("OK"); - assert_eq!( - policy.classify_retry(make_err(UnmodeledError, test_response).as_ref()), - RetryKind::UnretryableFailure - ); - } - - #[test] - fn classify_by_response_status() { - let policy = AwsResponseRetryClassifier::new(); - let test_resp = http::Response::builder() - .status(500) - .body("error!") - .unwrap(); - assert_eq!( - policy.classify_retry(make_err(UnmodeledError, test_resp).as_ref()), - RetryKind::Error(ErrorKind::TransientError) - ); - } - - #[test] - fn classify_by_response_status_not_retryable() { - let policy = AwsResponseRetryClassifier::new(); - let test_resp = http::Response::builder() - .status(408) - .body("error!") - .unwrap(); - assert_eq!( - policy.classify_retry(make_err(UnmodeledError, test_resp).as_ref()), - RetryKind::UnretryableFailure - ); - } - - #[test] - fn classify_by_error_code() { - let test_response = http::Response::new("OK"); - let policy = AwsResponseRetryClassifier::new(); - - assert_eq!( - policy.classify_retry( - make_err(CodedError { code: "Throttling" }, test_response).as_ref() - ), - RetryKind::Error(ErrorKind::ThrottlingError) - ); - - let test_response = http::Response::new("OK"); - assert_eq!( - policy.classify_retry( - make_err( - CodedError { - code: "RequestTimeout" - }, - test_response, - ) - .as_ref() - ), - RetryKind::Error(ErrorKind::TransientError) - ) - } - - #[test] - fn classify_generic() { - let err = aws_smithy_types::Error::builder().code("SlowDown").build(); - let test_response = http::Response::new("OK"); - let policy = AwsResponseRetryClassifier::new(); - assert_eq!( - policy.classify_retry(make_err(err, test_response).as_ref()), - RetryKind::Error(ErrorKind::ThrottlingError) - ); - } - - #[test] - fn classify_by_error_kind() { - struct ModeledRetries; - let test_response = http::Response::new("OK"); - impl ProvideErrorKind for ModeledRetries { - fn retryable_error_kind(&self) -> Option { - Some(ErrorKind::ClientError) - } - - fn code(&self) -> Option<&str> { - // code should not be called when `error_kind` is provided - unimplemented!() - } - } - - let policy = AwsResponseRetryClassifier::new(); - - assert_eq!( - policy.classify_retry(make_err(ModeledRetries, test_response).as_ref()), - RetryKind::Error(ErrorKind::ClientError) - ); - } - - #[test] - fn test_retry_after_header() { - let policy = AwsResponseRetryClassifier::new(); - let test_response = http::Response::builder() - .header("x-amz-retry-after", "5000") - .body("retry later") - .unwrap(); - - assert_eq!( - policy.classify_retry(make_err(UnmodeledError, test_response).as_ref()), - RetryKind::Explicit(Duration::from_millis(5000)) - ); - } - - #[test] - fn classify_response_error() { - let policy = AwsResponseRetryClassifier::new(); - assert_eq!( - policy.classify_retry( - Result::, SdkError>::Err(SdkError::response_error( - UnmodeledError, - operation::Response::new(http::Response::new("OK").map(SdkBody::from)), - )) - .as_ref() - ), - RetryKind::Error(ErrorKind::TransientError) - ); - } - - #[test] - fn test_timeout_error() { - let policy = AwsResponseRetryClassifier::new(); - let err: Result<(), SdkError> = Err(SdkError::timeout_error("blah")); - assert_eq!( - policy.classify_retry(err.as_ref()), - RetryKind::Error(ErrorKind::TransientError) - ); - } -} diff --git a/aws/rust-runtime/aws-http/src/user_agent.rs b/aws/rust-runtime/aws-http/src/user_agent.rs index 27abba7bba..1e26da4d4e 100644 --- a/aws/rust-runtime/aws-http/src/user_agent.rs +++ b/aws/rust-runtime/aws-http/src/user_agent.rs @@ -3,24 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -use aws_smithy_http::middleware::MapRequest; -use aws_smithy_http::operation::Request; 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::{HeaderName, InvalidHeaderValue, USER_AGENT}; -use http::HeaderValue; use std::borrow::Cow; -use std::convert::TryFrom; use std::error::Error; use std::fmt; /// AWS User Agent /// -/// Ths struct should be inserted into the [`PropertyBag`](aws_smithy_http::operation::Request::properties) -/// during operation construction. [`UserAgentStage`](UserAgentStage) reads `AwsUserAgent` -/// from the property bag and sets the `User-Agent` and `x-amz-user-agent` headers. +/// 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, @@ -522,116 +517,12 @@ impl fmt::Display for ExecEnvMetadata { } } -// TODO(enableNewSmithyRuntimeCleanup): Delete the user agent Tower middleware and consider moving all the remaining code into aws-runtime - -/// User agent middleware -#[non_exhaustive] -#[derive(Default, Clone, Debug)] -pub struct UserAgentStage; - -impl UserAgentStage { - /// Creates a new `UserAgentStage` - pub fn new() -> Self { - Self - } -} - -#[derive(Debug)] -enum UserAgentStageErrorKind { - /// There was no [`AwsUserAgent`] in the property bag. - UserAgentMissing, - /// The formatted user agent string is not a valid HTTP header value. This indicates a bug. - InvalidHeader(InvalidHeaderValue), -} - -/// Failures that can arise from the user agent middleware -#[derive(Debug)] -pub struct UserAgentStageError { - kind: UserAgentStageErrorKind, -} - -impl UserAgentStageError { - // `pub(crate)` method instead of implementing `From` so that we - // don't have to expose `InvalidHeaderValue` in public API. - pub(crate) fn from_invalid_header(value: InvalidHeaderValue) -> Self { - Self { - kind: UserAgentStageErrorKind::InvalidHeader(value), - } - } -} - -impl Error for UserAgentStageError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - use UserAgentStageErrorKind::*; - match &self.kind { - InvalidHeader(source) => Some(source as _), - UserAgentMissing => None, - } - } -} - -impl fmt::Display for UserAgentStageError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use UserAgentStageErrorKind::*; - match self.kind { - UserAgentMissing => write!(f, "user agent missing from property bag"), - InvalidHeader(_) => { - write!(f, "provided user agent header was invalid (this is a bug)") - } - } - } -} - -impl From for UserAgentStageError { - fn from(kind: UserAgentStageErrorKind) -> Self { - Self { kind } - } -} - -#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this -const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent"); - -impl MapRequest for UserAgentStage { - type Error = UserAgentStageError; - - fn name(&self) -> &'static str { - "generate_user_agent" - } - - fn apply(&self, request: Request) -> Result { - request.augment(|mut req, conf| { - let ua = conf - .get::() - .ok_or(UserAgentStageErrorKind::UserAgentMissing)?; - req.headers_mut().append( - USER_AGENT, - HeaderValue::try_from(ua.ua_header()) - .map_err(UserAgentStageError::from_invalid_header)?, - ); - req.headers_mut().append( - X_AMZ_USER_AGENT, - HeaderValue::try_from(ua.aws_ua_header()) - .map_err(UserAgentStageError::from_invalid_header)?, - ); - Ok(req) - }) - } -} - #[cfg(test)] mod test { - use crate::user_agent::{ - AdditionalMetadata, ApiMetadata, AwsUserAgent, ConfigMetadata, FrameworkMetadata, - UserAgentStage, - }; - use crate::user_agent::{FeatureMetadata, X_AMZ_USER_AGENT}; - use aws_smithy_http::body::SdkBody; - use aws_smithy_http::middleware::MapRequest; - use aws_smithy_http::operation; + use super::*; use aws_types::app_name::AppName; use aws_types::build_metadata::OsFamily; use aws_types::os_shim_internal::Env; - use http::header::USER_AGENT; use std::borrow::Cow; fn make_deterministic(ua: &mut AwsUserAgent) { @@ -771,32 +662,6 @@ mod test { "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0" ); } - - #[test] - fn ua_stage_adds_headers() { - let stage = UserAgentStage::new(); - let req = operation::Request::new(http::Request::new(SdkBody::from("some body"))); - stage - .apply(req) - .expect_err("adding UA should fail without a UA set"); - let mut req = operation::Request::new(http::Request::new(SdkBody::from("some body"))); - req.properties_mut() - .insert(AwsUserAgent::new_from_environment( - Env::from_slice(&[]), - ApiMetadata { - service_id: "dynamodb".into(), - version: "0.123", - }, - )); - let req = stage.apply(req).expect("setting user agent should succeed"); - let (req, _) = req.into_parts(); - req.headers() - .get(USER_AGENT) - .expect("UA header should be set"); - req.headers() - .get(X_AMZ_USER_AGENT) - .expect("UA header should be set"); - } } /* diff --git a/aws/rust-runtime/aws-hyper/README.md b/aws/rust-runtime/aws-hyper/README.md index ace56e1608..311b8c447f 100644 --- a/aws/rust-runtime/aws-hyper/README.md +++ b/aws/rust-runtime/aws-hyper/README.md @@ -1,6 +1,6 @@ # AWS Default Middleware -This crate has been removed. Middleware is now defined on a per-service basis. +This crate is no longer used by the AWS SDK. This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/awslabs/smithy-rs) code generator. In most cases, it should not be used directly. diff --git a/aws/rust-runtime/aws-hyper/src/lib.rs b/aws/rust-runtime/aws-hyper/src/lib.rs index 9a037350a7..232f28ce9f 100644 --- a/aws/rust-runtime/aws-hyper/src/lib.rs +++ b/aws/rust-runtime/aws-hyper/src/lib.rs @@ -7,4 +7,4 @@ since = "0.3.0", note = "The functionality of this crate is included in individual AWS services." )] -//! This crate has been removed. Its functionality has be merged into aws-smithy-client and individual AWS services. +//! This crate is no longer used by the AWS SDK. Its functionality has be merged into aws-smithy-runtime and individual AWS services. diff --git a/aws/rust-runtime/aws-inlineable/Cargo.toml b/aws/rust-runtime/aws-inlineable/Cargo.toml index 28f4aeb212..56ccc8068e 100644 --- a/aws/rust-runtime/aws-inlineable/Cargo.toml +++ b/aws/rust-runtime/aws-inlineable/Cargo.toml @@ -22,26 +22,20 @@ aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api" aws-smithy-runtime = { path = "../../../rust-runtime/aws-smithy-runtime", features = ["client"] } aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types" } aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["rt-tokio"] } -aws-types = { path = "../aws-types" } bytes = "1" -bytes-utils = "0.1.1" hex = "0.4.3" http = "0.2.9" http-body = "0.4.5" -md-5 = "0.10.1" ring = "0.16" -tokio = { version = "1.23.1", features = ["full"] } -tokio-stream = "0.1.5" +tokio = "1.23.1" tracing = "0.1" [dev-dependencies] -aws-credential-types = { path = "../aws-credential-types", features = ["test-util"] } -aws-smithy-client = { path = "../../../rust-runtime/aws-smithy-client", features = ["test-util"] } +aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["test-util"] } aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http", features = ["rt-tokio"] } aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api", features = ["test-util"] } tempfile = "3.6.0" -tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["test-util"] } +tokio = { version = "1.23.1", features = ["macros", "rt", "io-util"] } [package.metadata.docs.rs] all-features = true diff --git a/aws/rust-runtime/aws-inlineable/src/lib.rs b/aws/rust-runtime/aws-inlineable/src/lib.rs index 8ff2f5f478..05679f1f29 100644 --- a/aws/rust-runtime/aws-inlineable/src/lib.rs +++ b/aws/rust-runtime/aws-inlineable/src/lib.rs @@ -37,9 +37,6 @@ pub mod s3_request_id; /// Glacier-specific behavior pub mod glacier_interceptors; -/// Strip prefixes from IDs returned by Route53 operations when those IDs are used to construct requests -pub mod route53_resource_id_preprocessor_middleware; - /// Strip prefixes from IDs returned by Route53 operations when those IDs are used to construct requests pub mod route53_resource_id_preprocessor; diff --git a/aws/rust-runtime/aws-inlineable/src/route53_resource_id_preprocessor_middleware.rs b/aws/rust-runtime/aws-inlineable/src/route53_resource_id_preprocessor_middleware.rs deleted file mode 100644 index 545be270de..0000000000 --- a/aws/rust-runtime/aws-inlineable/src/route53_resource_id_preprocessor_middleware.rs +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -// TODO(enableNewSmithyRuntimeCleanup): Delete this module - -// This function is only used to strip prefixes from resource IDs at the time they're passed as -// input to a request. Resource IDs returned in responses may or may not include a prefix. -/// Strip the resource type prefix from resource ID return -pub fn trim_resource_id(resource_id: &mut Option) { - const PREFIXES: &[&str] = &[ - "/hostedzone/", - "hostedzone/", - "/change/", - "change/", - "/delegationset/", - "delegationset/", - ]; - - for prefix in PREFIXES { - if let Some(id) = resource_id - .as_deref() - .unwrap_or_default() - .strip_prefix(prefix) - { - *resource_id = Some(id.to_string()); - return; - } - } -} - -#[cfg(test)] -mod test { - use crate::route53_resource_id_preprocessor_middleware::trim_resource_id; - - #[test] - fn does_not_change_regular_zones() { - struct OperationInput { - resource: Option, - } - - let mut operation = OperationInput { - resource: Some("Z0441723226OZ66S5ZCNZ".to_string()), - }; - trim_resource_id(&mut operation.resource); - assert_eq!( - &operation.resource.unwrap_or_default(), - "Z0441723226OZ66S5ZCNZ" - ); - } - - #[test] - fn sanitizes_prefixed_zone() { - struct OperationInput { - change_id: Option, - } - - let mut operation = OperationInput { - change_id: Some("/change/Z0441723226OZ66S5ZCNZ".to_string()), - }; - trim_resource_id(&mut operation.change_id); - assert_eq!( - &operation.change_id.unwrap_or_default(), - "Z0441723226OZ66S5ZCNZ" - ); - } - - #[test] - fn allow_no_leading_slash() { - struct OperationInput { - hosted_zone: Option, - } - - let mut operation = OperationInput { - hosted_zone: Some("hostedzone/Z0441723226OZ66S5ZCNZ".to_string()), - }; - trim_resource_id(&mut operation.hosted_zone); - assert_eq!( - &operation.hosted_zone.unwrap_or_default(), - "Z0441723226OZ66S5ZCNZ" - ); - } -} diff --git a/aws/rust-runtime/aws-inlineable/src/s3_request_id.rs b/aws/rust-runtime/aws-inlineable/src/s3_request_id.rs index d2e64a7038..226eed5f7a 100644 --- a/aws/rust-runtime/aws-inlineable/src/s3_request_id.rs +++ b/aws/rust-runtime/aws-inlineable/src/s3_request_id.rs @@ -4,7 +4,6 @@ */ use aws_smithy_http::http::HttpHeaders; -use aws_smithy_http::operation; use aws_smithy_http::result::SdkError; use aws_smithy_types::error::metadata::{ Builder as ErrorMetadataBuilder, ErrorMetadata, ProvideErrorMetadata, @@ -47,12 +46,6 @@ impl RequestIdExt for Unhandled { } } -impl RequestIdExt for operation::Response { - fn extended_request_id(&self) -> Option<&str> { - extract_extended_request_id(self.http().headers()) - } -} - impl RequestIdExt for http::Response { fn extended_request_id(&self) -> Option<&str> { extract_extended_request_id(self.headers()) @@ -115,15 +108,12 @@ mod test { #[test] fn test_extended_request_id_sdk_error() { - let without_extended_request_id = - || operation::Response::new(Response::builder().body(SdkBody::empty()).unwrap()); + let without_extended_request_id = || Response::builder().body(SdkBody::empty()).unwrap(); let with_extended_request_id = || { - operation::Response::new( - Response::builder() - .header("x-amz-id-2", HeaderValue::from_static("some-request-id")) - .body(SdkBody::empty()) - .unwrap(), - ) + Response::builder() + .header("x-amz-id-2", HeaderValue::from_static("some-request-id")) + .body(SdkBody::empty()) + .unwrap() }; assert_eq!( None, diff --git a/aws/rust-runtime/aws-sig-auth/Cargo.toml b/aws/rust-runtime/aws-sig-auth/Cargo.toml index db5f4e3965..6d796a290b 100644 --- a/aws/rust-runtime/aws-sig-auth/Cargo.toml +++ b/aws/rust-runtime/aws-sig-auth/Cargo.toml @@ -2,33 +2,11 @@ name = "aws-sig-auth" version = "0.0.0-smithy-rs-head" authors = ["AWS Rust SDK Team ", "Russell Cohen "] -description = "SigV4 signing middleware for the AWS SDK." +description = "This crate is no longer used by the AWS SDK and is deprecated." edition = "2021" license = "Apache-2.0" repository = "https://github.com/awslabs/smithy-rs" -[features] -sign-eventstream = ["aws-smithy-eventstream", "aws-sigv4/sign-eventstream"] - -[dependencies] -aws-credential-types = { path = "../aws-credential-types" } -# TODO(httpRefactor): Remove feature was http refactor is complete -aws-sigv4 = { path = "../aws-sigv4", features = ["http0-compat"] } -aws-smithy-eventstream = { path = "../../../rust-runtime/aws-smithy-eventstream", optional = true } -aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" } -aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async" } -aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api" } -aws-types = { path = "../aws-types" } -http = "0.2.2" -tracing = "0.1" - -[dev-dependencies] -aws-credential-types = { path = "../aws-credential-types", features = ["test-util"] } -aws-endpoint = { path = "../aws-endpoint" } -aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types" } -tracing-test = "0.2.4" -aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["test-util"] } - [package.metadata.docs.rs] all-features = true targets = ["x86_64-unknown-linux-gnu"] diff --git a/aws/rust-runtime/aws-sig-auth/README.md b/aws/rust-runtime/aws-sig-auth/README.md index 1a579b0fe6..8a76b6c7e6 100644 --- a/aws/rust-runtime/aws-sig-auth/README.md +++ b/aws/rust-runtime/aws-sig-auth/README.md @@ -1,7 +1,6 @@ # aws-sig-auth -This crate implements a standalone request signer for AWS services. For examples, -see [docs.rs](https://docs.rs/aws-sig-auth). +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/awslabs/smithy-rs) code generator. In most cases, it should not be used directly. diff --git a/aws/rust-runtime/aws-sig-auth/src/event_stream.rs b/aws/rust-runtime/aws-sig-auth/src/event_stream.rs deleted file mode 100644 index 565377f9f4..0000000000 --- a/aws/rust-runtime/aws-sig-auth/src/event_stream.rs +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -// this code is dead -#![allow(deprecated)] -#![allow(clippy::disallowed_methods)] - -use crate::middleware::Signature; -use aws_sigv4::event_stream::{sign_empty_message, sign_message}; -use aws_sigv4::sign::v4; -use aws_smithy_eventstream::frame::{Message, SignMessage, SignMessageError}; -use aws_smithy_http::property_bag::{PropertyBag, SharedPropertyBag}; -use aws_smithy_runtime_api::client::identity::Identity; -use aws_types::region::SigningRegion; -use aws_types::SigningName; -use std::time::SystemTime; - -/// Event Stream SigV4 signing implementation. -#[derive(Debug)] -pub struct SigV4MessageSigner { - last_signature: String, - identity: Identity, - signing_region: SigningRegion, - signing_name: SigningName, - time: Option, -} - -impl SigV4MessageSigner { - pub fn new( - last_signature: String, - identity: Identity, - signing_region: SigningRegion, - signing_name: SigningName, - time: Option, - ) -> Self { - Self { - last_signature, - identity, - signing_region, - signing_name, - time, - } - } - - fn signing_params(&self) -> v4::SigningParams<()> { - let builder = v4::SigningParams::builder() - .identity(&self.identity) - .region(self.signing_region.as_ref()) - .name(self.signing_name.as_ref()) - .time(self.time.unwrap_or_else(SystemTime::now)) - .settings(()); - builder.build().unwrap() - } -} - -impl SignMessage for SigV4MessageSigner { - fn sign(&mut self, message: Message) -> Result { - let (signed_message, signature) = { - let params = self.signing_params(); - sign_message(&message, &self.last_signature, ¶ms)?.into_parts() - }; - self.last_signature = signature; - Ok(signed_message) - } - - fn sign_empty(&mut self) -> Option> { - let (signed_message, signature) = { - let params = self.signing_params(); - sign_empty_message(&self.last_signature, ¶ms) - .expect("signing an empty message will always succeed.") - .into_parts() - }; - self.last_signature = signature; - Some(Ok(signed_message)) - } -} - -#[cfg(test)] -mod tests { - use crate::event_stream::SigV4MessageSigner; - use aws_credential_types::Credentials; - use aws_smithy_eventstream::frame::{HeaderValue, Message, SignMessage}; - - use aws_types::region::Region; - use aws_types::region::SigningRegion; - use aws_types::SigningName; - use std::time::{Duration, UNIX_EPOCH}; - - fn check_send_sync(value: T) -> T { - value - } - - #[test] - fn sign_message() { - let region = Region::new("us-east-1"); - let mut signer = check_send_sync(SigV4MessageSigner::new( - "initial-signature".into(), - Credentials::for_tests_with_session_token().into(), - SigningRegion::from(region), - SigningName::from_static("transcribe"), - Some(UNIX_EPOCH + Duration::new(1611160427, 0)), - )); - let mut signatures = Vec::new(); - for _ in 0..5 { - let signed = signer - .sign(Message::new(&b"identical message"[..])) - .unwrap(); - if let HeaderValue::ByteArray(signature) = signed - .headers() - .iter() - .find(|h| h.name().as_str() == ":chunk-signature") - .unwrap() - .value() - { - signatures.push(signature.clone()); - } else { - panic!("failed to get the :chunk-signature") - } - } - for i in 1..signatures.len() { - assert_ne!(signatures[i - 1], signatures[i]); - } - } -} - -// TODO(enableNewSmithyRuntimeCleanup): Delete this old implementation that was kept around to support patch releases. -#[deprecated = "use aws_sig_auth::event_stream::SigV4MessageSigner instead (this may require upgrading the smithy-rs code generator)"] -#[derive(Debug)] -/// Event Stream SigV4 signing implementation. -pub struct SigV4Signer { - properties: SharedPropertyBag, - last_signature: Option, -} - -impl SigV4Signer { - pub fn new(properties: SharedPropertyBag) -> Self { - Self { - properties, - last_signature: None, - } - } - - fn signing_params(properties: &PropertyBag) -> v4::SigningParams<()> { - // Every single one of these values would have been retrieved during the initial request, - // so we can safely assume they all exist in the property bag at this point. - let identity = properties.get::().unwrap(); - let region = properties.get::().unwrap(); - let name = properties.get::().unwrap(); - let time = properties - .get::() - .copied() - .unwrap_or_else(SystemTime::now); - let builder = v4::SigningParams::builder() - .identity(identity) - .region(region.as_ref()) - .name(name.as_ref()) - .time(time) - .settings(()); - builder.build().unwrap() - } -} - -impl SignMessage for SigV4Signer { - fn sign(&mut self, message: Message) -> Result { - let properties = self.properties.acquire(); - if self.last_signature.is_none() { - // The Signature property should exist in the property bag for all Event Stream requests. - self.last_signature = Some( - properties - .get::() - .expect("property bag contains initial Signature") - .as_ref() - .into(), - ) - } - - let (signed_message, signature) = { - let params = Self::signing_params(&properties); - sign_message(&message, self.last_signature.as_ref().unwrap(), ¶ms)?.into_parts() - }; - self.last_signature = Some(signature); - Ok(signed_message) - } - - fn sign_empty(&mut self) -> Option> { - let properties = self.properties.acquire(); - if self.last_signature.is_none() { - // The Signature property should exist in the property bag for all Event Stream requests. - self.last_signature = Some(properties.get::().unwrap().as_ref().into()) - } - let (signed_message, signature) = { - let params = Self::signing_params(&properties); - sign_empty_message(self.last_signature.as_ref().unwrap(), ¶ms) - .ok()? - .into_parts() - }; - self.last_signature = Some(signature); - Some(Ok(signed_message)) - } -} - -// TODO(enableNewSmithyRuntimeCleanup): Delete this old implementation that was kept around to support patch releases. -#[cfg(test)] -mod old_tests { - use crate::event_stream::SigV4Signer; - use crate::middleware::Signature; - use aws_credential_types::Credentials; - use aws_smithy_eventstream::frame::{HeaderValue, Message, SignMessage}; - use aws_smithy_http::property_bag::PropertyBag; - use aws_smithy_runtime_api::client::identity::Identity; - use aws_types::region::Region; - use aws_types::region::SigningRegion; - use aws_types::SigningName; - use std::time::{Duration, UNIX_EPOCH}; - - #[test] - fn sign_message() { - let region = Region::new("us-east-1"); - let mut properties = PropertyBag::new(); - properties.insert(region.clone()); - properties.insert(UNIX_EPOCH + Duration::new(1611160427, 0)); - properties.insert::(Credentials::for_tests_with_session_token().into()); - properties.insert(SigningName::from_static("transcribe")); - properties.insert(SigningRegion::from(region)); - properties.insert(Signature::new("initial-signature".into())); - - let mut signer = SigV4Signer::new(properties.into()); - let mut signatures = Vec::new(); - for _ in 0..5 { - let signed = signer - .sign(Message::new(&b"identical message"[..])) - .unwrap(); - if let HeaderValue::ByteArray(signature) = signed - .headers() - .iter() - .find(|h| h.name().as_str() == ":chunk-signature") - .unwrap() - .value() - { - signatures.push(signature.clone()); - } else { - panic!("failed to get the :chunk-signature") - } - } - for i in 1..signatures.len() { - assert_ne!(signatures[i - 1], signatures[i]); - } - } -} diff --git a/aws/rust-runtime/aws-sig-auth/src/lib.rs b/aws/rust-runtime/aws-sig-auth/src/lib.rs index 163a881a99..2819d85118 100644 --- a/aws/rust-runtime/aws-sig-auth/src/lib.rs +++ b/aws/rust-runtime/aws-sig-auth/src/lib.rs @@ -3,119 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -// TODO(enableNewSmithyRuntimeCleanup): Deprecate this crate and replace it with empty contents. Remove references to it in the code generator. - -#![allow(clippy::derive_partial_eq_without_eq)] - -//! AWS Signature Authentication Package -//! -//! This crate may be used to generate presigned URLs for unmodeled behavior such as `rds-iam-token` -//! or to sign requests to APIGateway-based services with IAM authorization. -//! -//! # Examples -//! -//! ## Generate RDS IAM Token -//! ```rust -//! use aws_credential_types::Credentials; -//! use aws_smithy_http::body::SdkBody; -//! use aws_types::SigningName; -//! use aws_types::region::{Region, SigningRegion}; -//! use std::time::{Duration, SystemTime, UNIX_EPOCH}; -//! use aws_sig_auth::signer::{self, SigningError, OperationSigningConfig, HttpSignatureType, RequestConfig}; -//! use aws_smithy_runtime_api::client::identity::Identity; -//! -//! fn generate_rds_iam_token( -//! db_hostname: &str, -//! region: Region, -//! port: u16, -//! db_username: &str, -//! identity: &Identity, -//! timestamp: SystemTime, -//! ) -> Result { -//! let signer = signer::SigV4Signer::new(); -//! let mut operation_config = OperationSigningConfig::default_config(); -//! operation_config.signature_type = HttpSignatureType::HttpRequestQueryParams; -//! operation_config.expires_in = Some(Duration::from_secs(15 * 60)); -//! let request_config = RequestConfig { -//! request_ts: timestamp, -//! region: &SigningRegion::from(region), -//! name: &SigningName::from_static("rds-db"), -//! payload_override: None, -//! }; -//! let mut request = http::Request::builder() -//! .uri(format!( -//! "http://{db_hostname}:{port}/?Action=connect&DBUser={db_user}", -//! db_hostname = db_hostname, -//! port = port, -//! db_user = db_username -//! )) -//! .body(SdkBody::empty()) -//! .expect("valid request"); -//! let _signature = signer.sign( -//! &operation_config, -//! &request_config, -//! identity, -//! &mut request, -//! )?; -//! let mut uri = request.uri().to_string(); -//! assert!(uri.starts_with("http://")); -//! let uri = uri.split_off("http://".len()); -//! Ok(uri) -//! } -//! -//! // You will need to get an `identity` from a credentials provider ahead of time -//! # let identity = Credentials::new("AKIDEXAMPLE", "secret", None, None, "example").into(); -//! let token = generate_rds_iam_token( -//! "prod-instance.us-east-1.rds.amazonaws.com", -//! Region::from_static("us-east-1"), -//! 3306, -//! "dbuser", -//! &identity, -//! // this value is hard coded to create deterministic signature for tests. Generally, -//! // `SystemTime::now()` should be used -//! UNIX_EPOCH + Duration::from_secs(1635257380) -//! ).expect("failed to generate token"); -//! # // validate against token generated by the aws CLI -//! # assert_eq!(token, "prod-instance.us-east-1.rds.amazonaws.com:3306/?Action=connect&DBUser=dbuser&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIDEXAMPLE%2F20211026%2Fus-east-1%2Frds-db%2Faws4_request&X-Amz-Date=20211026T140940Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=9632f5f4fcd2087a3c523f55f72d2fe97fad03b71a0a23b8c1edfb104e8072d1"); -//! ``` -//! -//! ## Sign a request for APIGateway execute-api -//! -//! ```no_run -//! use aws_credential_types::provider::ProvideCredentials; -//! use aws_sig_auth::signer::{OperationSigningConfig, RequestConfig, SigV4Signer}; -//! use aws_smithy_http::body::SdkBody; -//! use aws_types::region::{Region, SigningRegion}; -//! use aws_types::SigningName; -//! use std::error::Error; -//! use std::time::SystemTime; -//! use aws_smithy_runtime_api::client::identity::Identity; -//! async fn sign_request( -//! mut request: &mut http::Request, -//! region: Region, -//! credentials_provider: &impl ProvideCredentials, -//! ) -> Result<(), Box> { -//! let now = SystemTime::now(); -//! let signer = SigV4Signer::new(); -//! let request_config = RequestConfig { -//! request_ts: now, -//! region: &SigningRegion::from(region), -//! name: &SigningName::from_static("execute-api"), -//! payload_override: None, -//! }; -//! let identity = credentials_provider.provide_credentials().await?.into(); -//! signer.sign( -//! &OperationSigningConfig::default_config(), -//! &request_config, -//! &identity, -//! &mut request, -//! )?; -//! Ok((())) -//! } -//! ``` - -#[cfg(feature = "sign-eventstream")] -pub mod event_stream; - -pub mod middleware; -pub mod signer; +//! This crate is no longer used by the AWS SDK and is deprecated. diff --git a/aws/rust-runtime/aws-sig-auth/src/middleware.rs b/aws/rust-runtime/aws-sig-auth/src/middleware.rs deleted file mode 100644 index 3d4e788e37..0000000000 --- a/aws/rust-runtime/aws-sig-auth/src/middleware.rs +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use std::error::Error; -use std::fmt::{Display, Formatter}; - -use aws_smithy_http::middleware::MapRequest; -use aws_smithy_http::operation::Request; -use aws_smithy_http::property_bag::PropertyBag; - -use aws_credential_types::Credentials; -use aws_sigv4::http_request::SignableBody; -use aws_smithy_async::time::SharedTimeSource; -use aws_types::region::SigningRegion; -use aws_types::SigningName; - -use crate::signer::{ - OperationSigningConfig, RequestConfig, SigV4Signer, SigningError, SigningRequirements, -}; - -#[cfg(feature = "sign-eventstream")] -use crate::event_stream::SigV4MessageSigner as EventStreamSigV4Signer; -#[cfg(feature = "sign-eventstream")] -use aws_smithy_eventstream::frame::DeferredSignerSender; - -// TODO(enableNewSmithyRuntimeCleanup): Delete `Signature` when switching to the orchestrator -/// Container for the request signature for use in the property bag. -#[non_exhaustive] -#[derive(Debug, Clone)] -pub struct Signature(String); - -impl Signature { - pub fn new(signature: String) -> Self { - Self(signature) - } -} - -impl AsRef for Signature { - fn as_ref(&self) -> &str { - &self.0 - } -} - -/// Middleware stage to sign requests with SigV4 -/// -/// SigV4RequestSignerStage will load configuration from the request property bag and add -/// a signature. -/// -/// Prior to signing, the following fields MUST be present in the property bag: -/// - [`SigningRegion`]: The region used when signing the request, e.g. `us-east-1` -/// - [`SigningName`]: The name of the service to use when signing the request, e.g. `dynamodb` -/// - [`Credentials`]: Credentials to sign with -/// - [`OperationSigningConfig`]: Operation specific signing configuration, e.g. -/// changes to URL encoding behavior, or headers that must be omitted. -/// - [`SharedTimeSource`]: The time source to use when signing the request. -/// If any of these fields are missing, the middleware will return an error. -#[derive(Clone, Debug)] -pub struct SigV4SigningStage { - signer: SigV4Signer, -} - -impl SigV4SigningStage { - pub fn new(signer: SigV4Signer) -> Self { - Self { signer } - } -} - -#[derive(Debug)] -enum SigningStageErrorKind { - MissingCredentials, - MissingSigningRegion, - MissingSigningName, - MissingSigningConfig, - SigningFailure(SigningError), -} - -#[derive(Debug)] -pub struct SigningStageError { - kind: SigningStageErrorKind, -} - -impl Display for SigningStageError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - use SigningStageErrorKind::*; - match self.kind { - MissingCredentials => { - write!(f, "no credentials in the property bag") - } - MissingSigningRegion => { - write!(f, "no signing region in the property bag") - } - MissingSigningName => { - write!(f, "no signing service in the property bag") - } - MissingSigningConfig => { - write!(f, "no signing configuration in the property bag") - } - SigningFailure(_) => write!(f, "signing failed"), - } - } -} - -impl Error for SigningStageError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - use SigningStageErrorKind as ErrorKind; - match &self.kind { - ErrorKind::SigningFailure(err) => Some(err), - ErrorKind::MissingCredentials - | ErrorKind::MissingSigningRegion - | ErrorKind::MissingSigningName - | ErrorKind::MissingSigningConfig => None, - } - } -} - -impl From for SigningStageError { - fn from(kind: SigningStageErrorKind) -> Self { - Self { kind } - } -} - -impl From for SigningStageError { - fn from(error: SigningError) -> Self { - Self { - kind: SigningStageErrorKind::SigningFailure(error), - } - } -} - -/// Extract a signing config from a [`PropertyBag`](aws_smithy_http::property_bag::PropertyBag) -fn signing_config( - config: &PropertyBag, -) -> Result<(&OperationSigningConfig, RequestConfig, Credentials), SigningStageError> { - let operation_config = config - .get::() - .ok_or(SigningStageErrorKind::MissingSigningConfig)?; - let credentials = config - .get::() - .ok_or(SigningStageErrorKind::MissingCredentials)? - .clone(); - let region = config - .get::() - .ok_or(SigningStageErrorKind::MissingSigningRegion)?; - let name = config - .get::() - .ok_or(SigningStageErrorKind::MissingSigningName)?; - let payload_override = config.get::>(); - let request_config = RequestConfig { - request_ts: config - .get::() - .map(|t| t.now()) - .unwrap_or_else(|| SharedTimeSource::default().now()), - region, - payload_override, - name, - }; - Ok((operation_config, request_config, credentials)) -} - -impl MapRequest for SigV4SigningStage { - type Error = SigningStageError; - - fn name(&self) -> &'static str { - "sigv4_sign_request" - } - - fn apply(&self, req: Request) -> Result { - req.augment(|mut req, config| { - let operation_config = config - .get::() - .ok_or(SigningStageErrorKind::MissingSigningConfig)?; - let (operation_config, request_config, creds) = - match &operation_config.signing_requirements { - SigningRequirements::Disabled => return Ok(req), - SigningRequirements::Optional => match signing_config(config) { - Ok(parts) => parts, - Err(_) => return Ok(req), - }, - SigningRequirements::Required => signing_config(config)?, - }; - let identity = creds.into(); - - let signature = self - .signer - .sign(operation_config, &request_config, &identity, &mut req) - .map_err(SigningStageErrorKind::SigningFailure)?; - - // If this is an event stream operation, set up the event stream signer - #[cfg(feature = "sign-eventstream")] - if let Some(signer_sender) = config.get::() { - let time_override = config.get::().map(|ts| ts.now()); - signer_sender - .send(Box::new(EventStreamSigV4Signer::new( - signature.as_ref().into(), - identity, - request_config.region.clone(), - request_config.name.clone(), - time_override, - )) as _) - .expect("failed to send deferred signer"); - } - - config.insert(signature); - Ok(req) - }) - } -} - -#[cfg(test)] -mod test { - use std::convert::Infallible; - use std::time::{Duration, UNIX_EPOCH}; - - use aws_smithy_http::body::SdkBody; - use aws_smithy_http::middleware::MapRequest; - use aws_smithy_http::operation; - use http::header::AUTHORIZATION; - - use aws_credential_types::Credentials; - use aws_endpoint::AwsAuthStage; - use aws_smithy_async::time::SharedTimeSource; - - use aws_types::region::{Region, SigningRegion}; - use aws_types::SigningName; - - use crate::middleware::{ - SigV4SigningStage, Signature, SigningStageError, SigningStageErrorKind, - }; - use crate::signer::{OperationSigningConfig, SigV4Signer}; - - #[test] - fn places_signature_in_property_bag() { - let req = http::Request::builder() - .uri("https://test-service.test-region.amazonaws.com/") - .body(SdkBody::from("")) - .unwrap(); - let region = Region::new("us-east-1"); - let req = operation::Request::new(req) - .augment(|req, properties| { - properties.insert(region.clone()); - properties.insert(UNIX_EPOCH + Duration::new(1611160427, 0)); - properties.insert(SigningName::from_static("kinesis")); - properties.insert(OperationSigningConfig::default_config()); - properties.insert(Credentials::for_tests_with_session_token()); - properties.insert(SigningRegion::from(region)); - Result::<_, Infallible>::Ok(req) - }) - .expect("succeeds"); - - let signer = SigV4SigningStage::new(SigV4Signer::new()); - let req = signer.apply(req).unwrap(); - - let property_bag = req.properties(); - let signature = property_bag.get::(); - assert!(signature.is_some()); - } - - #[cfg(feature = "sign-eventstream")] - #[test] - fn sends_event_stream_signer_for_event_stream_operations() { - use crate::event_stream::SigV4MessageSigner as EventStreamSigV4Signer; - use aws_smithy_eventstream::frame::{DeferredSigner, SignMessage}; - - let (mut deferred_signer, deferred_signer_sender) = DeferredSigner::new(); - let req = http::Request::builder() - .uri("https://test-service.test-region.amazonaws.com/") - .body(SdkBody::from("")) - .unwrap(); - let region = Region::new("us-east-1"); - let req = operation::Request::new(req) - .augment(|req, properties| { - properties.insert(region.clone()); - properties.insert::(SharedTimeSource::new( - UNIX_EPOCH + Duration::new(1611160427, 0), - )); - properties.insert(SigningName::from_static("kinesis")); - properties.insert(OperationSigningConfig::default_config()); - properties.insert(Credentials::for_tests_with_session_token()); - properties.insert(SigningRegion::from(region.clone())); - properties.insert(deferred_signer_sender); - Result::<_, Infallible>::Ok(req) - }) - .expect("succeeds"); - - let signer = SigV4SigningStage::new(SigV4Signer::new()); - let _ = signer.apply(req).unwrap(); - - let mut signer_for_comparison = EventStreamSigV4Signer::new( - // This is the expected SigV4 signature for the HTTP request above - "abac477b4afabf5651079e7b9a0aa6a1a3e356a7418a81d974cdae9d4c8e5441".into(), - Credentials::for_tests_with_session_token().into(), - SigningRegion::from(region), - SigningName::from_static("kinesis"), - Some(UNIX_EPOCH + Duration::new(1611160427, 0)), - ); - - let expected_signed_empty = signer_for_comparison.sign_empty().unwrap().unwrap(); - let actual_signed_empty = deferred_signer.sign_empty().unwrap().unwrap(); - assert_eq!(expected_signed_empty, actual_signed_empty); - } - - // check that the endpoint middleware followed by signing middleware produce the expected result - #[test] - fn endpoint_plus_signer() { - use aws_smithy_types::endpoint::Endpoint; - let endpoint = Endpoint::builder() - .url("https://kinesis.us-east-1.amazonaws.com") - .build(); - let req = http::Request::builder() - .uri("https://kinesis.us-east-1.amazonaws.com") - .body(SdkBody::from("")) - .unwrap(); - let region = SigningRegion::from_static("us-east-1"); - let req = operation::Request::new(req) - .augment(|req, conf| { - conf.insert(region.clone()); - conf.insert(SharedTimeSource::new( - UNIX_EPOCH + Duration::new(1611160427, 0), - )); - conf.insert(SigningName::from_static("kinesis")); - conf.insert(endpoint); - Result::<_, Infallible>::Ok(req) - }) - .expect("succeeds"); - - let endpoint = AwsAuthStage; - let signer = SigV4SigningStage::new(SigV4Signer::new()); - let mut req = endpoint.apply(req).expect("add endpoint should succeed"); - let mut errs = vec![signer - .apply(req.try_clone().expect("can clone")) - .expect_err("no signing config")]; - let mut config = OperationSigningConfig::default_config(); - config.signing_options.content_sha256_header = true; - req.properties_mut().insert(config); - errs.push( - signer - .apply(req.try_clone().expect("can clone")) - .expect_err("no cred provider"), - ); - req.properties_mut() - .insert(Credentials::for_tests_with_session_token()); - let req = signer.apply(req).expect("signing succeeded"); - // make sure we got the correct error types in any order - assert!(errs.iter().all(|el| matches!( - el, - SigningStageError { - kind: SigningStageErrorKind::MissingCredentials - | SigningStageErrorKind::MissingSigningConfig - } - ))); - - let (req, _) = req.into_parts(); - assert_eq!( - req.headers() - .get("x-amz-date") - .expect("x-amz-date must be present"), - "20210120T163347Z" - ); - let auth_header = req - .headers() - .get(AUTHORIZATION) - .expect("auth header must be present") - .to_str() - .unwrap(); - assert_eq!(auth_header, "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210120/us-east-1/kinesis/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=228edaefb06378ac8d050252ea18a219da66117dd72759f4d1d60f02ebc3db64"); - } -} diff --git a/aws/rust-runtime/aws-sig-auth/src/signer.rs b/aws/rust-runtime/aws-sig-auth/src/signer.rs deleted file mode 100644 index feda470087..0000000000 --- a/aws/rust-runtime/aws-sig-auth/src/signer.rs +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use crate::middleware::Signature; -pub use aws_sigv4::http_request::SignableBody; -use aws_sigv4::http_request::{ - sign, PayloadChecksumKind, PercentEncodingMode, SessionTokenMode, SignableRequest, - SignatureLocation, SigningParams, SigningSettings, UriPathNormalizationMode, -}; -use aws_sigv4::sign::v4; -use aws_smithy_http::body::SdkBody; -use aws_smithy_runtime_api::client::identity::Identity; -use aws_types::region::SigningRegion; -use aws_types::SigningName; -use std::fmt; -use std::time::{Duration, SystemTime}; - -pub type SigningError = aws_sigv4::http_request::SigningError; - -const EXPIRATION_WARNING: &str = "Presigned request will expire before the given \ - `expires_in` duration because the credentials used to sign it will expire first."; - -#[derive(Eq, PartialEq, Clone, Copy)] -pub enum SigningAlgorithm { - SigV4, -} - -#[derive(Eq, PartialEq, Clone, Copy)] -pub enum HttpSignatureType { - /// A signature for a full http request should be computed, with header updates applied to the signing result. - HttpRequestHeaders, - - /// A signature for a full http request should be computed, with query param updates applied to the signing result. - /// - /// This is typically used for presigned URLs. - HttpRequestQueryParams, -} - -/// Signing Configuration for an Operation -/// -/// Although these fields MAY be customized on a per request basis, they are generally static -/// for a given operation -#[derive(Clone, PartialEq, Eq)] -#[non_exhaustive] -pub struct OperationSigningConfig { - pub algorithm: SigningAlgorithm, - pub signature_type: HttpSignatureType, - pub signing_options: SigningOptions, - pub signing_requirements: SigningRequirements, - pub expires_in: Option, -} - -impl OperationSigningConfig { - /// Placeholder method to provide a the signing configuration used for most operation - /// - /// In the future, we will code-generate a default configuration for each service - pub fn default_config() -> Self { - OperationSigningConfig { - algorithm: SigningAlgorithm::SigV4, - signature_type: HttpSignatureType::HttpRequestHeaders, - signing_options: SigningOptions { - double_uri_encode: true, - content_sha256_header: false, - normalize_uri_path: true, - omit_session_token: false, - }, - signing_requirements: SigningRequirements::Required, - expires_in: None, - } - } -} - -#[derive(Clone, Copy, Eq, PartialEq)] -pub enum SigningRequirements { - /// A signature MAY be added if credentials are defined - Optional, - - /// A signature MUST be added. - /// - /// If no credentials are provided, this will return an error without dispatching the operation. - Required, - - /// A signature MUST NOT be added. - Disabled, -} - -#[derive(Clone, Eq, PartialEq)] -#[non_exhaustive] -pub struct SigningOptions { - pub double_uri_encode: bool, - pub content_sha256_header: bool, - pub normalize_uri_path: bool, - pub omit_session_token: bool, -} - -/// Signing Configuration for an individual Request -/// -/// These fields may vary on a per-request basis -#[derive(Clone, PartialEq, Eq)] -pub struct RequestConfig<'a> { - pub request_ts: SystemTime, - pub region: &'a SigningRegion, - pub name: &'a SigningName, - pub payload_override: Option<&'a SignableBody<'static>>, -} - -#[derive(Clone, Default)] -pub struct SigV4Signer { - // In the future, the SigV4Signer will use the CRT signer. This will require constructing - // and holding an instance of the signer, so prevent people from constructing a SigV4Signer without - // going through the constructor. - _private: (), -} - -impl fmt::Debug for SigV4Signer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut formatter = f.debug_struct("SigV4Signer"); - formatter.finish() - } -} - -impl SigV4Signer { - pub fn new() -> Self { - SigV4Signer { _private: () } - } - - fn settings(operation_config: &OperationSigningConfig) -> SigningSettings { - let mut settings = SigningSettings::default(); - settings.percent_encoding_mode = if operation_config.signing_options.double_uri_encode { - PercentEncodingMode::Double - } else { - PercentEncodingMode::Single - }; - settings.payload_checksum_kind = if operation_config.signing_options.content_sha256_header { - PayloadChecksumKind::XAmzSha256 - } else { - PayloadChecksumKind::NoHeader - }; - settings.uri_path_normalization_mode = - if operation_config.signing_options.normalize_uri_path { - UriPathNormalizationMode::Enabled - } else { - UriPathNormalizationMode::Disabled - }; - settings.session_token_mode = if operation_config.signing_options.omit_session_token { - SessionTokenMode::Exclude - } else { - SessionTokenMode::Include - }; - settings.signature_location = match operation_config.signature_type { - HttpSignatureType::HttpRequestHeaders => SignatureLocation::Headers, - HttpSignatureType::HttpRequestQueryParams => SignatureLocation::QueryParams, - }; - settings.expires_in = operation_config.expires_in; - settings - } - - fn signing_params<'a>( - settings: SigningSettings, - identity: &'a Identity, - request_config: &'a RequestConfig<'a>, - ) -> SigningParams<'a> { - if let Some(expires_in) = settings.expires_in { - if let Some(creds_expires_time) = identity.expiration().cloned() { - let presigned_expires_time = request_config.request_ts + expires_in; - if presigned_expires_time > creds_expires_time { - tracing::warn!(EXPIRATION_WARNING); - } - } - } - - let builder = v4::SigningParams::builder() - .identity(identity) - .region(request_config.region.as_ref()) - .name(request_config.name.as_ref()) - .time(request_config.request_ts) - .settings(settings); - builder.build().expect("all required fields set").into() - } - - /// Sign a request using the SigV4 Protocol - /// - /// Although this function may be used, end users will not typically - /// interact with this code. It is generally used via middleware in the request pipeline. See [`SigV4SigningStage`](crate::middleware::SigV4SigningStage). - pub fn sign( - &self, - operation_config: &OperationSigningConfig, - request_config: &RequestConfig<'_>, - identity: &Identity, - request: &mut http::Request, - ) -> Result { - let settings = Self::settings(operation_config); - let signing_params = Self::signing_params(settings, identity, request_config); - - let (signing_instructions, signature) = { - // A body that is already in memory can be signed directly. A body that is not in memory - // (any sort of streaming body or presigned request) will be signed via UNSIGNED-PAYLOAD. - let signable_body = request_config - .payload_override - // the payload_override is a cheap clone because it contains either a - // reference or a short checksum (we're not cloning the entire body) - .cloned() - .unwrap_or_else(|| { - request - .body() - .bytes() - .map(SignableBody::Bytes) - .unwrap_or(SignableBody::UnsignedPayload) - }); - - let signable_request = SignableRequest::new( - request.method().as_str(), - request.uri().to_string(), - request.headers().iter().map(|(k, v)| { - ( - k.as_str(), - std::str::from_utf8(v.as_bytes()) - .expect("only string headers are signable"), - ) - }), - signable_body, - )?; - sign(signable_request, &signing_params)? - } - .into_parts(); - - signing_instructions.apply_to_request(request); - - Ok(Signature::new(signature)) - } -} - -#[cfg(test)] -mod tests { - use super::{RequestConfig, SigV4Signer, EXPIRATION_WARNING}; - use aws_credential_types::Credentials; - use aws_sigv4::http_request::SigningSettings; - - use aws_types::region::SigningRegion; - use aws_types::SigningName; - use std::time::{Duration, SystemTime}; - use tracing_test::traced_test; - - #[test] - #[traced_test] - fn expiration_warning() { - let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1000); - let creds_expire_in = Duration::from_secs(100); - - let mut settings = SigningSettings::default(); - settings.expires_in = Some(creds_expire_in - Duration::from_secs(10)); - - let identity = Credentials::new( - "test-access-key", - "test-secret-key", - Some("test-session-token".into()), - Some(now + creds_expire_in), - "test", - ) - .into(); - let request_config = RequestConfig { - request_ts: now, - region: &SigningRegion::from_static("test"), - name: &SigningName::from_static("test"), - payload_override: None, - }; - SigV4Signer::signing_params(settings, &identity, &request_config); - assert!(!logs_contain(EXPIRATION_WARNING)); - - let mut settings = SigningSettings::default(); - settings.expires_in = Some(creds_expire_in + Duration::from_secs(10)); - - SigV4Signer::signing_params(settings, &identity, &request_config); - assert!(logs_contain(EXPIRATION_WARNING)); - } -} diff --git a/aws/rust-runtime/aws-sigv4/README.md b/aws/rust-runtime/aws-sigv4/README.md index 6e69eb5e90..5159a9f01a 100644 --- a/aws/rust-runtime/aws-sigv4/README.md +++ b/aws/rust-runtime/aws-sigv4/README.md @@ -1,8 +1,6 @@ # aws-sigv4 -Low-level SigV4 request signing implementations. Customers will not generally need to use this crate directly. If you -need to manually sign requests, [aws-sig-auth](https://crates.io/crates/aws-sig-auth) offers a higher level interface -for signing. +Low-level SigV4 request signing implementations. This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/awslabs/smithy-rs) code generator. In most cases, it should not be used directly. 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 c0127ae1e9..4141c7e0b1 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 @@ -15,12 +15,9 @@ fun RuntimeConfig.awsRuntimeCrate(name: String, features: Set = setOf()) object AwsCargoDependency { fun awsConfig(runtimeConfig: RuntimeConfig) = runtimeConfig.awsRuntimeCrate("aws-config") fun awsCredentialTypes(runtimeConfig: RuntimeConfig) = runtimeConfig.awsRuntimeCrate("aws-credential-types") - fun awsEndpoint(runtimeConfig: RuntimeConfig) = runtimeConfig.awsRuntimeCrate("aws-endpoint") 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") - fun awsSigAuth(runtimeConfig: RuntimeConfig) = runtimeConfig.awsRuntimeCrate("aws-sig-auth") - fun awsSigAuthEventStream(runtimeConfig: RuntimeConfig) = runtimeConfig.awsRuntimeCrate("aws-sig-auth", setOf("sign-eventstream")) fun awsSigv4(runtimeConfig: RuntimeConfig) = runtimeConfig.awsRuntimeCrate("aws-sigv4") fun awsTypes(runtimeConfig: RuntimeConfig) = runtimeConfig.awsRuntimeCrate("aws-types") } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt index fb4b5fa58e..3a1e6e6fbe 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsFluentClientDecorator.kt @@ -30,19 +30,12 @@ import software.amazon.smithy.rust.codegen.core.smithy.generators.LibRsSection import software.amazon.smithy.rust.codegen.core.util.serviceNameOrDefault private class Types(runtimeConfig: RuntimeConfig) { - private val smithyClient = RuntimeType.smithyClient(runtimeConfig) private val smithyHttp = RuntimeType.smithyHttp(runtimeConfig) private val smithyTypes = RuntimeType.smithyTypes(runtimeConfig) val awsTypes = AwsRuntimeType.awsTypes(runtimeConfig) val connectorError = smithyHttp.resolve("result::ConnectorError") - val connectorSettings = smithyClient.resolve("http_connector::ConnectorSettings") - val dynConnector = smithyClient.resolve("erase::DynConnector") - val dynMiddleware = smithyClient.resolve("erase::DynMiddleware") val retryConfig = smithyTypes.resolve("retry::RetryConfig") - val smithyClientBuilder = smithyClient.resolve("Builder") - val smithyClientRetry = smithyClient.resolve("retry") - val smithyConnector = smithyClient.resolve("bounds::SmithyConnector") val timeoutConfig = smithyTypes.resolve("timeout::TimeoutConfig") } @@ -57,7 +50,6 @@ class AwsFluentClientDecorator : ClientCodegenDecorator { val types = Types(runtimeConfig) FluentClientGenerator( codegenContext, - reexportSmithyClientBuilder = false, customizations = listOf( AwsPresignedFluentBuilderMethod(codegenContext), AwsFluentClientDocs(codegenContext), @@ -111,15 +103,9 @@ private class AwsFluentClientExtensions(private val codegenContext: ClientCodege private val codegenScope = arrayOf( "Arc" to RuntimeType.Arc, "ConnectorError" to types.connectorError, - "ConnectorSettings" to types.connectorSettings, - "DynConnector" to types.dynConnector, - "DynMiddleware" to types.dynMiddleware, "RetryConfig" to types.retryConfig, - "SmithyConnector" to types.smithyConnector, "TimeoutConfig" to types.timeoutConfig, - "SmithyClientBuilder" to types.smithyClientBuilder, "aws_types" to types.awsTypes, - "retry" to types.smithyClientRetry, ) fun render(writer: RustWriter) { 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 47570de7f4..d77cc0a29c 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 @@ -42,26 +42,11 @@ object AwsRuntimeType { ), ) - // TODO(enableNewSmithyRuntimeCleanup): Delete defaultMiddleware and middleware.rs, and remove tower dependency from inlinables, when cleaning up middleware - fun RuntimeConfig.defaultMiddleware() = RuntimeType.forInlineDependency( - InlineAwsDependency.forRustFile( - "middleware", visibility = Visibility.PUBLIC, - CargoDependency.smithyHttp(this), - CargoDependency.smithyHttpTower(this), - CargoDependency.smithyClient(this), - CargoDependency.Tower, - AwsCargoDependency.awsSigAuth(this), - AwsCargoDependency.awsHttp(this), - AwsCargoDependency.awsEndpoint(this), - ), - ).resolve("DefaultMiddleware") - fun awsCredentialTypes(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsCredentialTypes(runtimeConfig).toType() fun awsCredentialTypesTestUtil(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsCredentialTypes(runtimeConfig).toDevDependency().withFeature("test-util").toType() - fun awsEndpoint(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsEndpoint(runtimeConfig).toType() fun awsHttp(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsHttp(runtimeConfig).toType() fun awsSigv4(runtimeConfig: RuntimeConfig) = AwsCargoDependency.awsSigv4(runtimeConfig).toType() diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/OperationInputTestGenerator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/OperationInputTestGenerator.kt index 43210766e0..7408f5b76c 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/OperationInputTestGenerator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/endpoints/OperationInputTestGenerator.kt @@ -83,7 +83,7 @@ fun usesDeprecatedBuiltIns(testOperationInput: EndpointTestOperationInput): Bool * "AWS::S3::UseArnRegion": false * } */ * /* clientParams: {} */ - * let (http_client, rcvr) = aws_smithy_client::test_connection::capture_request(None); + * let (http_client, rcvr) = aws_smithy_runtime::client::http::test_util::capture_request(None); * let conf = { * #[allow(unused_mut)] * let mut builder = aws_sdk_s3::Config::builder() diff --git a/aws/sdk/integration-tests/Makefile b/aws/sdk/integration-tests/Makefile new file mode 100644 index 0000000000..6a4673485b --- /dev/null +++ b/aws/sdk/integration-tests/Makefile @@ -0,0 +1,6 @@ +all: test + +test: + ./test.sh + +.PHONY: all test diff --git a/aws/sdk/integration-tests/iam/Cargo.toml b/aws/sdk/integration-tests/iam/Cargo.toml index e1d358ea44..296e9167d1 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-endpoint = { path = "../../build/aws-sdk/sdk/aws-endpoint"} aws-http = { path = "../../build/aws-sdk/sdk/aws-http"} aws-sdk-iam = { path = "../../build/aws-sdk/sdk/iam" } aws-smithy-runtime = { path = "../../build/aws-sdk/sdk/aws-smithy-runtime", features = ["client", "test-util"] } diff --git a/aws/sdk/integration-tests/test.sh b/aws/sdk/integration-tests/test.sh new file mode 100755 index 0000000000..f22bf3f6e7 --- /dev/null +++ b/aws/sdk/integration-tests/test.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +# + +set -eu -o pipefail + +for f in *; do + if [[ -f "${f}/Cargo.toml" ]]; then + echo + echo "Testing ${f}..." + echo "###############" + cargo test --manifest-path "${f}/Cargo.toml" + fi +done diff --git a/aws/sdk/integration-tests/timestreamquery/tests/endpoint_disco.rs b/aws/sdk/integration-tests/timestreamquery/tests/endpoint_disco.rs index 4bff784ade..42d5461971 100644 --- a/aws/sdk/integration-tests/timestreamquery/tests/endpoint_disco.rs +++ b/aws/sdk/integration-tests/timestreamquery/tests/endpoint_disco.rs @@ -19,8 +19,9 @@ async fn do_endpoint_discovery() { let _logs = aws_smithy_runtime::test_util::capture_test_logs::capture_test_logs(); + // For recording, switch to: + // let http_client = aws_smithy_runtime::client::http::test_util::dvr::RecordingClient::new(client); let http_client = ReplayingClient::from_file("tests/traffic.json").unwrap(); - //let conn = aws_smithy_client::dvr::RecordingConnection::new(conn); let start = UNIX_EPOCH + Duration::from_secs(1234567890); let (ts, sleep, mut gate) = controlled_time_and_sleep(start); let config = SdkConfig::builder() diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt index 684def389a..375a87a2e6 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt @@ -26,7 +26,6 @@ import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig -import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.util.dq import software.amazon.smithy.rust.codegen.core.util.getTrait @@ -51,7 +50,6 @@ private fun codegenScope(runtimeConfig: RuntimeConfig): Array> "HTTP_DIGEST_AUTH_SCHEME_ID" to authHttpApi.resolve("HTTP_DIGEST_AUTH_SCHEME_ID"), "IdentityResolver" to smithyRuntimeApi.resolve("client::identity::IdentityResolver"), "Login" to smithyRuntimeApi.resolve("client::identity::http::Login"), - "PropertyBag" to RuntimeType.smithyHttp(runtimeConfig).resolve("property_bag::PropertyBag"), "SharedAuthScheme" to smithyRuntimeApi.resolve("client::auth::SharedAuthScheme"), "SharedIdentityResolver" to smithyRuntimeApi.resolve("client::identity::SharedIdentityResolver"), "Token" to smithyRuntimeApi.resolve("client::identity::http::Token"), diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpConnectorConfigDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpConnectorConfigDecorator.kt index 91f08b80be..0feb667fe0 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpConnectorConfigDecorator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpConnectorConfigDecorator.kt @@ -33,7 +33,6 @@ private class HttpConnectorConfigCustomization( private val codegenScope = arrayOf( *preludeScope, "Connection" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("client::orchestrator::Connection"), - "ConnectorSettings" to RuntimeType.smithyClient(runtimeConfig).resolve("http_connector::ConnectorSettings"), "HttpClient" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("client::http::HttpClient"), "IntoShared" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("shared::IntoShared"), "Resolver" to RuntimeType.smithyRuntime(runtimeConfig).resolve("client::config_override::Resolver"), diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt index 6f9892b2cc..2f47ffba7a 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/PaginatorGenerator.kt @@ -89,7 +89,6 @@ class PaginatorGenerator private constructor( // SDK Types "HttpResponse" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("client::orchestrator::HttpResponse"), "SdkError" to RuntimeType.sdkError(runtimeConfig), - "client" to RuntimeType.smithyClient(runtimeConfig), "pagination_stream" to RuntimeType.smithyAsync(runtimeConfig).resolve("future::pagination_stream"), // External Types diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientDecorator.kt index dfe7ca5802..73296eb337 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientDecorator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientDecorator.kt @@ -53,7 +53,7 @@ class FluentClientDecorator : ClientCodegenDecorator { return baseCustomizations + object : LibRsCustomization() { override fun section(section: LibRsSection) = when (section) { is LibRsSection.Body -> writable { - rust("pub use client::{Client, Builder};") + rust("pub use client::Client;") } else -> emptySection } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt index 4435f73271..4a9c162696 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/client/FluentClientGenerator.kt @@ -35,7 +35,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.normalizeHtml import software.amazon.smithy.rust.codegen.core.rustlang.qualifiedName import software.amazon.smithy.rust.codegen.core.rustlang.render import software.amazon.smithy.rust.codegen.core.rustlang.rust -import software.amazon.smithy.rust.codegen.core.rustlang.rustBlockTemplate +import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.stripOuter import software.amazon.smithy.rust.codegen.core.rustlang.withBlockTemplate @@ -57,7 +57,6 @@ import software.amazon.smithy.rust.codegen.core.util.toSnakeCase class FluentClientGenerator( private val codegenContext: ClientCodegenContext, - private val reexportSmithyClientBuilder: Boolean = true, private val customizations: List = emptyList(), ) { companion object { @@ -94,19 +93,9 @@ class FluentClientGenerator( private fun renderFluentClient(crate: RustCrate) { crate.withModule(ClientRustModule.client) { - if (reexportSmithyClientBuilder) { - rustTemplate( - """ - ##[doc(inline)] - pub use #{client}::Builder; - """, - "client" to RuntimeType.smithyClient(runtimeConfig), - ) - } val clientScope = arrayOf( *preludeScope, "Arc" to RuntimeType.Arc, - "client" to RuntimeType.smithyClient(runtimeConfig), "client_docs" to writable { customizations.forEach { @@ -181,10 +170,7 @@ class FluentClientGenerator( val privateModule = RustModule.private(moduleName, parent = ClientRustModule.client) crate.withModule(privateModule) { - rustBlockTemplate( - "impl super::Client", - "client" to RuntimeType.smithyClient(runtimeConfig), - ) { + rustBlock("impl super::Client") { val fullPath = operation.fullyQualifiedFluentBuilder(symbolProvider) val maybePaginated = if (operation.isPaginated(model)) { "\n/// This operation supports pagination; See [`into_paginator()`]($fullPath::into_paginator)." @@ -327,10 +313,7 @@ class FluentClientGenerator( "SdkError" to RuntimeType.sdkError(runtimeConfig), ) - rustBlockTemplate( - "impl $builderName", - "client" to RuntimeType.smithyClient(runtimeConfig), - ) { + rustBlock("impl $builderName") { rust("/// Creates a new `${operationSymbol.name}`.") withBlockTemplate( "pub(crate) fn new(handle: #{Arc}) -> Self {", diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt index 9dbb43f497..aa5d3f5169 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt @@ -283,14 +283,9 @@ data class CargoDependency( fun smithyAsync(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-async") fun smithyChecksums(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-checksums") - fun smithyClient(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-client") - fun smithyClientTestUtil(runtimeConfig: RuntimeConfig) = - smithyClient(runtimeConfig).toDevDependency().withFeature("test-util") fun smithyEventStream(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-eventstream") fun smithyHttp(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-http") - fun smithyHttpAuth(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-http-auth") - fun smithyHttpTower(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-http-tower") fun smithyJson(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-json") fun smithyProtocolTestHelpers(runtimeConfig: RuntimeConfig) = runtimeConfig.smithyRuntimeCrate("smithy-protocol-test", scope = DependencyScope.Dev) diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt index 639964f762..b356058437 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt @@ -317,14 +317,9 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null) // smithy runtime types fun smithyAsync(runtimeConfig: RuntimeConfig) = CargoDependency.smithyAsync(runtimeConfig).toType() fun smithyChecksums(runtimeConfig: RuntimeConfig) = CargoDependency.smithyChecksums(runtimeConfig).toType() - fun smithyClient(runtimeConfig: RuntimeConfig) = CargoDependency.smithyClient(runtimeConfig).toType() - fun smithyClientTestUtil(runtimeConfig: RuntimeConfig) = CargoDependency.smithyClient(runtimeConfig) - .copy(features = setOf("test-util"), scope = DependencyScope.Dev).toType() fun smithyEventStream(runtimeConfig: RuntimeConfig) = CargoDependency.smithyEventStream(runtimeConfig).toType() fun smithyHttp(runtimeConfig: RuntimeConfig) = CargoDependency.smithyHttp(runtimeConfig).toType() - fun smithyHttpAuth(runtimeConfig: RuntimeConfig) = CargoDependency.smithyHttpAuth(runtimeConfig).toType() - fun smithyHttpTower(runtimeConfig: RuntimeConfig) = CargoDependency.smithyHttpTower(runtimeConfig).toType() fun smithyJson(runtimeConfig: RuntimeConfig) = CargoDependency.smithyJson(runtimeConfig).toType() fun smithyQuery(runtimeConfig: RuntimeConfig) = CargoDependency.smithyQuery(runtimeConfig).toType() fun smithyRuntime(runtimeConfig: RuntimeConfig) = CargoDependency.smithyRuntime(runtimeConfig).toType() @@ -400,7 +395,6 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null) fun blob(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("Blob") fun byteStream(runtimeConfig: RuntimeConfig) = smithyHttp(runtimeConfig).resolve("byte_stream::ByteStream") - fun classifyRetry(runtimeConfig: RuntimeConfig) = smithyHttp(runtimeConfig).resolve("retry::ClassifyRetry") fun dateTime(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("DateTime") fun document(runtimeConfig: RuntimeConfig): RuntimeType = smithyTypes(runtimeConfig).resolve("Document") fun format(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("date_time::Format") @@ -427,11 +421,6 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null) fun labelFormat(runtimeConfig: RuntimeConfig, func: String) = smithyHttp(runtimeConfig).resolve("label::$func") fun operation(runtimeConfig: RuntimeConfig) = smithyHttp(runtimeConfig).resolve("operation::Operation") fun operationModule(runtimeConfig: RuntimeConfig) = smithyHttp(runtimeConfig).resolve("operation") - fun parseHttpResponse(runtimeConfig: RuntimeConfig) = - smithyHttp(runtimeConfig).resolve("response::ParseHttpResponse") - - fun parseStrictResponse(runtimeConfig: RuntimeConfig) = - smithyHttp(runtimeConfig).resolve("response::ParseStrictResponse") fun protocolTest(runtimeConfig: RuntimeConfig, func: String): RuntimeType = smithyProtocolTest(runtimeConfig).resolve(func) diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/ServerRequiredCustomizations.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/ServerRequiredCustomizations.kt index 5fc86ee04f..a353eb636b 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/ServerRequiredCustomizations.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/customizations/ServerRequiredCustomizations.kt @@ -42,13 +42,10 @@ class ServerRequiredCustomizations : ServerCodegenDecorator { rustCrate.withModule(ServerRustModule.Types) { pubUseSmithyPrimitives(codegenContext, codegenContext.model)(this) - // TODO(enableNewSmithyRuntimeCleanup): Remove re-export of SdkError in server and add changelog entry rustTemplate( """ - pub type SdkError = #{SdkError}; pub use #{DisplayErrorContext}; """, - "SdkError" to RuntimeType.smithyHttp(rc).resolve("result::SdkError"), "Response" to RuntimeType.smithyHttp(rc).resolve("operation::Response"), "DisplayErrorContext" to RuntimeType.smithyTypes(rc).resolve("error::display::DisplayErrorContext"), ) diff --git a/examples/pokemon-service-common/tests/plugins_execution_order.rs b/examples/pokemon-service-common/tests/plugins_execution_order.rs index 35d3ba5fe2..9981540cb8 100644 --- a/examples/pokemon-service-common/tests/plugins_execution_order.rs +++ b/examples/pokemon-service-common/tests/plugins_execution_order.rs @@ -10,7 +10,6 @@ use std::{ task::{Context, Poll}, }; -use aws_smithy_http::body::SdkBody; use aws_smithy_http_server::plugin::{HttpMarker, HttpPlugins, IdentityPlugin, Plugin}; use tower::{Layer, Service}; @@ -18,17 +17,6 @@ use aws_smithy_runtime::client::http::test_util::capture_request; use pokemon_service_client::{Client, Config}; use pokemon_service_common::do_nothing; -trait OperationExt { - /// Convert an SDK operation into an `http::Request`. - fn into_http(self) -> http::Request; -} - -impl OperationExt for aws_smithy_http::operation::Operation { - fn into_http(self) -> http::Request { - self.into_request_response().0.into_parts().0 - } -} - #[tokio::test] async fn plugin_layers_are_executed_in_registration_order() { // Each plugin layer will push its name into this vector when it gets invoked. diff --git a/examples/pokemon-service-tls/tests/common/mod.rs b/examples/pokemon-service-tls/tests/common/mod.rs index a13c2e60ee..8954365a20 100644 --- a/examples/pokemon-service-tls/tests/common/mod.rs +++ b/examples/pokemon-service-tls/tests/common/mod.rs @@ -50,8 +50,8 @@ pub fn client_http2_only() -> Client { Client::from_conf(config) } -/// A `hyper` connector that uses the `native-tls` crate for TLS. To use this in a smithy client, -/// wrap it in a [aws_smithy_client::hyper_ext::Adapter]. +/// A `hyper` connector that uses the `native-tls` crate for TLS. To use this in a Smithy client, +/// wrap with a [`HyperClientBuilder`]. pub type NativeTlsConnector = hyper_tls::HttpsConnector; fn native_tls_connector() -> NativeTlsConnector { diff --git a/examples/pokemon-service/Cargo.toml b/examples/pokemon-service/Cargo.toml index d3bc81ea0b..1a2da1ce8f 100644 --- a/examples/pokemon-service/Cargo.toml +++ b/examples/pokemon-service/Cargo.toml @@ -31,7 +31,6 @@ hyper = { version = "0.14.26", features = ["server", "client"] } hyper-rustls = { version = "0.24", features = ["http2"] } # Local paths -aws-smithy-client = { path = "../../rust-runtime/aws-smithy-client/", features = ["rustls"] } aws-smithy-http = { path = "../../rust-runtime/aws-smithy-http/" } aws-smithy-types = { path = "../../rust-runtime/aws-smithy-types/" } pokemon-service-client = { path = "../pokemon-service-client/" } diff --git a/rust-runtime/aws-smithy-client/Cargo.toml b/rust-runtime/aws-smithy-client/Cargo.toml index 8e8c5a39c7..0eb190b56d 100644 --- a/rust-runtime/aws-smithy-client/Cargo.toml +++ b/rust-runtime/aws-smithy-client/Cargo.toml @@ -2,70 +2,13 @@ name = "aws-smithy-client" version = "0.0.0-smithy-rs-head" authors = ["AWS Rust SDK Team ", "Russell Cohen "] -description = "Client for smithy-rs." +description = "This crate is no longer used by smithy-rs and is deprecated." edition = "2021" license = "Apache-2.0" repository = "https://github.com/awslabs/smithy-rs" -[features] -rt-tokio = ["aws-smithy-async/rt-tokio"] -test-util = ["dep:aws-smithy-protocol-test", "dep:serde", "dep:serde_json", "serde?/derive"] -wiremock = ["test-util", "dep:hyper", "hyper?/server", "hyper?/h2", "rustls", "tokio/full"] -native-tls = [] -allow-compilation = [] # our tests use `cargo test --all-features` and native-tls breaks CI -rustls = ["dep:hyper-rustls", "dep:lazy_static", "dep:rustls", "client-hyper", "rt-tokio"] -client-hyper = ["dep:hyper", "hyper?/client", "hyper?/http2", "hyper?/http1", "hyper?/tcp", "dep:h2"] -hyper-webpki-doctest-only = ["dep:hyper-rustls", "hyper-rustls?/webpki-roots"] - -[dependencies] -aws-smithy-async = { path = "../aws-smithy-async" } -aws-smithy-http = { path = "../aws-smithy-http" } -aws-smithy-http-tower = { path = "../aws-smithy-http-tower" } -aws-smithy-protocol-test = { path = "../aws-smithy-protocol-test", optional = true } -aws-smithy-types = { path = "../aws-smithy-types" } -bytes = "1" -fastrand = "2.0.0" -http = "0.2.3" -http-body = "0.4.4" -hyper = { version = "0.14.26", default-features = false, optional = true } -h2 = { version = "0.3", default-features = false, optional = true } -# cargo does not support optional test dependencies, so to completely disable rustls -# we need to add the webpki-roots feature here. -# https://github.com/rust-lang/cargo/issues/1596 -hyper-rustls = { version = "0.24", optional = true, features = ["rustls-native-certs", "http2"] } -hyper-tls = { version = "0.5.0", optional = true } -# This forces a more recent version of `openssl-sys` to be brought in. -# Without this `cargo minimal-versions check` would fail in a container whose base image is `amazonlinux:2023` -# with the failure symptom: https://github.com/sfackler/rust-openssl/issues/1724 -openssl = { version = "0.10.52", optional = true } -rustls = { version = "0.21.1", optional = true } -lazy_static = { version = "1", optional = true } -pin-project-lite = "0.2.7" -serde = { version = "1", features = ["derive"], optional = true } -serde_json = { version = "1", optional = true } -tokio = { version = "1.13.1" } -tower = { version = "0.4.6", features = ["util", "retry"] } -tracing = "0.1" - -[dev-dependencies] -aws-smithy-async = { path = "../aws-smithy-async", features = ["rt-tokio"] } -hyper-tls = { version = "0.5.0" } -# Dependency on `openssl` above needs to be repeated here. -openssl = "0.10.52" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -tokio = { version = "1.23.1", features = ["full", "test-util"] } -tower-test = "0.4.0" -tracing-subscriber = "0.3.16" -tracing-test = "0.2.4" - - [package.metadata.docs.rs] all-features = true targets = ["x86_64-unknown-linux-gnu"] rustdoc-args = ["--cfg", "docsrs"] # End of docs.rs metadata - -[[test]] -name = "e2e_test" -required-features = ["test-util", "rt-tokio"] diff --git a/rust-runtime/aws-smithy-client/README.md b/rust-runtime/aws-smithy-client/README.md index b1deae09be..3c87cd70d4 100644 --- a/rust-runtime/aws-smithy-client/README.md +++ b/rust-runtime/aws-smithy-client/README.md @@ -1,12 +1,6 @@ # aws-smithy-client -`aws-smithy-client` defines a Tower-based client that implements functionality that exists across all service clients -generated by [smithy-rs](https://github.com/awslabs/smithy-rs) including: - -- Retries -- Connector, request attempt, and multi-request timeouts -- Configurable middleware -- HTTPS implementations +This crate is no longer used by smithy-rs and is deprecated. Its equivalent logic is now in aws-smithy-runtime-api and aws-smithy-runtime. This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/awslabs/smithy-rs) code generator. In most cases, it should not be used directly. diff --git a/rust-runtime/aws-smithy-client/additional-ci b/rust-runtime/aws-smithy-client/additional-ci deleted file mode 100755 index e2ada6a73e..0000000000 --- a/rust-runtime/aws-smithy-client/additional-ci +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -# -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -# - -# This script contains additional CI checks to run for this specific package - -set -e - -# TODO(msrvUpgrade): This can be enabled when upgrading to Rust 1.71 -# echo '### Checking compilation under WASM' -# cargo hack check --no-dev-deps --target wasm32-unknown-unknown - -echo "### Checking for duplicate dependency versions in the normal dependency graph with all features enabled" -cargo tree -d --edges normal --all-features - -echo "### Testing every combination of features (excluding --all-features)" -cargo hack test --feature-powerset --exclude-all-features --exclude-features native-tls diff --git a/rust-runtime/aws-smithy-client/external-types.toml b/rust-runtime/aws-smithy-client/external-types.toml index 74daaad215..7fa182b39d 100644 --- a/rust-runtime/aws-smithy-client/external-types.toml +++ b/rust-runtime/aws-smithy-client/external-types.toml @@ -1,42 +1 @@ -allowed_external_types = [ - "aws_smithy_async::*", - "aws_smithy_http::*", - "aws_smithy_http_tower::*", - "aws_smithy_types::*", - "aws_smithy_protocol_test::MediaType", - "http::header::name::HeaderName", - "http::request::Request", - "http::response::Response", - "http::uri::Uri", - "tower::retry::policy::Policy", - "tower_service::Service", - - # TODO(https://github.com/awslabs/smithy-rs/issues/1193): Move `rustls` feature into separate crates - "hyper::client::connect::http::HttpConnector", - "hyper_rustls::connector::HttpsConnector", - "hyper_tls::client::HttpsConnector", - - # TODO(https://github.com/awslabs/smithy-rs/issues/1193): Once tooling permits it, only allow the following types in the `client-hyper` feature - "hyper::client::client::Builder", - "hyper::client::connect::Connection", - "tokio::io::async_read::AsyncRead", - "tokio::io::async_write::AsyncWrite", - - - # TODO(https://github.com/awslabs/smithy-rs/issues/1193): Once tooling permits it, only allow the following types in the `test-util` feature - "bytes::bytes::Bytes", - "serde::ser::Serialize", - "serde::de::Deserialize", - "hyper::client::connect::dns::Name", - - # TODO(https://github.com/awslabs/smithy-rs/issues/1193): Decide if we want to continue exposing tower_layer - "tower_layer::Layer", - - # TODO(https://github.com/awslabs/smithy-rs/issues/1193): Feature gate middleware_fn and service_fn, or remove them if they're unused - "tower::util::map_request::MapRequestLayer", - "tower::util::service_fn::ServiceFn", - "tower_util::MapRequestLayer", - - # TODO(https://github.com/awslabs/smithy-rs/issues/1193): Don't expose on `tower::BoxError` - "tower::BoxError", -] +allowed_external_types = [] diff --git a/rust-runtime/aws-smithy-client/src/bounds.rs b/rust-runtime/aws-smithy-client/src/bounds.rs deleted file mode 100644 index e0abf9e4df..0000000000 --- a/rust-runtime/aws-smithy-client/src/bounds.rs +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! This module holds convenient short-hands for the otherwise fairly extensive trait bounds -//! required for `call` and friends. -//! -//! The short-hands will one day be true [trait aliases], but for now they are traits with blanket -//! implementations. Also, due to [compiler limitations], the bounds repeat a number of associated -//! types with bounds so that those bounds [do not need to be repeated] at the call site. It's a -//! bit of a mess to define, but _should_ be invisible to callers. -//! -//! [trait aliases]: https://rust-lang.github.io/rfcs/1733-trait-alias.html -//! [compiler limitations]: https://github.com/rust-lang/rust/issues/20671 -//! [do not need to be repeated]: https://github.com/rust-lang/rust/issues/20671#issuecomment-529752828 - -use crate::erase::DynConnector; -use crate::http_connector::HttpConnector; -use aws_smithy_http::body::SdkBody; -use aws_smithy_http::operation::{self, Operation}; -use aws_smithy_http::response::ParseHttpResponse; -use aws_smithy_http::result::{ConnectorError, SdkError, SdkSuccess}; -use aws_smithy_http::retry::ClassifyRetry; -use tower::{Layer, Service}; - -/// A service that has parsed a raw Smithy response. -pub type Parsed = - aws_smithy_http_tower::parse_response::ParseResponseService; - -/// A low-level Smithy connector that maps from [`http::Request`] to [`http::Response`]. -/// -/// This trait has a blanket implementation for all compatible types, and should never be -/// implemented. -pub trait SmithyConnector: - Service< - http::Request, - Response = http::Response, - Error = ::Error, - Future = ::Future, - > + Send - + Sync - + Clone - + 'static -{ - /// Forwarding type to `::Error` for bound inference. - /// - /// See module-level docs for details. - type Error: Into + Send + Sync + 'static; - - /// Forwarding type to `::Future` for bound inference. - /// - /// See module-level docs for details. - type Future: Send + 'static; -} - -impl SmithyConnector for T -where - T: Service, Response = http::Response> - + Send - + Sync - + Clone - + 'static, - T::Error: Into + Send + Sync + 'static, - T::Future: Send + 'static, -{ - type Error = T::Error; - type Future = T::Future; -} - -impl From for HttpConnector -where - E: Into + Send + Sync + 'static, - F: Send + 'static, - T: SmithyConnector>, -{ - fn from(smithy_connector: T) -> Self { - HttpConnector::Prebuilt(Some(DynConnector::new(smithy_connector))) - } -} - -/// A Smithy middleware service that adjusts [`aws_smithy_http::operation::Request`](operation::Request)s. -/// -/// This trait has a blanket implementation for all compatible types, and should never be -/// implemented. -pub trait SmithyMiddlewareService: - Service< - operation::Request, - Response = operation::Response, - Error = aws_smithy_http_tower::SendOperationError, - Future = ::Future, -> -{ - /// Forwarding type to `::Future` for bound inference. - /// - /// See module-level docs for details. - type Future: Send + 'static; -} - -impl SmithyMiddlewareService for T -where - T: Service< - operation::Request, - Response = operation::Response, - Error = aws_smithy_http_tower::SendOperationError, - >, - T::Future: Send + 'static, -{ - type Future = T::Future; -} - -/// A Smithy middleware layer (i.e., factory). -/// -/// This trait has a blanket implementation for all compatible types, and should never be -/// implemented. -pub trait SmithyMiddleware: - Layer< - aws_smithy_http_tower::dispatch::DispatchService, - Service = >::Service, -> -{ - /// Forwarding type to `::Service` for bound inference. - /// - /// See module-level docs for details. - type Service: SmithyMiddlewareService + Send + Sync + Clone + 'static; -} - -impl SmithyMiddleware for T -where - T: Layer>, - T::Service: SmithyMiddlewareService + Send + Sync + Clone + 'static, -{ - type Service = T::Service; -} - -/// A Smithy retry policy. -/// -/// This trait has a blanket implementation for all compatible types, and should never be -/// implemented. -pub trait SmithyRetryPolicy: - tower::retry::Policy, SdkSuccess, SdkError> + Clone -{ - /// Forwarding type to `O` for bound inference. - /// - /// See module-level docs for details. - type O: ParseHttpResponse> + Send + Sync + Clone + 'static; - /// Forwarding type to `E` for bound inference. - /// - /// See module-level docs for details. - type E: std::error::Error; - - /// Forwarding type to `Retry` for bound inference. - /// - /// See module-level docs for details. - type Retry: ClassifyRetry, SdkError>; -} - -impl SmithyRetryPolicy for R -where - R: tower::retry::Policy, SdkSuccess, SdkError> + Clone, - O: ParseHttpResponse> + Send + Sync + Clone + 'static, - E: std::error::Error, - Retry: ClassifyRetry, SdkError>, -{ - type O = O; - type E = E; - type Retry = Retry; -} diff --git a/rust-runtime/aws-smithy-client/src/builder.rs b/rust-runtime/aws-smithy-client/src/builder.rs deleted file mode 100644 index 5602c330f5..0000000000 --- a/rust-runtime/aws-smithy-client/src/builder.rs +++ /dev/null @@ -1,567 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use crate::{bounds, erase, retry, Client}; -use aws_smithy_async::rt::sleep::{default_async_sleep, SharedAsyncSleep}; -use aws_smithy_http::body::SdkBody; -use aws_smithy_http::result::ConnectorError; -use aws_smithy_types::retry::ReconnectMode; -use aws_smithy_types::timeout::{OperationTimeoutConfig, TimeoutConfig}; - -#[derive(Clone, Debug)] -struct MaybeRequiresSleep { - requires_sleep: bool, - implementation: I, -} - -impl MaybeRequiresSleep { - fn new(requires_sleep: bool, implementation: I) -> Self { - Self { - requires_sleep, - implementation, - } - } -} - -/// A builder that provides more customization options when constructing a [`Client`]. -/// -/// To start, call [`Builder::new`]. Then, chain the method calls to configure the `Builder`. -/// When configured to your liking, call [`Builder::build`]. The individual methods have additional -/// documentation. -#[derive(Clone, Debug)] -pub struct Builder { - connector: MaybeRequiresSleep, - middleware: M, - retry_policy: MaybeRequiresSleep, - operation_timeout_config: Option, - sleep_impl: Option, - reconnect_mode: Option, -} - -/// transitional default: disable this behavior by default -const fn default_reconnect_mode() -> ReconnectMode { - ReconnectMode::ReuseAllConnections -} - -impl Default for Builder -where - C: Default, - M: Default, -{ - fn default() -> Self { - let default_retry_config = retry::Config::default(); - Self { - connector: MaybeRequiresSleep::new(false, Default::default()), - middleware: Default::default(), - retry_policy: MaybeRequiresSleep::new( - default_retry_config.has_retry(), - retry::Standard::new(default_retry_config), - ), - operation_timeout_config: None, - sleep_impl: default_async_sleep(), - reconnect_mode: Some(default_reconnect_mode()), - } - } -} - -// It'd be nice to include R where R: Default here, but then the caller ends up always having to -// specify R explicitly since type parameter defaults (like the one for R) aren't picked up when R -// cannot be inferred. This is, arguably, a compiler bug/missing language feature, but is -// complicated: https://github.com/rust-lang/rust/issues/27336. -// -// For the time being, we stick with just for ::new. Those can usually be inferred since we -// only implement .constructor and .middleware when C and M are () respectively. Users who really -// need a builder for a custom R can use ::default instead. -impl Builder -where - C: Default, - M: Default, -{ - /// Construct a new builder. This does not specify a [connector](Builder::connector) - /// or [middleware](Builder::middleware). - /// It uses the [standard retry mechanism](retry::Standard). - pub fn new() -> Self { - Self::default() - } -} - -#[cfg(feature = "rustls")] -use crate::erase::DynConnector; -#[cfg(feature = "rustls")] -use crate::http_connector::ConnectorSettings; -#[cfg(feature = "rustls")] -use crate::hyper_ext::Adapter as HyperAdapter; - -#[cfg(all(feature = "native-tls", not(feature = "allow-compilation")))] -compile_error!("Feature native-tls has been removed. For upgrade instructions, see: https://awslabs.github.io/smithy-rs/design/transport/connector.html"); - -/// Max idle connections is not standardized across SDKs. Java V1 and V2 use 50, and Go V2 uses 100. -/// The number below was chosen arbitrarily between those two reference points, and should allow -/// for 14 separate SDK clients in a Lambda where the max file handles is 1024. -#[cfg(feature = "rustls")] -const DEFAULT_MAX_IDLE_CONNECTIONS: usize = 70; - -/// Returns default HTTP client settings for hyper. -#[cfg(feature = "rustls")] -fn default_hyper_builder() -> hyper::client::Builder { - let mut builder = hyper::client::Builder::default(); - builder.pool_max_idle_per_host(DEFAULT_MAX_IDLE_CONNECTIONS); - builder -} - -#[cfg(feature = "rustls")] -impl Builder<(), M, R> { - /// Connect to the service over HTTPS using Rustls using dynamic dispatch. - pub fn rustls_connector( - self, - connector_settings: ConnectorSettings, - ) -> Builder { - self.connector(DynConnector::new( - HyperAdapter::builder() - .hyper_builder(default_hyper_builder()) - .connector_settings(connector_settings) - .build(crate::conns::https()), - )) - } -} - -#[cfg(feature = "rustls")] -impl Builder<(), M, R> { - /// Create a Smithy client builder with an HTTPS connector and the [standard retry - /// policy](crate::retry::Standard) over the default middleware implementation. - /// - /// For convenience, this constructor type-erases the concrete TLS connector backend used using - /// dynamic dispatch. This comes at a slight runtime performance cost. See - /// [`DynConnector`](crate::erase::DynConnector) for details. To avoid that overhead, use - /// [`Builder::rustls_connector`] instead. - #[cfg(feature = "rustls")] - pub fn dyn_https_connector( - self, - connector_settings: ConnectorSettings, - ) -> Builder { - let with_https = |b: Builder<_, M, R>| b.rustls_connector(connector_settings); - with_https(self) - } -} - -impl Builder<(), M, R> { - /// Specify the connector for the eventual client to use. - /// - /// The connector dictates how requests are turned into responses. Normally, this would entail - /// sending the request to some kind of remote server, but in certain settings it's useful to - /// be able to use a custom connector instead, such as to mock the network for tests. - /// - /// If you just want to specify a function from request to response instead, use - /// [`Builder::connector_fn`]. - pub fn connector(self, connector: C) -> Builder { - Builder { - connector: MaybeRequiresSleep::new(false, connector), - middleware: self.middleware, - retry_policy: self.retry_policy, - operation_timeout_config: self.operation_timeout_config, - sleep_impl: self.sleep_impl, - reconnect_mode: self.reconnect_mode, - } - } - - /// Use a function that directly maps each request to a response as a connector. - /// - /// ```no_run - /// use aws_smithy_client::Builder; - /// use aws_smithy_http::body::SdkBody; - /// let client = Builder::new() - /// # /* - /// .middleware(..) - /// # */ - /// # .middleware(tower::layer::util::Identity::new()) - /// .connector_fn(|req: http::Request| { - /// async move { - /// Ok(http::Response::new(SdkBody::empty())) - /// } - /// }) - /// .build(); - /// # client.check(); - /// ``` - pub fn connector_fn(self, map: F) -> Builder, M, R> - where - F: Fn(http::Request) -> FF + Send, - FF: std::future::Future, ConnectorError>>, - // NOTE: The extra bound here is to help the type checker give better errors earlier. - tower::util::ServiceFn: bounds::SmithyConnector, - { - self.connector(tower::service_fn(map)) - } -} - -impl Builder { - /// Specify the middleware for the eventual client ot use. - /// - /// The middleware adjusts requests before they are dispatched to the connector. It is - /// responsible for filling in any request parameters that aren't specified by the Smithy - /// protocol definition, such as those used for routing (like the URL), authentication, and - /// authorization. - /// - /// The middleware takes the form of a [`tower::Layer`] that wraps the actual connection for - /// each request. The [`tower::Service`] that the middleware produces must accept requests of - /// the type [`aws_smithy_http::operation::Request`] and return responses of the type - /// [`http::Response`], most likely by modifying the provided request in place, - /// passing it to the inner service, and then ultimately returning the inner service's - /// response. - /// - /// If your requests are already ready to be sent and need no adjustment, you can use - /// [`tower::layer::util::Identity`] as your middleware. - pub fn middleware(self, middleware: M) -> Builder { - Builder { - connector: self.connector, - retry_policy: self.retry_policy, - operation_timeout_config: self.operation_timeout_config, - middleware, - sleep_impl: self.sleep_impl, - reconnect_mode: self.reconnect_mode, - } - } - - /// Use a function-like middleware that directly maps each request. - /// - /// ```no_run - /// use aws_smithy_client::Builder; - /// use aws_smithy_client::erase::DynConnector; - /// use aws_smithy_client::never::NeverConnector; - /// use aws_smithy_http::body::SdkBody; - /// let my_connector = DynConnector::new( - /// // Your own connector here or use `dyn_https_connector()` - /// # NeverConnector::new() - /// ); - /// let client = Builder::new() - /// .connector(my_connector) - /// .middleware_fn(|req: aws_smithy_http::operation::Request| { - /// req - /// }) - /// .build(); - /// # client.check(); - /// ``` - pub fn middleware_fn(self, map: F) -> Builder, R> - where - F: Fn(aws_smithy_http::operation::Request) -> aws_smithy_http::operation::Request - + Clone - + Send - + Sync - + 'static, - { - self.middleware(tower::util::MapRequestLayer::new(map)) - } -} - -impl Builder { - /// Specify the retry policy for the eventual client to use. - /// - /// By default, the Smithy client uses a standard retry policy that works well in most - /// settings. You can use this method to override that policy with a custom one. A new policy - /// instance will be instantiated for each request using [`retry::NewRequestPolicy`]. Each - /// policy instance must implement [`tower::retry::Policy`]. - /// - /// If you just want to modify the policy _configuration_ for the standard retry policy, use - /// [`Builder::set_retry_config`]. - pub fn retry_policy(self, retry_policy: R) -> Builder { - Builder { - connector: self.connector, - retry_policy: MaybeRequiresSleep::new(false, retry_policy), - operation_timeout_config: self.operation_timeout_config, - middleware: self.middleware, - sleep_impl: self.sleep_impl, - reconnect_mode: self.reconnect_mode, - } - } -} - -impl Builder { - /// Set the standard retry policy's configuration. When `config` is `None`, - /// the default retry policy will be used. - pub fn set_retry_config(&mut self, config: Option) -> &mut Self { - let config = config.unwrap_or_default(); - self.retry_policy = - MaybeRequiresSleep::new(config.has_retry(), retry::Standard::new(config)); - self - } - - /// Set the standard retry policy's configuration. - pub fn retry_config(mut self, config: retry::Config) -> Self { - self.set_retry_config(Some(config)); - self - } - - /// Set operation timeout config for the client. If `operation_timeout_config` is - /// `None`, timeouts will be disabled. - pub fn set_operation_timeout_config( - &mut self, - operation_timeout_config: Option, - ) -> &mut Self { - self.operation_timeout_config = operation_timeout_config; - self - } - - /// Set operation timeout config for the client. - pub fn operation_timeout_config( - mut self, - operation_timeout_config: OperationTimeoutConfig, - ) -> Self { - self.operation_timeout_config = Some(operation_timeout_config); - self - } - - /// Set [`aws_smithy_async::rt::sleep::SharedAsyncSleep`] that the [`Client`] will use to create things like timeout futures. - pub fn set_sleep_impl(&mut self, async_sleep: Option) -> &mut Self { - self.sleep_impl = async_sleep; - self - } - - /// Set [`aws_smithy_async::rt::sleep::SharedAsyncSleep`] that the [`Client`] will use to create things like timeout futures. - pub fn sleep_impl(mut self, async_sleep: SharedAsyncSleep) -> Self { - self.set_sleep_impl(Some(async_sleep)); - self - } -} - -impl Builder { - /// Use a connector that wraps the current connector. - pub fn map_connector(self, map: F) -> Builder - where - F: FnOnce(C) -> C2, - { - Builder { - connector: MaybeRequiresSleep::new( - self.connector.requires_sleep, - map(self.connector.implementation), - ), - middleware: self.middleware, - retry_policy: self.retry_policy, - operation_timeout_config: self.operation_timeout_config, - sleep_impl: self.sleep_impl, - reconnect_mode: self.reconnect_mode, - } - } - - /// Use a middleware that wraps the current middleware. - pub fn map_middleware(self, map: F) -> Builder - where - F: FnOnce(M) -> M2, - { - Builder { - connector: self.connector, - middleware: map(self.middleware), - retry_policy: self.retry_policy, - operation_timeout_config: self.operation_timeout_config, - sleep_impl: self.sleep_impl, - reconnect_mode: self.reconnect_mode, - } - } - - /// Set the [`ReconnectMode`] for the retry strategy - /// - /// By default, no reconnection occurs. - /// - /// When enabled and a transient error is encountered, the connection in use will be poisoned. - /// This prevents reusing a connection to a potentially bad host. - pub fn reconnect_mode(mut self, reconnect_mode: ReconnectMode) -> Self { - self.set_reconnect_mode(Some(reconnect_mode)); - self - } - - /// Set the [`ReconnectMode`] for the retry strategy - /// - /// By default, no reconnection occurs. - /// - /// When enabled and a transient error is encountered, the connection in use will be poisoned. - /// This prevents reusing a connection to a potentially bad host. - pub fn set_reconnect_mode(&mut self, reconnect_mode: Option) -> &mut Self { - self.reconnect_mode = reconnect_mode; - self - } - - /// Enable reconnection on transient errors - /// - /// By default, when a transient error is encountered, the connection in use will be poisoned. - /// This prevents reusing a connection to a potentially bad host but may increase the load on - /// the server. - pub fn reconnect_on_transient_errors(self) -> Self { - self.reconnect_mode(ReconnectMode::ReconnectOnTransientError) - } - - /// Build a Smithy service [`Client`]. - pub fn build(self) -> Client { - let operation_timeout_config = self - .operation_timeout_config - .unwrap_or_else(|| TimeoutConfig::disabled().into()); - if self.sleep_impl.is_none() { - const ADDITIONAL_HELP: &str = - "Either disable retry by setting max attempts to one, or pass in a `sleep_impl`. \ - If you're not using Tokio, then an implementation of the `AsyncSleep` trait from \ - the `aws-smithy-async` crate is required for your async runtime. If you are using \ - Tokio, then make sure the `rt-tokio` feature is enabled to have its sleep \ - implementation set automatically."; - if self.connector.requires_sleep { - panic!("Socket-level retries for the default connector require a `sleep_impl`, but none was passed into the builder. {ADDITIONAL_HELP}"); - } - if self.retry_policy.requires_sleep { - panic!("Retries require a `sleep_impl`, but none was passed into the builder. {ADDITIONAL_HELP}"); - } - if operation_timeout_config.has_timeouts() { - panic!("Operation timeouts require a `sleep_impl`, but none was passed into the builder. {ADDITIONAL_HELP}"); - } - } - Client { - connector: self.connector.implementation, - retry_policy: self.retry_policy.implementation, - middleware: self.middleware, - operation_timeout_config, - sleep_impl: self.sleep_impl, - reconnect_mode: self.reconnect_mode.unwrap_or(default_reconnect_mode()), - } - } -} - -impl Builder -where - C: bounds::SmithyConnector, - M: bounds::SmithyMiddleware + Send + Sync + 'static, - R: retry::NewRequestPolicy, -{ - /// Build a type-erased Smithy service [`Client`]. - /// - /// Note that if you're using the standard retry mechanism, [`retry::Standard`], `DynClient` - /// is equivalent to [`Client`] with no type arguments. - /// - /// ```no_run - /// # #[cfg(feature = "https")] - /// # fn not_main() { - /// use aws_smithy_client::{Builder, Client}; - /// struct MyClient { - /// client: aws_smithy_client::Client, - /// } - /// - /// let client = Builder::new() - /// .https() - /// .middleware(tower::layer::util::Identity::new()) - /// .build_dyn(); - /// let client = MyClient { client }; - /// # client.client.check(); - /// # } - pub fn build_dyn(self) -> erase::DynClient { - self.build().into_dyn() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::never::NeverConnector; - use aws_smithy_async::rt::sleep::{AsyncSleep, Sleep}; - use std::panic::{self, AssertUnwindSafe}; - use std::time::Duration; - - #[derive(Clone, Debug)] - struct StubSleep; - impl AsyncSleep for StubSleep { - fn sleep(&self, _duration: Duration) -> Sleep { - todo!() - } - } - - #[test] - fn defaults_dont_panic() { - let builder = Builder::new() - .connector(NeverConnector::new()) - .middleware(tower::layer::util::Identity::new()); - - let _ = builder.build(); - } - - #[test] - fn defaults_panic_if_default_tokio_sleep_not_available() { - let mut builder = Builder::new() - .connector(NeverConnector::new()) - .middleware(tower::layer::util::Identity::new()); - builder.set_sleep_impl(None); - - let result = panic::catch_unwind(AssertUnwindSafe(move || { - let _ = builder.build(); - })); - assert!(result.is_err()); - } - - #[test] - fn timeouts_without_sleep_panics() { - let mut builder = Builder::new() - .connector(NeverConnector::new()) - .middleware(tower::layer::util::Identity::new()); - builder.set_sleep_impl(None); - - let timeout_config = TimeoutConfig::builder() - .connect_timeout(Duration::from_secs(1)) - .build(); - assert!(timeout_config.has_timeouts()); - builder.set_operation_timeout_config(Some(timeout_config.into())); - - let result = panic::catch_unwind(AssertUnwindSafe(move || { - let _ = builder.build(); - })); - assert!(result.is_err()); - } - - #[test] - fn retry_without_sleep_panics() { - let mut builder = Builder::new() - .connector(NeverConnector::new()) - .middleware(tower::layer::util::Identity::new()); - builder.set_sleep_impl(None); - - let retry_config = retry::Config::default(); - assert!(retry_config.has_retry()); - builder.set_retry_config(Some(retry_config)); - - let result = panic::catch_unwind(AssertUnwindSafe(move || { - let _ = builder.build(); - })); - assert!(result.is_err()); - } - - #[test] - fn custom_retry_policy_without_sleep_doesnt_panic() { - let mut builder = Builder::new() - .connector(NeverConnector::new()) - .middleware(tower::layer::util::Identity::new()) - // Using standard retry here as a shortcut in the test; someone setting - // a custom retry policy would manually implement the required traits - .retry_policy(retry::Standard::default()); - builder.set_sleep_impl(None); - let _ = builder.build(); - } - - #[test] - fn no_panics_when_sleep_given() { - let mut builder = Builder::new() - .connector(NeverConnector::new()) - .middleware(tower::layer::util::Identity::new()); - - let timeout_config = TimeoutConfig::builder() - .connect_timeout(Duration::from_secs(1)) - .build(); - assert!(timeout_config.has_timeouts()); - builder.set_operation_timeout_config(Some(timeout_config.into())); - - let retry_config = retry::Config::default(); - assert!(retry_config.has_retry()); - builder.set_retry_config(Some(retry_config)); - - let _ = builder.build(); - } - - #[test] - fn builder_connection_helpers_are_dyn() { - #[cfg(feature = "rustls")] - let _builder: Builder = - Builder::new().rustls_connector(Default::default()); - } -} diff --git a/rust-runtime/aws-smithy-client/src/conns.rs b/rust-runtime/aws-smithy-client/src/conns.rs deleted file mode 100644 index 8ee1d8a929..0000000000 --- a/rust-runtime/aws-smithy-client/src/conns.rs +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Type aliases for standard connection types. - -use crate::erase::DynConnector; - -#[cfg(feature = "rustls")] -/// A `hyper` connector that uses the `rustls` crate for TLS. To use this in a smithy client, -/// wrap it in a [hyper_ext::Adapter](crate::hyper_ext::Adapter). -pub type Https = hyper_rustls::HttpsConnector; - -#[cfg(feature = "rustls")] -/// A smithy connector that uses the `rustls` crate for TLS. -pub type Rustls = crate::hyper_ext::Adapter; - -#[cfg(feature = "rustls")] -use hyper_rustls::ConfigBuilderExt; - -// Creating a `with_native_roots` HTTP client takes 300ms on OS X. Cache this so that we -// don't need to repeatedly incur that cost. -#[cfg(feature = "rustls")] -lazy_static::lazy_static! { - static ref HTTPS_NATIVE_ROOTS: Https = { - hyper_rustls::HttpsConnectorBuilder::new() - .with_tls_config( - rustls::ClientConfig::builder() - .with_cipher_suites(&[ - // TLS1.3 suites - rustls::cipher_suite::TLS13_AES_256_GCM_SHA384, - rustls::cipher_suite::TLS13_AES_128_GCM_SHA256, - // TLS1.2 suites - rustls::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - rustls::cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - rustls::cipher_suite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - rustls::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - rustls::cipher_suite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - ]) - .with_safe_default_kx_groups() - .with_safe_default_protocol_versions() - .expect("Error with the TLS configuration. Please file a bug report under https://github.com/awslabs/smithy-rs/issues.") - .with_native_roots() - .with_no_client_auth() - ) - .https_or_http() - .enable_http1() - .enable_http2() - .build() - }; -} - -mod default_connector { - use crate::erase::DynConnector; - use crate::http_connector::ConnectorSettings; - use aws_smithy_async::rt::sleep::SharedAsyncSleep; - - #[cfg(feature = "rustls")] - fn base( - settings: &ConnectorSettings, - sleep: Option, - ) -> crate::hyper_ext::Builder { - let mut hyper = crate::hyper_ext::Adapter::builder().connector_settings(settings.clone()); - if let Some(sleep) = sleep { - hyper = hyper.sleep_impl(sleep); - } - hyper - } - - /// Given `ConnectorSettings` and an `SharedAsyncSleep`, create a `DynConnector` from defaults depending on what cargo features are activated. - pub fn default_connector( - settings: &ConnectorSettings, - sleep: Option, - ) -> Option { - #[cfg(feature = "rustls")] - { - tracing::trace!(settings = ?settings, sleep = ?sleep, "creating a new default connector"); - let hyper = base(settings, sleep).build(super::https()); - Some(DynConnector::new(hyper)) - } - #[cfg(not(feature = "rustls"))] - { - tracing::trace!(settings = ?settings, sleep = ?sleep, "no default connector available"); - None - } - } -} -pub use default_connector::default_connector; - -/// Error that indicates a connector is required. -#[non_exhaustive] -#[derive(Debug, Default)] -pub struct ConnectorRequiredError; - -impl std::fmt::Display for ConnectorRequiredError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("No HTTP connector was available. Enable the `rustls` crate feature or set a connector to fix this.") - } -} - -impl std::error::Error for ConnectorRequiredError {} - -/// Converts an optional connector to a result. -pub fn require_connector( - connector: Option, -) -> Result { - connector.ok_or(ConnectorRequiredError) -} - -#[cfg(feature = "rustls")] -/// Return a default HTTPS connector backed by the `rustls` crate. -/// -/// It requires a minimum TLS version of 1.2. -/// It allows you to connect to both `http` and `https` URLs. -pub fn https() -> Https { - HTTPS_NATIVE_ROOTS.clone() -} - -#[cfg(all(test, feature = "rustls"))] -mod tests { - use crate::erase::DynConnector; - use crate::hyper_ext::Adapter; - use aws_smithy_http::body::SdkBody; - use http::{Method, Request, Uri}; - use tower::{Service, ServiceBuilder}; - - async fn send_request_and_assert_success(conn: DynConnector, uri: &Uri) { - let mut svc = ServiceBuilder::new().service(conn); - let req = Request::builder() - .uri(uri) - .method(Method::GET) - .body(SdkBody::empty()) - .unwrap(); - let res = svc.call(req).await.unwrap(); - assert!(res.status().is_success()); - } - - #[cfg(feature = "rustls")] - mod rustls_tests { - use super::super::https; - use super::*; - - #[tokio::test] - async fn test_rustls_connector_can_make_http_requests() { - let conn = Adapter::builder().build(https()); - let conn = DynConnector::new(conn); - let http_uri: Uri = "http://example.com/".parse().unwrap(); - - send_request_and_assert_success(conn, &http_uri).await; - } - - #[tokio::test] - async fn test_rustls_connector_can_make_https_requests() { - let conn = Adapter::builder().build(https()); - let conn = DynConnector::new(conn); - let https_uri: Uri = "https://example.com/".parse().unwrap(); - - send_request_and_assert_success(conn, &https_uri).await; - } - } -} - -#[cfg(test)] -mod custom_tls_tests { - use crate::erase::DynConnector; - use crate::hyper_ext::Adapter; - use aws_smithy_http::body::SdkBody; - use http::{Method, Request, Uri}; - use tower::{Service, ServiceBuilder}; - - type NativeTls = hyper_tls::HttpsConnector; - - fn native_tls() -> NativeTls { - let mut tls = hyper_tls::native_tls::TlsConnector::builder(); - let tls = tls - .min_protocol_version(Some(hyper_tls::native_tls::Protocol::Tlsv12)) - .build() - .unwrap_or_else(|e| panic!("Error while creating TLS connector: {}", e)); - let mut http = hyper::client::HttpConnector::new(); - http.enforce_http(false); - hyper_tls::HttpsConnector::from((http, tls.into())) - } - - #[tokio::test] - async fn test_native_tls_connector_can_make_http_requests() { - let conn = Adapter::builder().build(native_tls()); - let conn = DynConnector::new(conn); - let http_uri: Uri = "http://example.com/".parse().unwrap(); - - send_request_and_assert_success(conn, &http_uri).await; - } - - #[tokio::test] - async fn test_native_tls_connector_can_make_https_requests() { - let conn = Adapter::builder().build(native_tls()); - let conn = DynConnector::new(conn); - let https_uri: Uri = "https://example.com/".parse().unwrap(); - - send_request_and_assert_success(conn, &https_uri).await; - } - - async fn send_request_and_assert_success(conn: DynConnector, uri: &Uri) { - let mut svc = ServiceBuilder::new().service(conn); - let mut att = 0; - let res = loop { - let req = Request::builder() - .uri(uri) - .method(Method::GET) - .body(SdkBody::empty()) - .unwrap(); - if let Ok(res) = svc.call(req).await { - break res; - } - assert!(att < 5); - att += 1; - }; - assert!(res.status().is_success()); - } -} diff --git a/rust-runtime/aws-smithy-client/src/dvr.rs b/rust-runtime/aws-smithy-client/src/dvr.rs deleted file mode 100644 index f0f68a4709..0000000000 --- a/rust-runtime/aws-smithy-client/src/dvr.rs +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Extremely Experimental Test Connection -//! -//! Warning: Extremely experimental, API likely to change. -//! -//! DVR is an extremely experimental record & replay framework that supports multi-frame HTTP request / response traffic. - -use std::collections::HashMap; - -use bytes::Bytes; -use serde::{Deserialize, Serialize}; - -pub use aws_smithy_protocol_test::MediaType; -use aws_smithy_types::base64; -pub use record::RecordingConnection; -pub use replay::ReplayingConnection; - -mod record; -mod replay; - -/// A complete traffic recording -/// -/// A traffic recording can be replayed with [`RecordingConnection`](RecordingConnection) -#[derive(Debug, Serialize, Deserialize)] -pub struct NetworkTraffic { - events: Vec, - docs: Option, - version: Version, -} - -impl NetworkTraffic { - /// Network events - pub fn events(&self) -> &Vec { - &self.events - } -} - -/// Serialization version of DVR data -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub enum Version { - /// Initial network traffic version - V0, -} - -/// A network traffic recording may contain multiple different connections occurring simultaneously -#[derive(Copy, Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] -pub struct ConnectionId(usize); - -/// A network event -/// -/// Network events consist of a connection identifier and an action. An event is sufficient to -/// reproduce traffic later during replay -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -pub struct Event { - connection_id: ConnectionId, - action: Action, -} - -/// An initial HTTP request, roughly equivalent to `http::Request<()>` -/// -/// The initial request phase of an HTTP request. The body will be -/// sent later as a separate action. -#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] -pub struct Request { - uri: String, - headers: HashMap>, - method: String, -} - -/// An initial HTTP response roughly equivalent to `http::Response<()>` -/// -/// The initial response phase of an HTTP request. The body will be -/// sent later as a separate action. -#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] -pub struct Response { - status: u16, - version: String, - headers: HashMap>, -} - -impl From<&Request> for http::Request<()> { - fn from(request: &Request) -> Self { - let mut builder = http::Request::builder().uri(request.uri.as_str()); - for (k, values) in request.headers.iter() { - for v in values { - builder = builder.header(k, v); - } - } - builder.method(request.method.as_str()).body(()).unwrap() - } -} - -impl<'a, B> From<&'a http::Request> for Request { - fn from(req: &'a http::Request) -> Self { - let uri = req.uri().to_string(); - let headers = headers_to_map(req.headers()); - let method = req.method().to_string(); - Self { - uri, - headers, - method, - } - } -} - -fn headers_to_map(headers: &http::HeaderMap) -> HashMap> { - let mut out: HashMap<_, Vec<_>> = HashMap::new(); - for (header_name, header_value) in headers.iter() { - let entry = out.entry(header_name.to_string()).or_default(); - entry.push(header_value.to_str().unwrap().to_string()); - } - out -} - -impl<'a, B> From<&'a http::Response> for Response { - fn from(resp: &'a http::Response) -> Self { - let status = resp.status().as_u16(); - let version = format!("{:?}", resp.version()); - let headers = headers_to_map(resp.headers()); - Self { - status, - version, - headers, - } - } -} - -/// Error response wrapper -#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] -pub struct Error(String); - -/// Network Action -#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] -#[non_exhaustive] -pub enum Action { - /// Initial HTTP Request - Request { - /// HTTP Request headers, method, and URI - request: Request, - }, - - /// Initial HTTP response or failure - Response { - /// HTTP response or failure - response: Result, - }, - - /// Data segment - Data { - /// Body Data - data: BodyData, - /// Direction: request vs. response - direction: Direction, - }, - - /// End of data - Eof { - /// Succesful vs. failed termination - ok: bool, - /// Direction: request vs. response - direction: Direction, - }, -} - -/// Event direction -/// -/// During replay, this is used to replay data in the right direction -#[derive(Copy, Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub enum Direction { - /// Request phase - Request, - /// Response phase - Response, -} - -impl Direction { - /// The opposite of a given direction - pub fn opposite(self) -> Self { - match self { - Direction::Request => Direction::Response, - Direction::Response => Direction::Request, - } - } -} - -/// HTTP Body Data Abstraction -/// -/// When the data is a UTF-8 encoded string, it will be serialized as a string for readability. -/// Otherwise, it will be base64 encoded. -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[non_exhaustive] -pub enum BodyData { - /// UTF-8 encoded data - Utf8(String), - - /// Base64 encoded binary data - Base64(String), -} - -impl BodyData { - /// Convert [`BodyData`](BodyData) into Bytes - pub fn into_bytes(self) -> Vec { - match self { - BodyData::Utf8(string) => string.into_bytes(), - BodyData::Base64(string) => base64::decode(string).unwrap(), - } - } - - /// Copy [`BodyData`](BodyData) into a `Vec` - pub fn copy_to_vec(&self) -> Vec { - match self { - BodyData::Utf8(string) => string.as_bytes().into(), - BodyData::Base64(string) => base64::decode(string).unwrap(), - } - } -} - -impl From for BodyData { - fn from(data: Bytes) -> Self { - match std::str::from_utf8(data.as_ref()) { - Ok(string) => BodyData::Utf8(string.to_string()), - Err(_) => BodyData::Base64(base64::encode(data)), - } - } -} - -#[cfg(test)] -mod tests { - use std::error::Error; - use std::fs; - - use aws_smithy_http::body::SdkBody; - use aws_smithy_http::byte_stream::ByteStream; - - use crate::dvr::{NetworkTraffic, RecordingConnection, ReplayingConnection}; - use bytes::Bytes; - use http::Uri; - - #[tokio::test] - async fn turtles_all_the_way_down() -> Result<(), Box> { - // create a replaying connection from a recording, wrap a recording connection around it, - // make a request, then verify that the same traffic was recorded. - let network_traffic = fs::read_to_string("test-data/example.com.json")?; - let network_traffic: NetworkTraffic = serde_json::from_str(&network_traffic)?; - let inner = ReplayingConnection::new(network_traffic.events.clone()); - let mut connection = RecordingConnection::new(inner.clone()); - let req = http::Request::post("https://www.example.com") - .body(SdkBody::from("hello world")) - .unwrap(); - use tower::Service; - let mut resp = connection.call(req).await.expect("ok"); - let body = std::mem::replace(resp.body_mut(), SdkBody::taken()); - let data = ByteStream::new(body).collect().await.unwrap().into_bytes(); - assert_eq!( - String::from_utf8(data.to_vec()).unwrap(), - "hello from example.com" - ); - assert_eq!( - connection.events().as_slice(), - network_traffic.events.as_slice() - ); - let requests = inner.take_requests().await; - assert_eq!( - requests[0].uri(), - &Uri::from_static("https://www.example.com") - ); - assert_eq!( - requests[0].body(), - &Bytes::from_static("hello world".as_bytes()) - ); - Ok(()) - } -} diff --git a/rust-runtime/aws-smithy-client/src/dvr/record.rs b/rust-runtime/aws-smithy-client/src/dvr/record.rs deleted file mode 100644 index 9415528f73..0000000000 --- a/rust-runtime/aws-smithy-client/src/dvr/record.rs +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use std::future::Future; -use std::pin::Pin; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, MutexGuard}; -use std::task::{Context, Poll}; - -use http_body::Body; -use tokio::task::JoinHandle; -use tower::Service; - -use aws_smithy_http::body::SdkBody; - -use crate::dvr::{self, Action, BodyData, ConnectionId, Direction, Error, NetworkTraffic, Version}; - -use super::Event; -use std::fmt::Display; -use std::io; -use std::path::Path; - -/// Recording Connection Wrapper -/// -/// RecordingConnection wraps an inner connection and records all traffic, enabling traffic replay. -#[derive(Clone, Debug)] -pub struct RecordingConnection { - pub(crate) data: Arc>>, - pub(crate) num_events: Arc, - pub(crate) inner: S, -} - -#[cfg(all(feature = "rustls", feature = "client-hyper"))] -impl RecordingConnection> { - /// Construct a recording connection wrapping a default HTTPS implementation - pub fn https() -> Self { - Self { - data: Default::default(), - inner: crate::hyper_ext::Adapter::builder().build(crate::conns::https()), - num_events: Arc::new(AtomicUsize::new(0)), - } - } -} - -impl RecordingConnection { - /// Create a new recording connection from a connection - pub fn new(connection: S) -> Self { - Self { - data: Default::default(), - inner: connection, - num_events: Arc::new(AtomicUsize::new(0)), - } - } - - /// Return the traffic recorded by this connection - pub fn events(&self) -> MutexGuard<'_, Vec> { - self.data.lock().unwrap() - } - - /// NetworkTraffic struct suitable for serialization - pub fn network_traffic(&self) -> NetworkTraffic { - NetworkTraffic { - events: self.events().clone(), - docs: Some("todo docs".into()), - version: Version::V0, - } - } - - /// Dump the network traffic to a file - pub fn dump_to_file(&self, path: impl AsRef) -> Result<(), io::Error> { - std::fs::write( - path, - serde_json::to_string(&self.network_traffic()).unwrap(), - ) - } - - fn next_id(&self) -> ConnectionId { - ConnectionId(self.num_events.fetch_add(1, Ordering::Relaxed)) - } -} - -fn record_body( - body: &mut SdkBody, - event_id: ConnectionId, - direction: Direction, - event_bus: Arc>>, -) -> JoinHandle<()> { - let (sender, output_body) = hyper::Body::channel(); - let real_body = std::mem::replace(body, SdkBody::from(output_body)); - tokio::spawn(async move { - let mut real_body = real_body; - let mut sender = sender; - loop { - let data = real_body.data().await; - match data { - Some(Ok(data)) => { - event_bus.lock().unwrap().push(Event { - connection_id: event_id, - action: Action::Data { - data: BodyData::from(data.clone()), - direction, - }, - }); - // This happens if the real connection is closed during recording. - // Need to think more carefully if this is the correct thing to log in this - // case. - if sender.send_data(data).await.is_err() { - event_bus.lock().unwrap().push(Event { - connection_id: event_id, - action: Action::Eof { - direction: direction.opposite(), - ok: false, - }, - }) - }; - } - None => { - event_bus.lock().unwrap().push(Event { - connection_id: event_id, - action: Action::Eof { - ok: true, - direction, - }, - }); - drop(sender); - break; - } - Some(Err(_err)) => { - event_bus.lock().unwrap().push(Event { - connection_id: event_id, - action: Action::Eof { - ok: false, - direction, - }, - }); - sender.abort(); - break; - } - } - } - }) -} - -impl tower::Service> for RecordingConnection -where - S: Service, Response = http::Response> - + Send - + Clone - + 'static, - S::Error: Display + Send + Sync + 'static, - S::Future: Send + 'static, - ResponseBody: Into, -{ - type Response = http::Response; - type Error = S::Error; - #[allow(clippy::type_complexity)] - type Future = - Pin, Self::Error>> + Send>>; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, mut req: http::Request) -> Self::Future { - let event_id = self.next_id(); - // A request has two 3 phases: - // 1. A "Request" phase. This is initial HTTP request, headers, & URI - // 2. A body phase. This may contain multiple data segments. - // 3. A finalization phase. An EOF of some sort is sent on the body to indicate that - // the channel should be closed. - - // Phase 1: the initial http request - self.data.lock().unwrap().push(Event { - connection_id: event_id, - action: Action::Request { - request: dvr::Request::from(&req), - }, - }); - - // Phase 2: Swap out the real request body for one that will log all traffic that passes - // through it - // This will also handle phase three when the request body runs out of data. - record_body( - req.body_mut(), - event_id, - Direction::Request, - self.data.clone(), - ); - let events = self.data.clone(); - // create a channel we'll use to stream the data while reading it - let resp_fut = self.inner.call(req); - let fut = async move { - let resp = resp_fut.await; - match resp { - Ok(resp) => { - // wrap the hyper body in an SDK body - let mut resp = resp.map(|body| body.into()); - - // push the initial response event - events.lock().unwrap().push(Event { - connection_id: event_id, - action: Action::Response { - response: Ok(dvr::Response::from(&resp)), - }, - }); - - // instrument the body and record traffic - record_body(resp.body_mut(), event_id, Direction::Response, events); - Ok(resp) - } - Err(e) => { - events.lock().unwrap().push(Event { - connection_id: event_id, - action: Action::Response { - response: Err(Error(format!("{}", &e))), - }, - }); - Err(e) - } - } - }; - Box::pin(fut) - } -} diff --git a/rust-runtime/aws-smithy-client/src/dvr/replay.rs b/rust-runtime/aws-smithy-client/src/dvr/replay.rs deleted file mode 100644 index 27e606f082..0000000000 --- a/rust-runtime/aws-smithy-client/src/dvr/replay.rs +++ /dev/null @@ -1,365 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use std::collections::{HashMap, VecDeque}; -use std::error::Error; -use std::ops::DerefMut; -use std::path::Path; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; -use std::task::{Context, Poll}; - -use bytes::{Bytes, BytesMut}; -use http::{Request, Version}; -use http_body::Body; -use tokio::task::JoinHandle; - -use aws_smithy_http::body::SdkBody; -use aws_smithy_http::result::ConnectorError; -use aws_smithy_protocol_test::MediaType; -use aws_smithy_types::error::display::DisplayErrorContext; - -use crate::dvr::{Action, ConnectionId, Direction, Event, NetworkTraffic}; - -/// Wrapper type to enable optionally waiting for a future to complete -#[derive(Debug)] -enum Waitable { - Loading(JoinHandle), - Value(T), -} - -impl Waitable { - /// Consumes the future and returns the value - async fn take(self) -> T { - match self { - Waitable::Loading(f) => f.await.expect("join failed"), - Waitable::Value(value) => value, - } - } - - /// Waits for the future to be ready - async fn wait(&mut self) { - match self { - Waitable::Loading(f) => *self = Waitable::Value(f.await.expect("join failed")), - Waitable::Value(_) => {} - } - } -} - -/// Replay traffic recorded by a [`RecordingConnection`](super::RecordingConnection) -#[derive(Clone, Debug)] -pub struct ReplayingConnection { - live_events: Arc>>>, - verifiable_events: Arc>>, - num_events: Arc, - recorded_requests: Arc>>>>, -} - -impl ReplayingConnection { - fn next_id(&self) -> ConnectionId { - ConnectionId(self.num_events.fetch_add(1, Ordering::Relaxed)) - } - - /// Validate all headers and bodies - pub async fn full_validate(self, media_type: MediaType) -> Result<(), Box> { - self.validate_body_and_headers(None, media_type).await - } - - /// Validate actual requests against expected requests - pub async fn validate( - self, - checked_headers: &[&str], - body_comparer: impl Fn(&[u8], &[u8]) -> Result<(), Box>, - ) -> Result<(), Box> { - self.validate_base(Some(checked_headers), body_comparer) - .await - } - - /// Validate that the bodies match, using a given [`MediaType`] for comparison - /// - /// The specified headers are also validated - pub async fn validate_body_and_headers( - self, - checked_headers: Option<&[&str]>, - media_type: MediaType, - ) -> Result<(), Box> { - self.validate_base(checked_headers, |b1, b2| { - aws_smithy_protocol_test::validate_body( - b1, - std::str::from_utf8(b2).unwrap(), - media_type.clone(), - ) - .map_err(|e| Box::new(e) as _) - }) - .await - } - - async fn validate_base( - self, - checked_headers: Option<&[&str]>, - body_comparer: impl Fn(&[u8], &[u8]) -> Result<(), Box>, - ) -> Result<(), Box> { - let mut actual_requests = - std::mem::take(self.recorded_requests.lock().unwrap().deref_mut()); - for conn_id in 0..self.verifiable_events.len() { - let conn_id = ConnectionId(conn_id); - let expected = self.verifiable_events.get(&conn_id).unwrap(); - let actual = actual_requests - .remove(&conn_id) - .ok_or(format!( - "expected connection {:?} but request was never sent", - conn_id - ))? - .take() - .await; - aws_smithy_protocol_test::assert_uris_match(expected.uri(), actual.uri()); - body_comparer(expected.body().as_ref(), actual.body().as_ref())?; - let expected_headers = expected - .headers() - .keys() - .map(|k| k.as_str()) - .filter(|k| match checked_headers { - Some(list) => list.contains(k), - None => true, - }) - .flat_map(|key| { - let _ = expected.headers().get(key)?; - Some(( - key, - expected - .headers() - .get_all(key) - .iter() - .map(|h| h.to_str().unwrap()) - .collect::>() - .join(", "), - )) - }) - .collect::>(); - aws_smithy_protocol_test::validate_headers(actual.headers(), expected_headers) - .map_err(|err| { - format!( - "event {} validation failed with: {}", - conn_id.0, - DisplayErrorContext(&err) - ) - })?; - } - Ok(()) - } - - /// Return all the recorded requests for further analysis - pub async fn take_requests(self) -> Vec> { - let mut recorded_requests = - std::mem::take(self.recorded_requests.lock().unwrap().deref_mut()); - let mut out = Vec::with_capacity(recorded_requests.len()); - for conn_id in 0..recorded_requests.len() { - out.push( - recorded_requests - .remove(&ConnectionId(conn_id)) - .expect("should exist") - .take() - .await, - ) - } - out - } - - /// Build a replay connection from a JSON file - pub fn from_file(path: impl AsRef) -> Result> { - let events: NetworkTraffic = - serde_json::from_str(&std::fs::read_to_string(path.as_ref())?)?; - Ok(Self::new(events.events)) - } - - /// Build a replay connection from a sequence of events - pub fn new(events: Vec) -> Self { - let mut event_map: HashMap<_, VecDeque<_>> = HashMap::new(); - for event in events { - let event_buffer = event_map.entry(event.connection_id).or_default(); - event_buffer.push_back(event); - } - let verifiable_events = event_map - .iter() - .map(|(id, events)| { - let mut body = BytesMut::new(); - for event in events { - if let Action::Data { - direction: Direction::Request, - data, - } = &event.action - { - body.extend_from_slice(&data.copy_to_vec()); - } - } - let initial_request = events.iter().next().expect("must have one event"); - let request = match &initial_request.action { - Action::Request { request } => { - http::Request::from(request).map(|_| Bytes::from(body)) - } - _ => panic!("invalid first event"), - }; - (*id, request) - }) - .collect(); - let verifiable_events = Arc::new(verifiable_events); - - ReplayingConnection { - live_events: Arc::new(Mutex::new(event_map)), - num_events: Arc::new(AtomicUsize::new(0)), - recorded_requests: Default::default(), - verifiable_events, - } - } -} - -async fn replay_body(events: VecDeque, mut sender: hyper::body::Sender) { - for event in events { - match event.action { - Action::Request { .. } => panic!(), - Action::Response { .. } => panic!(), - Action::Data { - data, - direction: Direction::Response, - } => { - sender - .send_data(Bytes::from(data.into_bytes())) - .await - .expect("this is in memory traffic that should not fail to send"); - } - Action::Data { - data: _data, - direction: Direction::Request, - } => {} - Action::Eof { - direction: Direction::Request, - .. - } => {} - Action::Eof { - direction: Direction::Response, - ok: true, - .. - } => { - drop(sender); - break; - } - Action::Eof { - direction: Direction::Response, - ok: false, - .. - } => { - sender.abort(); - break; - } - } - } -} - -fn convert_version(version: &str) -> Version { - match version { - "HTTP/1.1" => Version::HTTP_11, - "HTTP/2.0" => Version::HTTP_2, - _ => panic!("unsupported: {}", version), - } -} - -impl tower::Service> for ReplayingConnection { - type Response = http::Response; - type Error = ConnectorError; - - #[allow(clippy::type_complexity)] - type Future = std::pin::Pin< - Box> + Send + 'static>, - >; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, mut req: Request) -> Self::Future { - let event_id = self.next_id(); - tracing::debug!("received event {}: {req:?}", event_id.0); - let mut events = match self.live_events.lock().unwrap().remove(&event_id) { - Some(traffic) => traffic, - None => { - return Box::pin(std::future::ready(Err(ConnectorError::other( - format!("no data for event {}. req: {:?}", event_id.0, req).into(), - None, - )))); - } - }; - - let _initial_request = events.pop_front().unwrap(); - let (sender, response_body) = hyper::Body::channel(); - let body = SdkBody::from(response_body); - let recording = self.recorded_requests.clone(); - let recorded_request = tokio::spawn(async move { - let mut data_read = vec![]; - while let Some(data) = req.body_mut().data().await { - data_read - .extend_from_slice(data.expect("in memory request should not fail").as_ref()) - } - req.map(|_| Bytes::from(data_read)) - }); - let mut recorded_request = Waitable::Loading(recorded_request); - let fut = async move { - let resp = loop { - let event = events - .pop_front() - .expect("no events, needed a response event"); - match event.action { - // to ensure deterministic behavior if the request EOF happens first in the log, - // wait for the request body to be done before returning a response. - Action::Eof { - direction: Direction::Request, - .. - } => { - recorded_request.wait().await; - } - Action::Request { .. } => panic!("invalid"), - Action::Response { - response: Err(error), - } => break Err(ConnectorError::other(error.0.into(), None)), - Action::Response { - response: Ok(response), - } => { - let mut builder = http::Response::builder() - .status(response.status) - .version(convert_version(&response.version)); - for (name, values) in response.headers { - for value in values { - builder = builder.header(&name, &value); - } - } - tokio::spawn(async move { - replay_body(events, sender).await; - // insert the finalized body into - }); - break Ok(builder.body(body).expect("valid builder")); - } - - Action::Data { - direction: Direction::Request, - data: _data, - } => { - tracing::info!("get request data"); - } - Action::Eof { - direction: Direction::Response, - .. - } => panic!("got eof before response"), - - Action::Data { - data: _, - direction: Direction::Response, - } => panic!("got response data before response"), - } - }; - recording.lock().unwrap().insert(event_id, recorded_request); - resp - }; - Box::pin(fut) - } -} diff --git a/rust-runtime/aws-smithy-client/src/erase.rs b/rust-runtime/aws-smithy-client/src/erase.rs deleted file mode 100644 index 406dd97393..0000000000 --- a/rust-runtime/aws-smithy-client/src/erase.rs +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Type-erased variants of [`Client`] and friends. - -use std::fmt; - -use tower::{Layer, Service, ServiceExt}; - -use aws_smithy_http::body::SdkBody; -use aws_smithy_http::result::ConnectorError; -use boxclone::*; - -use crate::{bounds, retry, Client}; - -// These types are technically public in that they're reachable from the public trait impls on -// DynMiddleware, but no-one should ever look at them or use them. -#[doc(hidden)] -pub mod boxclone; - -/// A [`Client`] whose connector and middleware types have been erased. -/// -/// Mainly useful if you need to name `R` in a type-erased client. If you do not, you can instead -/// just use `Client` with no type parameters, which ends up being the same type. -pub type DynClient = Client, R>; - -impl Client -where - C: bounds::SmithyConnector, - M: bounds::SmithyMiddleware + Send + Sync + 'static, - R: retry::NewRequestPolicy, -{ - /// Erase the middleware type from the client type signature. - /// - /// This makes the final client type easier to name, at the cost of a marginal increase in - /// runtime performance. See [`DynMiddleware`] for details. - /// - /// In practice, you'll use this method once you've constructed a client to your liking: - /// - /// ```no_run - /// # #[cfg(feature = "https")] - /// # fn not_main() { - /// use aws_smithy_client::{Builder, Client}; - /// struct MyClient { - /// client: Client, - /// } - /// - /// let client = Builder::new() - /// .https() - /// .middleware(tower::layer::util::Identity::new()) - /// .build(); - /// let client = MyClient { client: client.into_dyn_middleware() }; - /// # client.client.check(); - /// # } - pub fn into_dyn_middleware(self) -> Client, R> { - Client { - connector: self.connector, - middleware: DynMiddleware::new(self.middleware), - retry_policy: self.retry_policy, - operation_timeout_config: self.operation_timeout_config, - sleep_impl: self.sleep_impl, - reconnect_mode: self.reconnect_mode, - } - } -} - -impl Client -where - C: bounds::SmithyConnector, - M: bounds::SmithyMiddleware + Send + Sync + 'static, - R: retry::NewRequestPolicy, -{ - /// Erase the connector type from the client type signature. - /// - /// This makes the final client type easier to name, at the cost of a marginal increase in - /// runtime performance. See [`DynConnector`] for details. - /// - /// In practice, you'll use this method once you've constructed a client to your liking: - /// - /// ```no_run - /// # #[cfg(feature = "https")] - /// # fn not_main() { - /// # type MyMiddleware = aws_smithy_client::DynMiddleware; - /// use aws_smithy_client::{Builder, Client}; - /// struct MyClient { - /// client: Client, - /// } - /// - /// let client = Builder::new() - /// .https() - /// .middleware(tower::layer::util::Identity::new()) - /// .build(); - /// let client = MyClient { client: client.into_dyn_connector() }; - /// # client.client.check(); - /// # } - pub fn into_dyn_connector(self) -> Client { - Client { - connector: DynConnector::new(self.connector), - middleware: self.middleware, - retry_policy: self.retry_policy, - operation_timeout_config: self.operation_timeout_config, - sleep_impl: self.sleep_impl, - reconnect_mode: self.reconnect_mode, - } - } - - /// Erase the connector and middleware types from the client type signature. - /// - /// This makes the final client type easier to name, at the cost of a marginal increase in - /// runtime performance. See [`DynConnector`] and [`DynMiddleware`] for details. - /// - /// Note that if you're using the standard retry mechanism, [`retry::Standard`], `DynClient` - /// is equivalent to `Client` with no type arguments. - /// - /// In practice, you'll use this method once you've constructed a client to your liking: - /// - /// ```no_run - /// # #[cfg(feature = "https")] - /// # fn not_main() { - /// use aws_smithy_client::{Builder, Client}; - /// struct MyClient { - /// client: aws_smithy_client::Client, - /// } - /// - /// let client = Builder::new() - /// .https() - /// .middleware(tower::layer::util::Identity::new()) - /// .build(); - /// let client = MyClient { client: client.into_dyn() }; - /// # client.client.check(); - /// # } - pub fn into_dyn(self) -> DynClient { - self.into_dyn_connector().into_dyn_middleware() - } -} - -/// A Smithy connector that uses dynamic dispatch. -/// -/// This type allows you to pay a small runtime cost to avoid having to name the exact connector -/// you're using anywhere you want to hold a [`Client`]. Specifically, this will use `Box` to -/// enable dynamic dispatch for every request that goes through the connector, which increases -/// memory pressure and suffers an additional vtable indirection for each request, but is unlikely -/// to matter in all but the highest-performance settings. -#[non_exhaustive] -#[derive(Clone)] -pub struct DynConnector( - BoxCloneService, http::Response, ConnectorError>, -); - -impl fmt::Debug for DynConnector { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_struct("DynConnector").finish() - } -} - -impl DynConnector { - /// Construct a new dynamically-dispatched Smithy middleware. - pub fn new(connector: C) -> Self - where - C: bounds::SmithyConnector + Send + 'static, - E: Into, - { - Self(BoxCloneService::new(connector.map_err(|e| e.into()))) - } - - #[doc(hidden)] - pub fn call_lite( - &mut self, - req: http::Request, - ) -> BoxFuture, ConnectorError> { - Service::call(self, req) - } -} - -impl Service> for DynConnector { - type Response = http::Response; - type Error = ConnectorError; - type Future = BoxFuture; - - fn poll_ready( - &mut self, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.0.poll_ready(cx) - } - - fn call(&mut self, req: http::Request) -> Self::Future { - self.0.call(req) - } -} - -/// A Smithy middleware that uses dynamic dispatch. -/// -/// This type allows you to pay a small runtime cost to avoid having to name the exact middleware -/// you're using anywhere you want to hold a [`Client`]. Specifically, this will use `Box` to -/// enable dynamic dispatch for every request that goes through the middleware, which increases -/// memory pressure and suffers an additional vtable indirection for each request, but is unlikely -/// to matter in all but the highest-performance settings. -#[non_exhaustive] -pub struct DynMiddleware( - ArcCloneLayer< - aws_smithy_http_tower::dispatch::DispatchService, - aws_smithy_http::operation::Request, - aws_smithy_http::operation::Response, - aws_smithy_http_tower::SendOperationError, - >, -); - -impl Clone for DynMiddleware { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -impl fmt::Debug for DynMiddleware { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_struct("DynMiddleware").finish() - } -} - -impl DynMiddleware { - /// Construct a new dynamically-dispatched Smithy middleware. - pub fn new + Send + Sync + 'static>(middleware: M) -> Self { - Self(ArcCloneLayer::new(middleware)) - } -} - -impl Layer> for DynMiddleware { - type Service = BoxCloneService< - aws_smithy_http::operation::Request, - aws_smithy_http::operation::Response, - aws_smithy_http_tower::SendOperationError, - >; - - fn layer(&self, inner: aws_smithy_http_tower::dispatch::DispatchService) -> Self::Service { - self.0.layer(inner) - } -} diff --git a/rust-runtime/aws-smithy-client/src/erase/boxclone.rs b/rust-runtime/aws-smithy-client/src/erase/boxclone.rs deleted file mode 100644 index 1604d448b9..0000000000 --- a/rust-runtime/aws-smithy-client/src/erase/boxclone.rs +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -// This is an adaptation of tower::util::{BoxLayer, BoxService} that includes Clone and doesn't -// include Sync. - -use std::fmt; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; - -use tower::layer::{layer_fn, Layer}; -use tower::Service; - -pub(super) struct ArcCloneLayer { - inner: Arc> + Send + Sync>, -} - -impl Clone for ArcCloneLayer { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - } - } -} - -impl ArcCloneLayer { - /// Create a new [`BoxLayer`]. - pub(crate) fn new(inner_layer: L) -> Self - where - L: Layer + Send + Sync + 'static, - L::Service: Service + Clone + Send + Sync + 'static, - >::Future: Send + 'static, - { - let layer = layer_fn(move |inner: In| { - let out = inner_layer.layer(inner); - BoxCloneService::new(out) - }); - - Self { - inner: Arc::new(layer), - } - } -} - -impl Layer for ArcCloneLayer { - type Service = BoxCloneService; - - fn layer(&self, inner: In) -> Self::Service { - self.inner.layer(inner) - } -} - -impl fmt::Debug for ArcCloneLayer { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_struct("ArcCloneLayer").finish() - } -} - -trait CloneService: Service { - fn clone_box( - &self, - ) -> Box< - dyn CloneService - + Send - + Sync - + 'static, - >; -} - -impl CloneService for T -where - T: Service + Clone + Send + Sync + 'static, -{ - fn clone_box( - &self, - ) -> Box< - dyn CloneService< - Request, - Response = Self::Response, - Error = Self::Error, - Future = Self::Future, - > - + 'static - + Send - + Sync, - > { - Box::new(self.clone()) - } -} - -pub type BoxFuture = Pin> + Send>>; -pub struct BoxCloneService { - inner: Box< - dyn CloneService> - + Send - + Sync - + 'static, - >, -} - -#[derive(Debug, Clone)] -struct Boxed { - inner: S, -} - -impl BoxCloneService { - #[allow(missing_docs)] - pub fn new(inner: S) -> Self - where - S: Service + Send + Sync + 'static + Clone, - S::Future: Send + 'static, - { - let inner = Box::new(Boxed { inner }); - BoxCloneService { inner } - } -} - -impl Clone for BoxCloneService -where - T: 'static, - U: 'static, - E: 'static, -{ - fn clone(&self) -> Self { - Self { - inner: self.inner.clone_box(), - } - } -} - -impl Service for BoxCloneService { - type Response = U; - type Error = E; - type Future = BoxFuture; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, request: T) -> BoxFuture { - self.inner.call(request) - } -} - -impl fmt::Debug for BoxCloneService { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_struct("BoxCloneService").finish() - } -} - -impl Service for Boxed -where - S: Service + 'static, - S::Future: Send + 'static, -{ - type Response = S::Response; - type Error = S::Error; - - #[allow(clippy::type_complexity)] - type Future = Pin> + Send>>; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, request: Request) -> Self::Future { - Box::pin(self.inner.call(request)) - } -} diff --git a/rust-runtime/aws-smithy-client/src/http_connector.rs b/rust-runtime/aws-smithy-client/src/http_connector.rs deleted file mode 100644 index eaac5805c6..0000000000 --- a/rust-runtime/aws-smithy-client/src/http_connector.rs +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Default connectors based on what TLS features are active. Also contains HTTP-related abstractions -//! that enable passing HTTP connectors around. - -use crate::erase::DynConnector; -use aws_smithy_async::rt::sleep::SharedAsyncSleep; -use aws_smithy_types::config_bag::{Storable, StoreReplace}; -use aws_smithy_types::timeout::TimeoutConfig; -use std::time::Duration; -use std::{fmt::Debug, sync::Arc}; - -/// Type alias for a Connector factory function. -pub type MakeConnectorFn = - dyn Fn(&ConnectorSettings, Option) -> Option + Send + Sync; - -/// Enum for describing the two "kinds" of HTTP Connectors in smithy-rs. -#[derive(Clone)] -pub enum HttpConnector { - /// A `DynConnector` to be used for all requests. - Prebuilt(Option), - /// A factory function that will be used to create new `DynConnector`s whenever one is needed. - ConnectorFn(Arc), -} - -impl Debug for HttpConnector { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Prebuilt(Some(connector)) => { - write!(f, "Prebuilt({:?})", connector) - } - Self::Prebuilt(None) => { - write!(f, "Prebuilt(None)") - } - Self::ConnectorFn(_) => { - write!(f, "ConnectorFn()") - } - } - } -} - -impl Storable for HttpConnector { - type Storer = StoreReplace; -} - -impl HttpConnector { - /// If `HttpConnector` is `Prebuilt`, return a clone of that connector. - /// If `HttpConnector` is `ConnectorFn`, generate a new connector from settings and return it. - pub fn connector( - &self, - settings: &ConnectorSettings, - sleep: Option, - ) -> Option { - match self { - HttpConnector::Prebuilt(conn) => conn.clone(), - HttpConnector::ConnectorFn(func) => func(settings, sleep), - } - } -} - -/// Builder for [`ConnectorSettings`]. -#[non_exhaustive] -#[derive(Default, Debug)] -pub struct ConnectorSettingsBuilder { - connect_timeout: Option, - read_timeout: Option, -} - -impl ConnectorSettingsBuilder { - /// Creates a new builder. - pub fn new() -> Self { - Default::default() - } - - /// Sets the connect timeout that should be used. - /// - /// The connect timeout is a limit on the amount of time it takes to initiate a socket connection. - pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self { - self.connect_timeout = Some(connect_timeout); - self - } - - /// Sets the connect timeout that should be used. - /// - /// The connect timeout is a limit on the amount of time it takes to initiate a socket connection. - pub fn set_connect_timeout(&mut self, connect_timeout: Option) -> &mut Self { - self.connect_timeout = connect_timeout; - self - } - - /// Sets the read timeout that should be used. - /// - /// The read timeout is the limit on the amount of time it takes to read the first byte of a response - /// from the time the request is initiated. - pub fn read_timeout(mut self, read_timeout: Duration) -> Self { - self.read_timeout = Some(read_timeout); - self - } - - /// Sets the read timeout that should be used. - /// - /// The read timeout is the limit on the amount of time it takes to read the first byte of a response - /// from the time the request is initiated. - pub fn set_read_timeout(&mut self, read_timeout: Option) -> &mut Self { - self.read_timeout = read_timeout; - self - } - - /// Builds the [`ConnectorSettings`]. - pub fn build(self) -> ConnectorSettings { - ConnectorSettings { - connect_timeout: self.connect_timeout, - read_timeout: self.read_timeout, - } - } -} - -/// Settings for HTTP Connectors -#[non_exhaustive] -#[derive(Clone, Default, Debug)] -pub struct ConnectorSettings { - connect_timeout: Option, - read_timeout: Option, -} - -impl ConnectorSettings { - /// Returns a builder for `ConnectorSettings`. - pub fn builder() -> ConnectorSettingsBuilder { - Default::default() - } - - /// Returns the connect timeout that should be used. - /// - /// The connect timeout is a limit on the amount of time it takes to initiate a socket connection. - pub fn connect_timeout(&self) -> Option { - self.connect_timeout - } - - /// Returns the read timeout that should be used. - /// - /// The read timeout is the limit on the amount of time it takes to read the first byte of a response - /// from the time the request is initiated. - pub fn read_timeout(&self) -> Option { - self.read_timeout - } - - // This function may be removed/refactored in the future if other non-timeout - // properties are added to the `ConnectorSettings` struct. - #[doc(hidden)] - pub fn from_timeout_config(timeout_config: &TimeoutConfig) -> Self { - Self { - connect_timeout: timeout_config.connect_timeout(), - read_timeout: timeout_config.read_timeout(), - } - } -} diff --git a/rust-runtime/aws-smithy-client/src/hyper_ext.rs b/rust-runtime/aws-smithy-client/src/hyper_ext.rs deleted file mode 100644 index c5906fdb34..0000000000 --- a/rust-runtime/aws-smithy-client/src/hyper_ext.rs +++ /dev/null @@ -1,762 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Implementation of [`SmithyConnector`](crate::bounds::SmithyConnector) for Hyper -//! -//! The module provides [`Adapter`] which enables using a [`hyper::Client`] as the connector for a Smithy -//! [`Client`](crate::Client). For most use cases, this shouldn't need to be used directly, but it is -//! available as an option. -//! -//! # Examples -//! -//! ### Construct a Smithy Client with Hyper and Rustls -//! -//! In the basic case, customers should not need to use this module. A default implementation of Hyper -//! with `rustls` will be constructed during client creation. However, if you are creating a Smithy -//! [`Client`](crate::Client), directly, use the `dyn_https_https()` method to match that default behavior: -//! -#![cfg_attr( - not(all(feature = "rustls", feature = "client-hyper")), - doc = "```no_run,ignore" -)] -#![cfg_attr(all(feature = "rustls", feature = "client-hyper"), doc = "```no_run")] -//! use aws_smithy_client::Client; -//! -//! let client = Client::builder() -//! .dyn_https_connector(Default::default()) -//! .middleware( -//! // Replace this with your middleware type -//! tower::layer::util::Identity::default() -//! ) -//! .build(); -//! ``` -//! -//! ### Use a Hyper client that uses WebPKI roots -//! -//! A use case for where you may want to use the [`Adapter`] is when settings Hyper client settings -//! that aren't otherwise exposed by the `Client` builder interface. -//! -#![cfg_attr( - not(all(feature = "rustls", feature = "client-hyper")), - doc = "```no_run,ignore" -)] -#![cfg_attr( - all( - feature = "rustls", - feature = "client-hyper", - feature = "hyper-webpki-doctest-only" - ), - doc = "```no_run" -)] -//! use std::time::Duration; -//! use aws_smithy_client::{Client, conns, hyper_ext}; -//! use aws_smithy_client::erase::DynConnector; -//! use aws_smithy_client::http_connector::ConnectorSettings; -//! -//! let https_connector = hyper_rustls::HttpsConnectorBuilder::new() -//! .with_webpki_roots() -//! .https_only() -//! .enable_http1() -//! .enable_http2() -//! .build(); -//! let smithy_connector = hyper_ext::Adapter::builder() -//! // Optionally set things like timeouts as well -//! .connector_settings( -//! ConnectorSettings::builder() -//! .connect_timeout(Duration::from_secs(5)) -//! .build() -//! ) -//! .build(https_connector); -//! -//! // Once you have a Smithy connector, use it to construct a Smithy client: -//! let client = Client::builder() -//! .connector(smithy_connector) -//! .middleware(tower::layer::util::Identity::default()) -//! .build(); -//! ``` - -use crate::http_connector::ConnectorSettings; -use crate::hyper_ext::timeout_middleware::{ConnectTimeout, HttpReadTimeout, HttpTimeoutError}; -use crate::never::stream::EmptyStream; -use aws_smithy_async::future::timeout::TimedOutError; -use aws_smithy_async::rt::sleep::{default_async_sleep, SharedAsyncSleep}; -use aws_smithy_http::body::SdkBody; - -use aws_smithy_http::result::ConnectorError; -use aws_smithy_types::error::display::DisplayErrorContext; -use aws_smithy_types::retry::ErrorKind; -use http::{Extensions, Uri}; -use hyper::client::connect::{ - capture_connection, CaptureConnection, Connected, Connection, HttpInfo, -}; - -use h2::Reason; -use std::error::Error; -use std::fmt::Debug; - -use crate::erase::boxclone::BoxFuture; -use aws_smithy_http::connection::{CaptureSmithyConnection, ConnectionMetadata}; -use tokio::io::{AsyncRead, AsyncWrite}; -use tower::{BoxError, Service}; - -/// Adapter from a [`hyper::Client`](hyper::Client) to a connector usable by a Smithy [`Client`](crate::Client). -/// -/// This adapter also enables TCP `CONNECT` and HTTP `READ` timeouts via [`Adapter::builder`]. For examples -/// see [the module documentation](crate::hyper_ext). -#[derive(Clone, Debug)] -pub struct Adapter { - client: HttpReadTimeout, SdkBody>>, -} - -/// Extract a smithy connection from a hyper CaptureConnection -fn extract_smithy_connection(capture_conn: &CaptureConnection) -> Option { - let capture_conn = capture_conn.clone(); - if let Some(conn) = capture_conn.clone().connection_metadata().as_ref() { - let mut extensions = Extensions::new(); - conn.get_extras(&mut extensions); - let http_info = extensions.get::(); - let smithy_connection = ConnectionMetadata::new( - conn.is_proxied(), - http_info.map(|info| info.remote_addr()), - move || match capture_conn.connection_metadata().as_ref() { - Some(conn) => conn.poison(), - None => tracing::trace!("no connection existed to poison"), - }, - ); - Some(smithy_connection) - } else { - None - } -} - -impl Service> for Adapter -where - C: Clone + Send + Sync + 'static, - C: Service, - C::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static, - C::Future: Unpin + Send + 'static, - C::Error: Into, -{ - type Response = http::Response; - type Error = ConnectorError; - - type Future = BoxFuture; - - fn poll_ready( - &mut self, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.client.poll_ready(cx).map_err(downcast_error) - } - - fn call(&mut self, mut req: http::Request) -> Self::Future { - let capture_connection = capture_connection(&mut req); - if let Some(capture_smithy_connection) = req.extensions().get::() { - capture_smithy_connection - .set_connection_retriever(move || extract_smithy_connection(&capture_connection)); - } - let fut = self.client.call(req); - Box::pin(async move { Ok(fut.await.map_err(downcast_error)?.map(SdkBody::from)) }) - } -} - -impl Adapter<()> { - /// Builder for a Hyper Adapter - /// - /// Generally, end users should not need to construct an [`Adapter`] manually: a hyper adapter - /// will be constructed automatically during client creation. - pub fn builder() -> Builder { - Builder::default() - } -} - -/// Downcast errors coming out of hyper into an appropriate `ConnectorError` -fn downcast_error(err: BoxError) -> ConnectorError { - // is a `TimedOutError` (from aws_smithy_async::timeout) in the chain? if it is, this is a timeout - if find_source::(err.as_ref()).is_some() { - return ConnectorError::timeout(err); - } - // is the top of chain error actually already a `ConnectorError`? return that directly - let err = match err.downcast::() { - Ok(connector_error) => return *connector_error, - Err(box_error) => box_error, - }; - // generally, the top of chain will probably be a hyper error. Go through a set of hyper specific - // error classifications - let err = match err.downcast::() { - Ok(hyper_error) => return to_connector_error(*hyper_error), - Err(box_error) => box_error, - }; - - // otherwise, we have no idea! - ConnectorError::other(err, None) -} - -/// Convert a [`hyper::Error`] into a [`ConnectorError`] -fn to_connector_error(err: hyper::Error) -> ConnectorError { - if err.is_timeout() || find_source::(&err).is_some() { - return ConnectorError::timeout(err.into()); - } - if err.is_user() { - return ConnectorError::user(err.into()); - } - if err.is_closed() || err.is_canceled() || find_source::(&err).is_some() { - return ConnectorError::io(err.into()); - } - // We sometimes receive this from S3: hyper::Error(IncompleteMessage) - if err.is_incomplete_message() { - return ConnectorError::other(err.into(), Some(ErrorKind::TransientError)); - } - if let Some(h2_err) = find_source::(&err) { - if h2_err.is_go_away() - || (h2_err.is_reset() && h2_err.reason() == Some(Reason::REFUSED_STREAM)) - { - return ConnectorError::io(err.into()); - } - } - - tracing::warn!(err = %DisplayErrorContext(&err), "unrecognized error from Hyper. If this error should be retried, please file an issue."); - ConnectorError::other(err.into(), None) -} - -fn find_source<'a, E: Error + 'static>(err: &'a (dyn Error + 'static)) -> Option<&'a E> { - let mut next = Some(err); - while let Some(err) = next { - if let Some(matching_err) = err.downcast_ref::() { - return Some(matching_err); - } - next = err.source(); - } - None -} - -/// Builder for [`hyper_ext::Adapter`](Adapter) -/// -/// Unlike a Smithy client, the [`Service`] inside a [`hyper_ext::Adapter`](Adapter) is actually a service that -/// accepts a `Uri` and returns a TCP stream. One default implementation of this is provided, -/// that encrypts the stream with `rustls`. -/// -/// # Examples -/// Construct a HyperAdapter with the default HTTP implementation (rustls). This can be useful when you want to share a Hyper connector -/// between multiple Smithy clients. -/// -#[cfg_attr( - not(all(feature = "rustls", feature = "client-hyper")), - doc = "```no_run,ignore" -)] -#[cfg_attr(all(feature = "rustls", feature = "client-hyper"), doc = "```no_run")] -/// use tower::layer::util::Identity; -/// use aws_smithy_client::{conns, hyper_ext}; -/// use aws_smithy_client::erase::DynConnector; -/// -/// let hyper_connector = hyper_ext::Adapter::builder().build(conns::https()); -/// // this client can then be used when constructing a Smithy Client -/// // Replace `Identity` with your middleware implementation: -/// let client = aws_smithy_client::Client::::new(DynConnector::new(hyper_connector)); -/// ``` -#[derive(Default, Debug)] -pub struct Builder { - connector_settings: Option, - sleep_impl: Option, - client_builder: Option, -} - -impl Builder { - /// Create a HyperAdapter from this builder and a given connector - pub fn build(self, connector: C) -> Adapter - where - C: Clone + Send + Sync + 'static, - C: Service, - C::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static, - C::Future: Unpin + Send + 'static, - C::Error: Into, - { - let client_builder = self.client_builder.unwrap_or_default(); - let sleep_impl = self.sleep_impl.or_else(default_async_sleep); - let (connect_timeout, read_timeout) = self - .connector_settings - .map(|c| (c.connect_timeout(), c.read_timeout())) - .unwrap_or((None, None)); - - // if we are using Hyper, Tokio must already be enabled so we can fallback to Tokio. - let connector = match connect_timeout { - Some(duration) => ConnectTimeout::new( - connector, - sleep_impl - .clone() - .expect("a sleep impl must be provided in order to have a connect timeout"), - duration, - ), - None => ConnectTimeout::no_timeout(connector), - }; - let base = client_builder.build(connector); - let read_timeout = match read_timeout { - Some(duration) => HttpReadTimeout::new( - base, - sleep_impl.expect("a sleep impl must be provided in order to have a read timeout"), - duration, - ), - None => HttpReadTimeout::no_timeout(base), - }; - Adapter { - client: read_timeout, - } - } - - /// Set the async sleep implementation used for timeouts - /// - /// Calling this is only necessary for testing or to use something other than - /// [`default_async_sleep`]. - pub fn sleep_impl(mut self, sleep_impl: SharedAsyncSleep) -> Self { - self.sleep_impl = Some(sleep_impl); - self - } - - /// Set the async sleep implementation used for timeouts - /// - /// Calling this is only necessary for testing or to use something other than - /// [`default_async_sleep`]. - pub fn set_sleep_impl(&mut self, sleep_impl: Option) -> &mut Self { - self.sleep_impl = sleep_impl; - self - } - - /// Configure the HTTP settings for the `HyperAdapter` - pub fn connector_settings(mut self, connector_settings: ConnectorSettings) -> Self { - self.connector_settings = Some(connector_settings); - self - } - - /// Configure the HTTP settings for the `HyperAdapter` - pub fn set_connector_settings( - &mut self, - connector_settings: Option, - ) -> &mut Self { - self.connector_settings = connector_settings; - self - } - - /// Override the Hyper client [`Builder`](hyper::client::Builder) used to construct this client. - /// - /// This enables changing settings like forcing HTTP2 and modifying other default client behavior. - pub fn hyper_builder(mut self, hyper_builder: hyper::client::Builder) -> Self { - self.client_builder = Some(hyper_builder); - self - } - - /// Override the Hyper client [`Builder`](hyper::client::Builder) used to construct this client. - /// - /// This enables changing settings like forcing HTTP2 and modifying other default client behavior. - pub fn set_hyper_builder( - &mut self, - hyper_builder: Option, - ) -> &mut Self { - self.client_builder = hyper_builder; - self - } -} - -mod timeout_middleware { - use std::error::Error; - use std::fmt::Formatter; - use std::future::Future; - use std::pin::Pin; - use std::task::{Context, Poll}; - use std::time::Duration; - - use http::Uri; - use pin_project_lite::pin_project; - use tower::BoxError; - - use aws_smithy_async::future::timeout::{TimedOutError, Timeout}; - use aws_smithy_async::rt::sleep::Sleep; - use aws_smithy_async::rt::sleep::{AsyncSleep, SharedAsyncSleep}; - - #[derive(Debug)] - pub(crate) struct HttpTimeoutError { - kind: &'static str, - duration: Duration, - } - - impl std::fmt::Display for HttpTimeoutError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{} timeout occurred after {:?}", - self.kind, self.duration - ) - } - } - - impl Error for HttpTimeoutError { - // We implement the `source` function as returning a `TimedOutError` because when `downcast_error` - // or `find_source` is called with an `HttpTimeoutError` (or another error wrapping an `HttpTimeoutError`) - // this method will be checked to determine if it's a timeout-related error. - fn source(&self) -> Option<&(dyn Error + 'static)> { - Some(&TimedOutError) - } - } - - /// Timeout wrapper that will timeout on the initial TCP connection - /// - /// # Stability - /// This interface is unstable. - #[derive(Clone, Debug)] - pub(super) struct ConnectTimeout { - inner: I, - timeout: Option<(SharedAsyncSleep, Duration)>, - } - - impl ConnectTimeout { - /// Create a new `ConnectTimeout` around `inner`. - /// - /// Typically, `I` will implement [`hyper::client::connect::Connect`]. - pub(crate) fn new(inner: I, sleep: SharedAsyncSleep, timeout: Duration) -> Self { - Self { - inner, - timeout: Some((sleep, timeout)), - } - } - - pub(crate) fn no_timeout(inner: I) -> Self { - Self { - inner, - timeout: None, - } - } - } - - #[derive(Clone, Debug)] - pub(crate) struct HttpReadTimeout { - inner: I, - timeout: Option<(SharedAsyncSleep, Duration)>, - } - - impl HttpReadTimeout { - /// Create a new `HttpReadTimeout` around `inner`. - /// - /// Typically, `I` will implement [`tower::Service>`]. - pub(crate) fn new(inner: I, sleep: SharedAsyncSleep, timeout: Duration) -> Self { - Self { - inner, - timeout: Some((sleep, timeout)), - } - } - - pub(crate) fn no_timeout(inner: I) -> Self { - Self { - inner, - timeout: None, - } - } - } - - pin_project! { - /// Timeout future for Tower services - /// - /// Timeout future to handle timing out, mapping errors, and the possibility of not timing out - /// without incurring an additional allocation for each timeout layer. - #[project = MaybeTimeoutFutureProj] - pub enum MaybeTimeoutFuture { - Timeout { - #[pin] - timeout: Timeout, - error_type: &'static str, - duration: Duration, - }, - NoTimeout { - #[pin] - future: F - } - } - } - - impl Future for MaybeTimeoutFuture - where - F: Future>, - E: Into, - { - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let (timeout_future, kind, &mut duration) = match self.project() { - MaybeTimeoutFutureProj::NoTimeout { future } => { - return future.poll(cx).map_err(|err| err.into()); - } - MaybeTimeoutFutureProj::Timeout { - timeout, - error_type, - duration, - } => (timeout, error_type, duration), - }; - match timeout_future.poll(cx) { - Poll::Ready(Ok(response)) => Poll::Ready(response.map_err(|err| err.into())), - Poll::Ready(Err(_timeout)) => { - Poll::Ready(Err(HttpTimeoutError { kind, duration }.into())) - } - Poll::Pending => Poll::Pending, - } - } - } - - impl tower::Service for ConnectTimeout - where - I: tower::Service, - I::Error: Into, - { - type Response = I::Response; - type Error = BoxError; - type Future = MaybeTimeoutFuture; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx).map_err(|err| err.into()) - } - - fn call(&mut self, req: Uri) -> Self::Future { - match &self.timeout { - Some((sleep, duration)) => { - let sleep = sleep.sleep(*duration); - MaybeTimeoutFuture::Timeout { - timeout: Timeout::new(self.inner.call(req), sleep), - error_type: "HTTP connect", - duration: *duration, - } - } - None => MaybeTimeoutFuture::NoTimeout { - future: self.inner.call(req), - }, - } - } - } - - impl tower::Service> for HttpReadTimeout - where - I: tower::Service, Error = hyper::Error>, - { - type Response = I::Response; - type Error = BoxError; - type Future = MaybeTimeoutFuture; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx).map_err(|err| err.into()) - } - - fn call(&mut self, req: http::Request) -> Self::Future { - match &self.timeout { - Some((sleep, duration)) => { - let sleep = sleep.sleep(*duration); - MaybeTimeoutFuture::Timeout { - timeout: Timeout::new(self.inner.call(req), sleep), - error_type: "HTTP read", - duration: *duration, - } - } - None => MaybeTimeoutFuture::NoTimeout { - future: self.inner.call(req), - }, - } - } - } - - #[cfg(test)] - mod test { - use crate::http_connector::ConnectorSettings; - use crate::hyper_ext::Adapter; - use crate::never::{NeverConnected, NeverReplies}; - use aws_smithy_async::assert_elapsed; - use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep}; - use aws_smithy_http::body::SdkBody; - use aws_smithy_types::error::display::DisplayErrorContext; - use aws_smithy_types::timeout::TimeoutConfig; - use std::time::Duration; - use tower::Service; - - #[allow(unused)] - fn connect_timeout_is_correct() { - is_send_sync::>(); - } - - #[allow(unused)] - fn is_send_sync() {} - - #[tokio::test] - async fn http_connect_timeout_works() { - let inner = NeverConnected::new(); - let connector_settings = ConnectorSettings::from_timeout_config( - &TimeoutConfig::builder() - .connect_timeout(Duration::from_secs(1)) - .build(), - ); - let mut hyper = Adapter::builder() - .connector_settings(connector_settings) - .sleep_impl(SharedAsyncSleep::new(TokioSleep::new())) - .build(inner); - let now = tokio::time::Instant::now(); - tokio::time::pause(); - let resp = hyper - .call( - http::Request::builder() - .uri("http://foo.com") - .body(SdkBody::empty()) - .unwrap(), - ) - .await - .unwrap_err(); - assert!( - resp.is_timeout(), - "expected resp.is_timeout() to be true but it was false, resp == {:?}", - resp - ); - let message = DisplayErrorContext(&resp).to_string(); - let expected = - "timeout: error trying to connect: HTTP connect timeout occurred after 1s"; - assert!( - message.contains(expected), - "expected '{message}' to contain '{expected}'" - ); - assert_elapsed!(now, Duration::from_secs(1)); - } - - #[tokio::test] - async fn http_read_timeout_works() { - let inner = NeverReplies::new(); - let connector_settings = ConnectorSettings::from_timeout_config( - &TimeoutConfig::builder() - .connect_timeout(Duration::from_secs(1)) - .read_timeout(Duration::from_secs(2)) - .build(), - ); - let mut hyper = Adapter::builder() - .connector_settings(connector_settings) - .sleep_impl(SharedAsyncSleep::new(TokioSleep::new())) - .build(inner); - let now = tokio::time::Instant::now(); - tokio::time::pause(); - let resp = hyper - .call( - http::Request::builder() - .uri("http://foo.com") - .body(SdkBody::empty()) - .unwrap(), - ) - .await - .unwrap_err(); - assert!( - resp.is_timeout(), - "expected resp.is_timeout() to be true but it was false, resp == {:?}", - resp - ); - let message = format!("{}", DisplayErrorContext(&resp)); - let expected = "timeout: HTTP read timeout occurred after 2s"; - assert!( - message.contains(expected), - "expected '{message}' to contain '{expected}'" - ); - assert_elapsed!(now, Duration::from_secs(2)); - } - } -} - -/// Make `EmptyStream` compatible with Hyper -impl Connection for EmptyStream { - fn connected(&self) -> Connected { - Connected::new() - } -} - -#[cfg(test)] -mod test { - use crate::hyper_ext::Adapter; - use aws_smithy_http::body::SdkBody; - use http::Uri; - use hyper::client::connect::{Connected, Connection}; - use std::io::{Error, ErrorKind}; - use std::pin::Pin; - use std::task::{Context, Poll}; - use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; - use tower::BoxError; - - #[tokio::test] - async fn hyper_io_error() { - let connector = TestConnection { - inner: HangupStream, - }; - let mut adapter = Adapter::builder().build(connector); - use tower::Service; - let err = adapter - .call( - http::Request::builder() - .uri("http://amazon.com") - .body(SdkBody::empty()) - .unwrap(), - ) - .await - .expect_err("socket hangup"); - assert!(err.is_io(), "{:?}", err); - } - - // ---- machinery to make a Hyper connector that responds with an IO Error - #[derive(Clone)] - struct HangupStream; - - impl Connection for HangupStream { - fn connected(&self) -> Connected { - Connected::new() - } - } - - impl AsyncRead for HangupStream { - fn poll_read( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - _buf: &mut ReadBuf<'_>, - ) -> Poll> { - Poll::Ready(Err(Error::new( - ErrorKind::ConnectionReset, - "connection reset", - ))) - } - } - - impl AsyncWrite for HangupStream { - fn poll_write( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - _buf: &[u8], - ) -> Poll> { - Poll::Pending - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Pending - } - - fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Pending - } - } - - #[derive(Clone)] - struct TestConnection { - inner: T, - } - - impl tower::Service for TestConnection - where - T: Clone + Connection, - { - type Response = T; - type Error = BoxError; - type Future = std::future::Ready>; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, _req: Uri) -> Self::Future { - std::future::ready(Ok(self.inner.clone())) - } - } -} diff --git a/rust-runtime/aws-smithy-client/src/lib.rs b/rust-runtime/aws-smithy-client/src/lib.rs index 82ddc4cacb..32e46abdb4 100644 --- a/rust-runtime/aws-smithy-client/src/lib.rs +++ b/rust-runtime/aws-smithy-client/src/lib.rs @@ -3,260 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -//! A Hyper-based Smithy service client. -//! -//! | Feature | Description | -//! |-------------------|-------------| -//! | `event-stream` | Provides Sender/Receiver implementations for Event Stream codegen. | -//! | `rt-tokio` | Run async code with the `tokio` runtime | -//! | `test-util` | Include various testing utils | -//! | `rustls` | Use `rustls` as the HTTP client's TLS implementation | -//! | `client-hyper` | Use `hyper` to handle HTTP requests | +//! This crate is no longer used by smithy-rs and is deprecated. -#![allow(clippy::derive_partial_eq_without_eq)] #![warn( missing_docs, rustdoc::missing_crate_level_docs, unreachable_pub, rust_2018_idioms )] - -pub mod bounds; -pub mod erase; -pub mod http_connector; -pub mod never; -mod poison; -pub mod retry; -pub mod timeout; - -// https://github.com/rust-lang/rust/issues/72081 -#[allow(rustdoc::private_doc_tests)] -mod builder; -pub use builder::Builder; - -#[cfg(all(feature = "test-util", feature = "client-hyper"))] -pub mod dvr; -#[cfg(feature = "test-util")] -pub mod test_connection; - -#[cfg(feature = "client-hyper")] -pub mod conns; -#[cfg(feature = "client-hyper")] -pub mod hyper_ext; - -// The types in this module are only used to write the bounds in [`Client::check`]. Customers will -// not need them. But the module and its types must be public so that we can call `check` from -// doc-tests. -#[doc(hidden)] -pub mod static_tests; - -use crate::poison::PoisonLayer; -use aws_smithy_async::rt::sleep::SharedAsyncSleep; - -use aws_smithy_http::operation::Operation; -use aws_smithy_http::response::ParseHttpResponse; -pub use aws_smithy_http::result::{SdkError, SdkSuccess}; -use aws_smithy_http::retry::ClassifyRetry; -use aws_smithy_http_tower::dispatch::DispatchLayer; -use aws_smithy_http_tower::parse_response::ParseResponseLayer; -use aws_smithy_types::error::display::DisplayErrorContext; -use aws_smithy_types::retry::{ProvideErrorKind, ReconnectMode}; -use aws_smithy_types::timeout::OperationTimeoutConfig; -use timeout::ClientTimeoutParams; -pub use timeout::TimeoutLayer; -use tower::{Service, ServiceBuilder, ServiceExt}; -use tracing::{debug_span, field, Instrument}; - -/// Smithy service client. -/// -/// The service client is customizable in a number of ways (see [`Builder`]), but most customers -/// can stick with the standard constructor provided by [`Client::new`]. It takes only a single -/// argument, which is the middleware that fills out the [`http::Request`] for each higher-level -/// operation so that it can ultimately be sent to the remote host. The middleware is responsible -/// for filling in any request parameters that aren't specified by the Smithy protocol definition, -/// such as those used for routing (like the URL), authentication, and authorization. -/// -/// The middleware takes the form of a [`tower::Layer`] that wraps the actual connection for each -/// request. The [`tower::Service`](Service) that the middleware produces must accept requests of the type -/// [`aws_smithy_http::operation::Request`] and return responses of the type -/// [`http::Response`], most likely by modifying the provided request in place, passing it -/// to the inner service, and then ultimately returning the inner service's response. -/// -/// With the `hyper` feature enabled, you can construct a `Client` directly from a -/// `hyper::Client` using `hyper_ext::Adapter::builder`. You can also enable the `rustls` -/// feature to construct a Client against a standard HTTPS endpoint using `Builder::rustls_connector`. -#[derive(Debug)] -pub struct Client< - Connector = erase::DynConnector, - Middleware = erase::DynMiddleware, - RetryPolicy = retry::Standard, -> { - connector: Connector, - middleware: Middleware, - retry_policy: RetryPolicy, - reconnect_mode: ReconnectMode, - operation_timeout_config: OperationTimeoutConfig, - sleep_impl: Option, -} - -impl Client<(), (), ()> { - /// Returns a client builder - pub fn builder() -> Builder { - Builder::new() - } -} - -// Quick-create for people who just want "the default". -impl Client -where - M: Default, -{ - /// Create a Smithy client from the given `connector`, a middleware default, the - /// [standard retry policy](retry::Standard), and the - /// [`default_async_sleep`](aws_smithy_async::rt::sleep::default_async_sleep) sleep implementation. - pub fn new(connector: C) -> Self { - Builder::new() - .connector(connector) - .middleware(M::default()) - .build() - } -} - -fn check_send_sync(t: T) -> T { - t -} - -impl Client -where - C: bounds::SmithyConnector, - M: bounds::SmithyMiddleware, - R: retry::NewRequestPolicy, -{ - /// Dispatch this request to the network - /// - /// For ergonomics, this does not include the raw response for successful responses. To - /// access the raw response use `call_raw`. - pub async fn call(&self, op: Operation) -> Result> - where - O: Send + Sync, - E: std::error::Error + Send + Sync + 'static, - Retry: Send + Sync, - R::Policy: bounds::SmithyRetryPolicy, - Retry: ClassifyRetry, SdkError>, - bounds::Parsed<>::Service, O, Retry>: - Service, Response = SdkSuccess, Error = SdkError> + Clone, - { - self.call_raw(op).await.map(|res| res.parsed) - } - - /// Dispatch this request to the network - /// - /// The returned result contains the raw HTTP response which can be useful for debugging or - /// implementing unsupported features. - pub async fn call_raw( - &self, - op: Operation, - ) -> Result, SdkError> - where - O: Send + Sync, - E: std::error::Error + Send + Sync + 'static, - Retry: Send + Sync, - R::Policy: bounds::SmithyRetryPolicy, - Retry: ClassifyRetry, SdkError>, - // This bound is not _technically_ inferred by all the previous bounds, but in practice it - // is because _we_ know that there is only implementation of Service for Parsed - // (ParsedResponseService), and it will apply as long as the bounds on C, M, and R hold, - // and will produce (as expected) Response = SdkSuccess, Error = SdkError. But Rust - // doesn't know that -- there _could_ theoretically be other implementations of Service for - // Parsed that don't return those same types. So, we must give the bound. - bounds::Parsed<>::Service, O, Retry>: - Service, Response = SdkSuccess, Error = SdkError> + Clone, - { - let connector = self.connector.clone(); - - let timeout_params = - ClientTimeoutParams::new(&self.operation_timeout_config, self.sleep_impl.clone()); - - let svc = ServiceBuilder::new() - .layer(TimeoutLayer::new(timeout_params.operation_timeout)) - .retry( - self.retry_policy - .new_request_policy(self.sleep_impl.clone()), - ) - .layer(PoisonLayer::new(self.reconnect_mode)) - .layer(TimeoutLayer::new(timeout_params.operation_attempt_timeout)) - .layer(ParseResponseLayer::::new()) - // These layers can be considered as occurring in order. That is, first invoke the - // customer-provided middleware, then dispatch dispatch over the wire. - .layer(&self.middleware) - .layer(DispatchLayer::new()) - .service(connector); - - // send_operation records the full request-response lifecycle. - // NOTE: For operations that stream output, only the setup is captured in this span. - let span = debug_span!( - "send_operation", - operation = field::Empty, - service = field::Empty, - status = field::Empty, - message = field::Empty - ); - let (mut req, parts) = op.into_request_response(); - if let Some(metadata) = &parts.metadata { - // Clippy has a bug related to needless borrows so we need to allow them here - // https://github.com/rust-lang/rust-clippy/issues/9782 - #[allow(clippy::needless_borrow)] - { - span.record("operation", &metadata.name()); - span.record("service", &metadata.service()); - } - // This will clone two `Cow::<&'static str>::Borrow`s in the vast majority of cases - req.properties_mut().insert(metadata.clone()); - } - let op = Operation::from_parts(req, parts); - - let result = async move { check_send_sync(svc).ready().await?.call(op).await } - .instrument(span.clone()) - .await; - #[allow(clippy::needless_borrow)] - match &result { - Ok(_) => { - span.record("status", &"ok"); - } - Err(err) => { - span.record( - "status", - &match err { - SdkError::ConstructionFailure(_) => "construction_failure", - SdkError::DispatchFailure(_) => "dispatch_failure", - SdkError::ResponseError(_) => "response_error", - SdkError::ServiceError(_) => "service_error", - SdkError::TimeoutError(_) => "timeout_error", - _ => "error", - }, - ) - .record("message", &field::display(DisplayErrorContext(err))); - } - } - result - } - - /// Statically check the validity of a `Client` without a request to send. - /// - /// This will make sure that all the bounds hold that would be required by `call` and - /// `call_raw` (modulo those that relate to the specific `Operation` type). Comes in handy to - /// ensure (statically) that all the various constructors actually produce "useful" types. - #[doc(hidden)] - pub fn check(&self) - where - R::Policy: tower::retry::Policy< - static_tests::ValidTestOperation, - SdkSuccess<()>, - SdkError, - > + Clone, - { - let _ = |o: static_tests::ValidTestOperation| { - drop(self.call_raw(o)); - }; - } -} diff --git a/rust-runtime/aws-smithy-client/src/never.rs b/rust-runtime/aws-smithy-client/src/never.rs deleted file mode 100644 index b6dcc07ea2..0000000000 --- a/rust-runtime/aws-smithy-client/src/never.rs +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Test connectors that never return data - -use std::marker::PhantomData; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::task::{Context, Poll}; - -use http::Uri; -use tower::BoxError; - -use aws_smithy_async::future::never::Never; -use aws_smithy_http::body::SdkBody; -use aws_smithy_http::result::ConnectorError; - -use crate::erase::boxclone::BoxFuture; - -/// A service that will never return whatever it is you want -/// -/// Returned futures will return Pending forever -#[non_exhaustive] -#[derive(Debug)] -pub struct NeverService { - _resp: PhantomData<(Req, Resp, Err)>, - invocations: Arc, -} - -impl Clone for NeverService { - fn clone(&self) -> Self { - Self { - _resp: Default::default(), - invocations: self.invocations.clone(), - } - } -} - -impl Default for NeverService { - fn default() -> Self { - Self::new() - } -} - -impl NeverService { - /// Create a new NeverService - pub fn new() -> Self { - NeverService { - _resp: Default::default(), - invocations: Default::default(), - } - } - - /// Returns the number of invocations made to this service - pub fn num_calls(&self) -> usize { - self.invocations.load(Ordering::SeqCst) - } -} - -/// A Connector that can be use with [`Client`](crate::Client) that never returns a response. -pub type NeverConnector = - NeverService, http::Response, ConnectorError>; - -/// A service where the underlying TCP connection never connects. -pub type NeverConnected = NeverService; - -/// Streams that never return data -pub(crate) mod stream { - use std::io::Error; - use std::pin::Pin; - use std::task::{Context, Poll}; - - use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; - - /// A stream that will never return or accept any data - #[non_exhaustive] - #[derive(Debug, Default)] - pub struct EmptyStream; - - impl EmptyStream { - pub fn new() -> Self { - Self - } - } - - impl AsyncRead for EmptyStream { - fn poll_read( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - _buf: &mut ReadBuf<'_>, - ) -> Poll> { - Poll::Pending - } - } - - impl AsyncWrite for EmptyStream { - fn poll_write( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - _buf: &[u8], - ) -> Poll> { - Poll::Pending - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Pending - } - - fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Pending - } - } -} - -/// A service that will connect but never send any data -#[derive(Clone, Debug, Default)] -pub struct NeverReplies; -impl NeverReplies { - /// Create a new NeverReplies service - pub fn new() -> Self { - Self - } -} - -impl tower::Service for NeverReplies { - type Response = stream::EmptyStream; - type Error = BoxError; - type Future = std::future::Ready>; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, _req: Uri) -> Self::Future { - std::future::ready(Ok(stream::EmptyStream::new())) - } -} - -impl tower::Service for NeverService { - type Response = Resp; - type Error = Err; - type Future = BoxFuture; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, _req: Req) -> Self::Future { - self.invocations.fetch_add(1, Ordering::SeqCst); - Box::pin(async move { - Never::new().await; - unreachable!() - }) - } -} diff --git a/rust-runtime/aws-smithy-client/src/poison.rs b/rust-runtime/aws-smithy-client/src/poison.rs deleted file mode 100644 index ffbaaf8abc..0000000000 --- a/rust-runtime/aws-smithy-client/src/poison.rs +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Connection Poisoning -//! -//! The client supports behavior where on transient errors (e.g. timeouts, 503, etc.) it will ensure -//! that the offending connection is not reused. This happens to ensure that in the case where the -//! connection itself is broken (e.g. connected to a bad host) we don't reuse it for other requests. -//! -//! This relies on a series of mechanisms: -//! 1. [`CaptureSmithyConnection`] is a container which exists in the operation property bag. It is -//! inserted by this layer before the request is sent. -//! 2. The [`DispatchLayer`](aws_smithy_http_tower::dispatch::DispatchLayer) copies the field from operation extensions HTTP request extensions. -//! 3. The HTTP layer (e.g. Hyper) sets [`ConnectionMetadata`](aws_smithy_http::connection::ConnectionMetadata) -//! when it is available. -//! 4. When the response comes back, if indicated, this layer invokes -//! [`ConnectionMetadata::poison`](aws_smithy_http::connection::ConnectionMetadata::poison). -//! -//! ### Why isn't this integrated into `retry.rs`? -//! If the request has a streaming body, we won't attempt to retry because [`Operation::try_clone()`] will -//! return `None`. Therefore, we need to handle this inside of the retry loop. - -use std::future::Future; - -use aws_smithy_http::operation::Operation; -use aws_smithy_http::result::{SdkError, SdkSuccess}; -use aws_smithy_http::retry::ClassifyRetry; - -use aws_smithy_http::connection::CaptureSmithyConnection; -use aws_smithy_types::retry::{ErrorKind, ReconnectMode, RetryKind}; -use pin_project_lite::pin_project; -use std::marker::PhantomData; -use std::pin::Pin; -use std::task::{Context, Poll}; - -/// PoisonLayer that poisons connections depending on the error kind -pub(crate) struct PoisonLayer { - inner: PhantomData, - mode: ReconnectMode, -} - -impl PoisonLayer { - pub(crate) fn new(mode: ReconnectMode) -> Self { - Self { - inner: Default::default(), - mode, - } - } -} - -impl Clone for PoisonLayer { - fn clone(&self) -> Self { - Self { - inner: Default::default(), - mode: self.mode, - } - } -} - -impl tower::Layer for PoisonLayer { - type Service = PoisonService; - - fn layer(&self, inner: S) -> Self::Service { - PoisonService { - inner, - mode: self.mode, - } - } -} - -#[derive(Clone)] -pub(crate) struct PoisonService { - inner: S, - mode: ReconnectMode, -} - -impl tower::Service> for PoisonService -where - R: ClassifyRetry, SdkError>, - S: tower::Service, Response = SdkSuccess, Error = SdkError>, -{ - type Response = S::Response; - type Error = S::Error; - type Future = PoisonServiceFuture; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, mut req: Operation) -> Self::Future { - let classifier = req.retry_classifier().clone(); - let capture_smithy_connection = CaptureSmithyConnection::new(); - req.properties_mut() - .insert(capture_smithy_connection.clone()); - PoisonServiceFuture { - inner: self.inner.call(req), - conn: capture_smithy_connection, - mode: self.mode, - classifier, - } - } -} - -pin_project! { - pub struct PoisonServiceFuture { - #[pin] - inner: F, - classifier: R, - conn: CaptureSmithyConnection, - mode: ReconnectMode - } -} - -impl Future for PoisonServiceFuture -where - F: Future, SdkError>>, - R: ClassifyRetry, SdkError>, -{ - type Output = F::Output; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); - match this.inner.poll(cx) { - Poll::Ready(resp) => { - let retry_kind = this.classifier.classify_retry(resp.as_ref()); - if this.mode == &ReconnectMode::ReconnectOnTransientError - && retry_kind == RetryKind::Error(ErrorKind::TransientError) - { - if let Some(smithy_conn) = this.conn.get() { - tracing::info!("poisoning connection: {:?}", smithy_conn); - smithy_conn.poison(); - } else { - tracing::trace!("No smithy connection found! The underlying HTTP connection never set a connection."); - } - } - Poll::Ready(resp) - } - Poll::Pending => Poll::Pending, - } - } -} diff --git a/rust-runtime/aws-smithy-client/src/retry.rs b/rust-runtime/aws-smithy-client/src/retry.rs deleted file mode 100644 index 8c2fdc246c..0000000000 --- a/rust-runtime/aws-smithy-client/src/retry.rs +++ /dev/null @@ -1,614 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Retry support -//! -//! Components: -//! - [`Standard`]: Top level manager, intended to be associated with a [`Client`](crate::Client). -//! Its sole purpose in life is to create a [`RetryHandler`] for individual requests. -//! - [`RetryHandler`]: A request-scoped retry policy, backed by request-local state and shared -//! state contained within [`Standard`]. -//! - [`Config`]: Static configuration (max attempts, max backoff etc.) - -use std::future::Future; -use std::pin::Pin; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use tracing::Instrument; - -use aws_smithy_async::rt::sleep::{AsyncSleep, SharedAsyncSleep}; - -use aws_smithy_http::operation::Operation; -use aws_smithy_http::retry::ClassifyRetry; -use aws_smithy_types::retry::{ErrorKind, RetryKind}; - -use crate::{SdkError, SdkSuccess}; - -/// A policy instantiator. -/// -/// Implementors are essentially "policy factories" that can produce a new instance of a retry -/// policy mechanism for each request, which allows both shared global state _and_ per-request -/// local state. -pub trait NewRequestPolicy -where - Self::Policy: Send + Sync, -{ - /// The type of the per-request policy mechanism. - type Policy; - - /// Create a new policy mechanism instance. - fn new_request_policy(&self, sleep_impl: Option) -> Self::Policy; -} - -/// Retry Policy Configuration -/// -/// Without specific use cases, users should generally rely on the default values set -/// by [`Config::default`](Config::default). -/// -/// Currently these fields are private and no setters provided. As needed, this configuration -/// will become user-modifiable in the future. -#[derive(Clone, Debug)] -pub struct Config { - initial_retry_tokens: usize, - retry_cost: usize, - no_retry_increment: usize, - timeout_retry_cost: usize, - max_attempts: u32, - initial_backoff: Duration, - max_backoff: Duration, - base: fn() -> f64, -} - -impl Config { - /// Override `b` in the exponential backoff computation - /// - /// By default, `base` is a randomly generated value between 0 and 1. In tests, it can - /// be helpful to override this: - /// ```no_run - /// use aws_smithy_client::retry::Config; - /// let conf = Config::default().with_base(||1_f64); - /// ``` - pub fn with_base(mut self, base: fn() -> f64) -> Self { - self.base = base; - self - } - - /// Override the maximum number of attempts - /// - /// `max_attempts` must be set to a value of at least `1` (indicating that retries are disabled). - pub fn with_max_attempts(mut self, max_attempts: u32) -> Self { - self.max_attempts = max_attempts; - self - } - - /// Override the default backoff multiplier of 1 second. - /// - /// ## Example - /// - /// For a request that gets retried 3 times, when initial_backoff is 1 second: - /// - the first retry will occur after 0 to 1 seconds - /// - the second retry will occur after 0 to 2 seconds - /// - the third retry will occur after 0 to 4 seconds - /// - /// For a request that gets retried 3 times, when initial_backoff is 30 milliseconds: - /// - the first retry will occur after 0 to 30 milliseconds - /// - the second retry will occur after 0 to 60 milliseconds - /// - the third retry will occur after 0 to 120 milliseconds - pub fn with_initial_backoff(mut self, initial_backoff: Duration) -> Self { - self.initial_backoff = initial_backoff; - self - } - - /// Returns true if retry is enabled with this config - pub fn has_retry(&self) -> bool { - self.max_attempts > 1 - } -} - -impl Default for Config { - fn default() -> Self { - Self { - initial_retry_tokens: INITIAL_RETRY_TOKENS, - retry_cost: RETRY_COST, - no_retry_increment: 1, - timeout_retry_cost: 10, - max_attempts: MAX_ATTEMPTS, - max_backoff: Duration::from_secs(20), - // by default, use a random base for exponential backoff - base: fastrand::f64, - initial_backoff: Duration::from_secs(1), - } - } -} - -impl From for Config { - fn from(conf: aws_smithy_types::retry::RetryConfig) -> Self { - Self::default() - .with_max_attempts(conf.max_attempts()) - .with_initial_backoff(conf.initial_backoff()) - } -} - -const MAX_ATTEMPTS: u32 = 3; -const INITIAL_RETRY_TOKENS: usize = 500; -const RETRY_COST: usize = 5; - -/// Manage retries for a service -/// -/// An implementation of the `standard` AWS retry strategy. A `Strategy` is scoped to a client. -/// For an individual request, call [`Standard::new_request_policy()`](Standard::new_request_policy) -#[derive(Debug, Clone)] -pub struct Standard { - config: Config, - shared_state: CrossRequestRetryState, -} - -impl Standard { - /// Construct a new standard retry policy from the given policy configuration. - pub fn new(config: Config) -> Self { - Self { - shared_state: CrossRequestRetryState::new(config.initial_retry_tokens), - config, - } - } - - /// Set the configuration for this retry policy. - pub fn with_config(&mut self, config: Config) -> &mut Self { - self.config = config; - self - } -} - -impl NewRequestPolicy for Standard { - type Policy = RetryHandler; - - fn new_request_policy(&self, sleep_impl: Option) -> Self::Policy { - RetryHandler { - local: RequestLocalRetryState::new(), - shared: self.shared_state.clone(), - config: self.config.clone(), - sleep_impl, - } - } -} - -impl Default for Standard { - fn default() -> Self { - Self::new(Config::default()) - } -} - -#[derive(Clone, Debug)] -struct RequestLocalRetryState { - attempts: u32, - last_quota_usage: Option, -} - -impl Default for RequestLocalRetryState { - fn default() -> Self { - Self { - // Starts at one to account for the initial request that failed and warranted a retry - attempts: 1, - last_quota_usage: None, - } - } -} - -impl RequestLocalRetryState { - fn new() -> Self { - Self::default() - } -} - -/* TODO(retries) -/// RetryPartition represents a scope for cross request retry state -/// -/// For example, a retry partition could be the id of a service. This would give each service a separate retry budget. -struct RetryPartition(Cow<'static, str>); */ - -/// Shared state between multiple requests to the same client. -#[derive(Clone, Debug)] -struct CrossRequestRetryState { - quota_available: Arc>, -} - -// clippy is upset that we didn't use AtomicUsize here, but doing so makes the code -// significantly more complicated for negligible benefit. -#[allow(clippy::mutex_atomic)] -impl CrossRequestRetryState { - fn new(initial_quota: usize) -> Self { - Self { - quota_available: Arc::new(Mutex::new(initial_quota)), - } - } - - fn quota_release(&self, value: Option, config: &Config) { - let mut quota = self.quota_available.lock().unwrap(); - *quota += value.unwrap_or(config.no_retry_increment); - } - - /// Attempt to acquire retry quota for `ErrorKind` - /// - /// If quota is available, the amount of quota consumed is returned - /// If no quota is available, `None` is returned. - fn quota_acquire(&self, err: &ErrorKind, config: &Config) -> Option { - let mut quota = self.quota_available.lock().unwrap(); - let retry_cost = if err == &ErrorKind::TransientError { - config.timeout_retry_cost - } else { - config.retry_cost - }; - if retry_cost > *quota { - None - } else { - *quota -= retry_cost; - Some(retry_cost) - } - } -} - -type BoxFuture = Pin + Send>>; - -/// RetryHandler -/// -/// Implement retries for an individual request. -/// It is intended to be used as a [Tower Retry Policy](tower::retry::Policy) for use in tower-based -/// middleware stacks. -#[derive(Clone, Debug)] -pub struct RetryHandler { - local: RequestLocalRetryState, - shared: CrossRequestRetryState, - config: Config, - sleep_impl: Option, -} - -#[cfg(test)] -impl RetryHandler { - fn retry_quota(&self) -> usize { - *self.shared.quota_available.lock().unwrap() - } -} - -/// For a request that gets retried 3 times, when base is 1 and initial_backoff is 2 seconds: -/// - the first retry will occur after 0 to 2 seconds -/// - the second retry will occur after 0 to 4 seconds -/// - the third retry will occur after 0 to 8 seconds -/// -/// For a request that gets retried 3 times, when base is 1 and initial_backoff is 30 milliseconds: -/// - the first retry will occur after 0 to 30 milliseconds -/// - the second retry will occur after 0 to 60 milliseconds -/// - the third retry will occur after 0 to 120 milliseconds -fn calculate_exponential_backoff(base: f64, initial_backoff: f64, retry_attempts: u32) -> f64 { - base * initial_backoff * 2_u32.pow(retry_attempts) as f64 -} - -impl RetryHandler { - /// Determine the correct response given `retry_kind` - /// - /// If a retry is specified, this function returns `(next, backoff_duration)` - /// If no retry is specified, this function returns None - fn should_retry_error(&self, error_kind: &ErrorKind) -> Option<(Self, Duration)> { - let quota_used = { - if self.local.attempts == self.config.max_attempts { - tracing::trace!( - attempts = self.local.attempts, - max_attempts = self.config.max_attempts, - "not retrying becuase we are out of attempts" - ); - return None; - } - match self.shared.quota_acquire(error_kind, &self.config) { - Some(quota) => quota, - None => { - tracing::trace!(state = ?self.shared, "not retrying because no quota is available"); - return None; - } - } - }; - let backoff = calculate_exponential_backoff( - // Generate a random base multiplier to create jitter - (self.config.base)(), - // Get the backoff time multiplier in seconds (with fractional seconds) - self.config.initial_backoff.as_secs_f64(), - // `self.local.attempts` tracks number of requests made including the initial request - // The initial attempt shouldn't count towards backoff calculations so we subtract it - self.local.attempts - 1, - ); - let backoff = Duration::from_secs_f64(backoff).min(self.config.max_backoff); - let next = RetryHandler { - local: RequestLocalRetryState { - attempts: self.local.attempts + 1, - last_quota_usage: Some(quota_used), - }, - shared: self.shared.clone(), - config: self.config.clone(), - sleep_impl: self.sleep_impl.clone(), - }; - - Some((next, backoff)) - } - - fn should_retry(&self, retry_kind: &RetryKind) -> Option<(Self, Duration)> { - match retry_kind { - RetryKind::Explicit(dur) => Some((self.clone(), *dur)), - RetryKind::UnretryableFailure => None, - RetryKind::Unnecessary => { - self.shared - .quota_release(self.local.last_quota_usage, &self.config); - None - } - RetryKind::Error(err) => self.should_retry_error(err), - _ => None, - } - } - - fn retry_for(&self, retry_kind: RetryKind) -> Option> { - let retry = self.should_retry(&retry_kind); - tracing::trace!(retry=?retry, retry_kind = ?retry_kind, "retry action"); - let (next, dur) = retry?; - - let sleep = match &self.sleep_impl { - Some(sleep) => sleep, - None => { - if retry_kind != RetryKind::UnretryableFailure { - tracing::debug!("cannot retry because no sleep implementation exists"); - } - return None; - } - }; - - tracing::debug!( - "attempt {} failed with {:?}; retrying after {:?}", - self.local.attempts, - retry_kind, - dur - ); - let sleep_future = sleep.sleep(dur); - let fut = async move { - sleep_future.await; - next - } - .instrument(tracing::debug_span!("retry", kind = &debug(retry_kind))); - Some(check_send(Box::pin(fut))) - } -} - -impl tower::retry::Policy, SdkSuccess, SdkError> - for RetryHandler -where - Handler: Clone, - R: ClassifyRetry, SdkError>, -{ - type Future = BoxFuture; - - fn retry( - &self, - req: &Operation, - result: Result<&SdkSuccess, &SdkError>, - ) -> Option { - let classifier = req.retry_classifier(); - let retry_kind = classifier.classify_retry(result); - tracing::trace!(retry_kind = ?retry_kind, "retry classification"); - self.retry_for(retry_kind) - } - - fn clone_request(&self, req: &Operation) -> Option> { - req.try_clone() - } -} - -fn check_send(t: T) -> T { - t -} - -#[cfg(test)] -mod test { - use super::{calculate_exponential_backoff, Config, NewRequestPolicy, RetryHandler, Standard}; - - use aws_smithy_types::retry::{ErrorKind, RetryKind}; - - use std::time::Duration; - - fn test_config() -> Config { - Config::default().with_base(|| 1_f64) - } - - #[test] - fn retry_handler_send_sync() { - fn must_be_send_sync() {} - - must_be_send_sync::() - } - - #[test] - fn eventual_success() { - let policy = Standard::new(test_config()).new_request_policy(None); - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(1)); - assert_eq!(policy.retry_quota(), 495); - - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(2)); - assert_eq!(policy.retry_quota(), 490); - - let no_retry = policy.should_retry(&RetryKind::Unnecessary); - assert!(no_retry.is_none()); - assert_eq!(policy.retry_quota(), 495); - } - - #[test] - fn no_more_attempts() { - let policy = Standard::new(test_config()).new_request_policy(None); - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(1)); - assert_eq!(policy.retry_quota(), 495); - - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(2)); - assert_eq!(policy.retry_quota(), 490); - - let no_retry = policy.should_retry(&RetryKind::Error(ErrorKind::ServerError)); - assert!(no_retry.is_none()); - assert_eq!(policy.retry_quota(), 490); - } - - #[test] - fn no_quota() { - let mut conf = test_config(); - conf.initial_retry_tokens = 5; - let policy = Standard::new(conf).new_request_policy(None); - - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(1)); - assert_eq!(policy.retry_quota(), 0); - - let no_retry = policy.should_retry(&RetryKind::Error(ErrorKind::ServerError)); - assert!(no_retry.is_none()); - assert_eq!(policy.retry_quota(), 0); - } - - #[test] - fn quota_replenishes_on_success() { - let mut conf = test_config(); - conf.initial_retry_tokens = 100; - let policy = Standard::new(conf).new_request_policy(None); - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::TransientError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(1)); - assert_eq!(policy.retry_quota(), 90); - - let (policy, dur) = policy - .should_retry(&RetryKind::Explicit(Duration::from_secs(1))) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(1)); - assert_eq!( - policy.retry_quota(), - 90, - "explicit retry should not subtract from quota" - ); - - assert!( - policy.should_retry(&RetryKind::Unnecessary).is_none(), - "it should not retry success" - ); - let available = policy.shared.quota_available.lock().unwrap(); - assert_eq!(100, *available, "successful request should replenish quota"); - } - - #[test] - fn backoff_timing() { - let mut conf = test_config(); - conf.max_attempts = 5; - let policy = Standard::new(conf).new_request_policy(None); - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(1)); - assert_eq!(policy.retry_quota(), 495); - - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(2)); - assert_eq!(policy.retry_quota(), 490); - - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(4)); - assert_eq!(policy.retry_quota(), 485); - - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(8)); - assert_eq!(policy.retry_quota(), 480); - - let no_retry = policy.should_retry(&RetryKind::Error(ErrorKind::ServerError)); - assert!(no_retry.is_none()); - assert_eq!(policy.retry_quota(), 480); - } - - #[test] - fn max_backoff_time() { - let mut conf = test_config(); - conf.max_attempts = 5; - conf.initial_backoff = Duration::from_secs(1); - conf.max_backoff = Duration::from_secs(3); - let policy = Standard::new(conf).new_request_policy(None); - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(1)); - assert_eq!(policy.retry_quota(), 495); - - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(2)); - assert_eq!(policy.retry_quota(), 490); - - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(3)); - assert_eq!(policy.retry_quota(), 485); - - let (policy, dur) = policy - .should_retry(&RetryKind::Error(ErrorKind::ServerError)) - .expect("should retry"); - assert_eq!(dur, Duration::from_secs(3)); - assert_eq!(policy.retry_quota(), 480); - - let no_retry = policy.should_retry(&RetryKind::Error(ErrorKind::ServerError)); - assert!(no_retry.is_none()); - assert_eq!(policy.retry_quota(), 480); - } - - #[test] - fn calculate_exponential_backoff_where_initial_backoff_is_one() { - let initial_backoff = 1.0; - - for (attempt, expected_backoff) in [initial_backoff, 2.0, 4.0].into_iter().enumerate() { - let actual_backoff = - calculate_exponential_backoff(1.0, initial_backoff, attempt as u32); - assert_eq!(expected_backoff, actual_backoff); - } - } - - #[test] - fn calculate_exponential_backoff_where_initial_backoff_is_greater_than_one() { - let initial_backoff = 3.0; - - for (attempt, expected_backoff) in [initial_backoff, 6.0, 12.0].into_iter().enumerate() { - let actual_backoff = - calculate_exponential_backoff(1.0, initial_backoff, attempt as u32); - assert_eq!(expected_backoff, actual_backoff); - } - } - - #[test] - fn calculate_exponential_backoff_where_initial_backoff_is_less_than_one() { - let initial_backoff = 0.03; - - for (attempt, expected_backoff) in [initial_backoff, 0.06, 0.12].into_iter().enumerate() { - let actual_backoff = - calculate_exponential_backoff(1.0, initial_backoff, attempt as u32); - assert_eq!(expected_backoff, actual_backoff); - } - } -} diff --git a/rust-runtime/aws-smithy-client/src/static_tests.rs b/rust-runtime/aws-smithy-client/src/static_tests.rs deleted file mode 100644 index a8cd503022..0000000000 --- a/rust-runtime/aws-smithy-client/src/static_tests.rs +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -//! This module provides types useful for static tests. -#![allow(missing_docs, missing_debug_implementations)] - -use crate::{Builder, Operation, ParseHttpResponse, ProvideErrorKind}; -use aws_smithy_http::operation; -use aws_smithy_http::retry::DefaultResponseRetryClassifier; - -#[derive(Debug)] -#[non_exhaustive] -pub struct TestOperationError; -impl std::fmt::Display for TestOperationError { - fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - unreachable!("only used for static tests") - } -} -impl std::error::Error for TestOperationError {} -impl ProvideErrorKind for TestOperationError { - fn retryable_error_kind(&self) -> Option { - unreachable!("only used for static tests") - } - - fn code(&self) -> Option<&str> { - unreachable!("only used for static tests") - } -} -#[derive(Clone)] -#[non_exhaustive] -pub struct TestOperation; -impl ParseHttpResponse for TestOperation { - type Output = Result<(), TestOperationError>; - - fn parse_unloaded(&self, _: &mut operation::Response) -> Option { - unreachable!("only used for static tests") - } - - fn parse_loaded(&self, _response: &http::Response) -> Self::Output { - unreachable!("only used for static tests") - } -} -pub type ValidTestOperation = Operation; - -// Statically check that a standard retry can actually be used to build a Client. -#[allow(dead_code)] -#[cfg(test)] -fn sanity_retry() { - Builder::new() - .middleware(tower::layer::util::Identity::new()) - .connector_fn(|_| async { unreachable!() }) - .build() - .check(); -} - -// Statically check that a hyper client can actually be used to build a Client. -#[allow(dead_code)] -#[cfg(all(test, feature = "hyper"))] -fn sanity_hyper(hc: crate::hyper_ext::Adapter) { - Builder::new() - .middleware(tower::layer::util::Identity::new()) - .connector(hc) - .build() - .check(); -} - -// Statically check that a type-erased middleware client is actually a valid Client. -#[allow(dead_code)] -fn sanity_erase_middleware() { - Builder::new() - .middleware(tower::layer::util::Identity::new()) - .connector_fn(|_| async { unreachable!() }) - .build() - .into_dyn_middleware() - .check(); -} - -// Statically check that a type-erased connector client is actually a valid Client. -#[allow(dead_code)] -fn sanity_erase_connector() { - Builder::new() - .middleware(tower::layer::util::Identity::new()) - .connector_fn(|_| async { unreachable!() }) - .build() - .into_dyn_connector() - .check(); -} - -// Statically check that a fully type-erased client is actually a valid Client. -#[allow(dead_code)] -fn sanity_erase_full() { - Builder::new() - .middleware(tower::layer::util::Identity::new()) - .connector_fn(|_| async { unreachable!() }) - .build() - .into_dyn() - .check(); -} - -fn is_send_sync(_: T) {} -fn noarg_is_send_sync() {} - -// Statically check that a fully type-erased client is still Send + Sync. -#[allow(dead_code)] -fn erased_is_send_sync() { - noarg_is_send_sync::(); - noarg_is_send_sync::>(); - is_send_sync( - Builder::new() - .middleware(tower::layer::util::Identity::new()) - .connector_fn(|_| async { unreachable!() }) - .build() - .into_dyn(), - ); -} diff --git a/rust-runtime/aws-smithy-client/src/test_connection.rs b/rust-runtime/aws-smithy-client/src/test_connection.rs deleted file mode 100644 index 622f5fedce..0000000000 --- a/rust-runtime/aws-smithy-client/src/test_connection.rs +++ /dev/null @@ -1,728 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -//! Module with client connectors useful for testing. - -// TODO(docs) -#![allow(missing_docs)] - -use std::fmt::{Debug, Formatter}; -use std::future::Ready; -use std::ops::Deref; -use std::sync::{Arc, Mutex}; -use std::task::{Context, Poll}; - -use http::header::{HeaderName, CONTENT_TYPE}; -use http::Request; -use tokio::sync::oneshot; - -use crate::erase::DynConnector; -use aws_smithy_http::body::SdkBody; -use aws_smithy_http::result::ConnectorError; -use aws_smithy_protocol_test::{assert_ok, validate_body, MediaType}; - -#[doc(inline)] -pub use crate::never; - -/// Test Connection to capture a single request -#[derive(Debug, Clone)] -pub struct CaptureRequestHandler(Arc>); - -#[derive(Debug)] -struct Inner { - response: Option>, - sender: Option>>, -} - -/// Receiver for [`CaptureRequestHandler`](CaptureRequestHandler) -#[derive(Debug)] -pub struct CaptureRequestReceiver { - receiver: oneshot::Receiver>, -} - -impl CaptureRequestReceiver { - /// Expect that a request was sent. Returns the captured request. - /// - /// # Panics - /// If no request was received - #[track_caller] - pub fn expect_request(mut self) -> http::Request { - self.receiver.try_recv().expect("no request was received") - } - - /// Expect that no request was captured. Panics if a request was received. - /// - /// # Panics - /// If a request was received - #[track_caller] - pub fn expect_no_request(mut self) { - self.receiver - .try_recv() - .expect_err("expected no request to be received!"); - } -} - -impl tower::Service> for CaptureRequestHandler { - type Response = http::Response; - type Error = ConnectorError; - type Future = Ready>; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: Request) -> Self::Future { - let mut inner = self.0.lock().unwrap(); - inner - .sender - .take() - .expect("already sent") - .send(req) - .expect("channel not ready"); - std::future::ready(Ok(inner - .response - .take() - .expect("could not handle second request"))) - } -} - -/// Test connection used to capture a single request -/// -/// If response is `None`, it will reply with a 200 response with an empty body -/// -/// Example: -/// ```compile_fail -/// let (server, request) = capture_request(None); -/// let conf = aws_sdk_sts::Config::builder() -/// .http_connector(server) -/// .build(); -/// let client = aws_sdk_sts::Client::from_conf(conf); -/// let _ = client.assume_role_with_saml().send().await; -/// // web identity should be unsigned -/// assert_eq!( -/// request.expect_request().headers().get("AUTHORIZATION"), -/// None -/// ); -/// ``` -pub fn capture_request( - response: Option>, -) -> (CaptureRequestHandler, CaptureRequestReceiver) { - let (tx, rx) = oneshot::channel(); - ( - CaptureRequestHandler(Arc::new(Mutex::new(Inner { - response: Some(response.unwrap_or_else(|| { - http::Response::builder() - .status(200) - .body(SdkBody::empty()) - .expect("unreachable") - })), - sender: Some(tx), - }))), - CaptureRequestReceiver { receiver: rx }, - ) -} - -type ConnectVec = Vec<(http::Request, http::Response)>; - -#[derive(Debug)] -pub struct ValidateRequest { - pub expected: http::Request, - pub actual: http::Request, -} - -impl ValidateRequest { - pub fn assert_matches(&self, ignore_headers: &[HeaderName]) { - let (actual, expected) = (&self.actual, &self.expected); - assert_eq!(expected.uri(), actual.uri()); - for (name, value) in expected.headers() { - if !ignore_headers.contains(name) { - let actual_header = actual - .headers() - .get(name) - .unwrap_or_else(|| panic!("Header {:?} missing", name)); - assert_eq!( - actual_header.to_str().unwrap(), - value.to_str().unwrap(), - "Header mismatch for {:?}", - name - ); - } - } - let actual_str = std::str::from_utf8(actual.body().bytes().unwrap_or(&[])); - let expected_str = std::str::from_utf8(expected.body().bytes().unwrap_or(&[])); - let media_type = if actual - .headers() - .get(CONTENT_TYPE) - .map(|v| v.to_str().unwrap().contains("json")) - .unwrap_or(false) - { - MediaType::Json - } else { - MediaType::Other("unknown".to_string()) - }; - match (actual_str, expected_str) { - (Ok(actual), Ok(expected)) => assert_ok(validate_body(actual, expected, media_type)), - _ => assert_eq!(expected.body().bytes(), actual.body().bytes()), - }; - } -} - -/// TestConnection for use with a [`Client`](crate::Client). -/// -/// A basic test connection. It will: -/// - Respond to requests with a preloaded series of responses -/// - Record requests for future examination -/// -/// The generic parameter `B` is the type of the response body. -/// For more complex use cases, see [Tower Test](https://docs.rs/tower-test/0.4.0/tower_test/) -/// Usage example: -/// ```no_run -/// use aws_smithy_client::test_connection::TestConnection; -/// use aws_smithy_http::body::SdkBody; -/// let events = vec![( -/// http::Request::new(SdkBody::from("request body")), -/// http::Response::builder() -/// .status(200) -/// .body("response body") -/// .unwrap(), -/// )]; -/// let conn = TestConnection::new(events); -/// let client = aws_smithy_client::Client::from(conn); -/// ``` -#[derive(Debug)] -pub struct TestConnection { - data: Arc>>, - requests: Arc>>, -} - -// Need a clone impl that ignores `B` -impl Clone for TestConnection { - fn clone(&self) -> Self { - TestConnection { - data: self.data.clone(), - requests: self.requests.clone(), - } - } -} - -impl TestConnection { - pub fn new(mut data: ConnectVec) -> Self { - data.reverse(); - TestConnection { - data: Arc::new(Mutex::new(data)), - requests: Default::default(), - } - } - - pub fn requests(&self) -> impl Deref> + '_ { - self.requests.lock().unwrap() - } - - #[track_caller] - pub fn assert_requests_match(&self, ignore_headers: &[HeaderName]) { - for req in self.requests().iter() { - req.assert_matches(ignore_headers) - } - let remaining_requests = self.data.lock().unwrap().len(); - let actual_requests = self.requests().len(); - assert_eq!( - remaining_requests, 0, - "Expected {} additional requests ({} were made)", - remaining_requests, actual_requests - ); - } -} - -impl tower::Service> for TestConnection -where - SdkBody: From, -{ - type Response = http::Response; - type Error = ConnectorError; - type Future = Ready>; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, actual: Request) -> Self::Future { - // todo: validate request - if let Some((expected, resp)) = self.data.lock().unwrap().pop() { - self.requests - .lock() - .unwrap() - .push(ValidateRequest { expected, actual }); - std::future::ready(Ok(resp.map(SdkBody::from))) - } else { - std::future::ready(Err(ConnectorError::other("No more data".into(), None))) - } - } -} - -impl From> for crate::Client, tower::layer::util::Identity> -where - B: Send + 'static, - SdkBody: From, -{ - fn from(tc: TestConnection) -> Self { - crate::Builder::new() - .middleware(tower::layer::util::Identity::new()) - .connector(tc) - .build() - } -} - -/// Create a DynConnector from `Fn(http:Request) -> http::Response` -/// -/// # Examples -/// -/// ```rust -/// use aws_smithy_client::test_connection::infallible_connection_fn; -/// let connector = infallible_connection_fn(|_req|http::Response::builder().status(200).body("OK!").unwrap()); -/// ``` -pub fn infallible_connection_fn( - f: impl Fn(http::Request) -> http::Response + Send + Sync + 'static, -) -> DynConnector -where - B: Into, -{ - ConnectionFn::infallible(f) -} - -#[derive(Clone)] -struct ConnectionFn { - #[allow(clippy::type_complexity)] - response: Arc< - dyn Fn(http::Request) -> Result, ConnectorError> - + Send - + Sync, - >, -} - -impl Debug for ConnectionFn { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ConnectionFn").finish() - } -} - -impl ConnectionFn { - fn infallible>( - f: impl Fn(http::Request) -> http::Response + Send + Sync + 'static, - ) -> DynConnector { - DynConnector::new(Self { - response: Arc::new(move |request| Ok(f(request).map(|b| b.into()))), - }) - } -} - -impl tower::Service> for ConnectionFn { - type Response = http::Response; - type Error = ConnectorError; - type Future = Ready>; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: Request) -> Self::Future { - std::future::ready((self.response)(req)) - } -} - -/// [`wire_mock`] contains utilities for mocking at the socket level -/// -/// Other tools in this module actually operate at the `http::Request` / `http::Response` level. This -/// is useful, but it shortcuts the HTTP implementation (e.g. Hyper). [`wire_mock::WireLevelTestConnection`] binds -/// to an actual socket on the host -/// -/// # Examples -/// ``` -/// use tower::layer::util::Identity; -/// use aws_smithy_client::http_connector::ConnectorSettings; -/// use aws_smithy_client::{match_events, ev}; -/// use aws_smithy_client::test_connection::wire_mock::check_matches; -/// # async fn example() { -/// use aws_smithy_client::test_connection::wire_mock::{ReplayedEvent, WireLevelTestConnection}; -/// // This connection binds to a local address -/// let mock = WireLevelTestConnection::spinup(vec![ -/// ReplayedEvent::status(503), -/// ReplayedEvent::status(200) -/// ]).await; -/// let client = aws_smithy_client::Client::builder() -/// .connector(mock.http_connector().connector(&ConnectorSettings::default(), None).unwrap()) -/// .middleware(Identity::new()) -/// .build(); -/// /* do something with */ -/// // assert that you got the events you expected -/// match_events!(ev!(dns), ev!(connect), ev!(http(200)))(&mock.events()); -/// # } -/// ``` -#[cfg(feature = "wiremock")] -pub mod wire_mock { - use bytes::Bytes; - use http::{Request, Response}; - use hyper::client::connect::dns::Name; - use hyper::server::conn::AddrStream; - use hyper::service::{make_service_fn, service_fn}; - use hyper::{Body, Server}; - use std::collections::HashSet; - use std::convert::Infallible; - use std::error::Error; - - use hyper::client::HttpConnector as HyperHttpConnector; - use std::iter; - use std::iter::Once; - use std::net::{SocketAddr, TcpListener}; - use std::sync::{Arc, Mutex}; - use std::task::{Context, Poll}; - - use tokio::spawn; - use tower::Service; - - /// An event recorded by [`WireLevelTestConnection`] - #[derive(Debug, Clone)] - pub enum RecordedEvent { - DnsLookup(String), - NewConnection, - Response(ReplayedEvent), - } - - type Matcher = ( - Box Result<(), Box>>, - &'static str, - ); - - /// This method should only be used by the macro - #[doc(hidden)] - pub fn check_matches(events: &[RecordedEvent], matchers: &[Matcher]) { - let mut events_iter = events.iter(); - let mut matcher_iter = matchers.iter(); - let mut idx = -1; - loop { - idx += 1; - let bail = |err: Box| panic!("failed on event {}:\n {}", idx, err); - match (events_iter.next(), matcher_iter.next()) { - (Some(event), Some((matcher, _msg))) => matcher(event).unwrap_or_else(bail), - (None, None) => return, - (Some(event), None) => { - bail(format!("got {:?} but no more events were expected", event).into()) - } - (None, Some((_expect, msg))) => { - bail(format!("expected {:?} but no more events were expected", msg).into()) - } - } - } - } - - #[macro_export] - macro_rules! matcher { - ($expect:tt) => { - ( - Box::new( - |event: &::aws_smithy_client::test_connection::wire_mock::RecordedEvent| { - if !matches!(event, $expect) { - return Err(format!( - "expected `{}` but got {:?}", - stringify!($expect), - event - ) - .into()); - } - Ok(()) - }, - ), - stringify!($expect), - ) - }; - } - - /// Helper macro to generate a series of test expectations - #[macro_export] - macro_rules! match_events { - ($( $expect:pat),*) => { - |events| { - check_matches(events, &[$( ::aws_smithy_client::matcher!($expect) ),*]); - } - }; - } - - /// Helper to generate match expressions for events - #[macro_export] - macro_rules! ev { - (http($status:expr)) => { - ::aws_smithy_client::test_connection::wire_mock::RecordedEvent::Response( - ReplayedEvent::HttpResponse { - status: $status, - .. - }, - ) - }; - (dns) => { - ::aws_smithy_client::test_connection::wire_mock::RecordedEvent::DnsLookup(_) - }; - (connect) => { - ::aws_smithy_client::test_connection::wire_mock::RecordedEvent::NewConnection - }; - (timeout) => { - ::aws_smithy_client::test_connection::wire_mock::RecordedEvent::Response( - ReplayedEvent::Timeout, - ) - }; - } - - pub use {ev, match_events, matcher}; - - #[derive(Clone, Debug, PartialEq, Eq)] - pub enum ReplayedEvent { - Timeout, - HttpResponse { status: u16, body: Bytes }, - } - - impl ReplayedEvent { - pub fn ok() -> Self { - Self::HttpResponse { - status: 200, - body: Bytes::new(), - } - } - - pub fn with_body(body: &str) -> Self { - Self::HttpResponse { - status: 200, - body: Bytes::copy_from_slice(body.as_ref()), - } - } - - pub fn status(status: u16) -> Self { - Self::HttpResponse { - status, - body: Bytes::new(), - } - } - } - - use crate::erase::boxclone::BoxFuture; - use crate::http_connector::HttpConnector; - use crate::hyper_ext; - use aws_smithy_async::future::never::Never; - use tokio::sync::oneshot; - - /// Test connection that starts a server bound to 0.0.0.0 - /// - /// See the [module docs](crate::test_connection::wire_mock) for a usage example. - /// - /// Usage: - /// - Call [`WireLevelTestConnection::spinup`] to start the server - /// - Use [`WireLevelTestConnection::http_connector`] or [`dns_resolver`](WireLevelTestConnection::dns_resolver) to configure your client. - /// - Make requests to [`endpoint_url`](WireLevelTestConnection::endpoint_url). - /// - Once the test is complete, retrieve a list of events from [`WireLevelTestConnection::events`] - #[derive(Debug)] - pub struct WireLevelTestConnection { - event_log: Arc>>, - bind_addr: SocketAddr, - // when the sender is dropped, that stops the server - shutdown_hook: oneshot::Sender<()>, - } - - impl WireLevelTestConnection { - pub async fn spinup(mut response_events: Vec) -> Self { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let (tx, rx) = oneshot::channel(); - let listener_addr = listener.local_addr().unwrap(); - response_events.reverse(); - let response_events = Arc::new(Mutex::new(response_events)); - let handler_events = response_events; - let wire_events = Arc::new(Mutex::new(vec![])); - let wire_log_for_service = wire_events.clone(); - let poisoned_conns: Arc>> = Default::default(); - let make_service = make_service_fn(move |connection: &AddrStream| { - let poisoned_conns = poisoned_conns.clone(); - let events = handler_events.clone(); - let wire_log = wire_log_for_service.clone(); - let remote_addr = connection.remote_addr(); - tracing::info!("established connection: {:?}", connection); - wire_log.lock().unwrap().push(RecordedEvent::NewConnection); - async move { - Ok::<_, Infallible>(service_fn(move |_: Request| { - if poisoned_conns.lock().unwrap().contains(&remote_addr) { - tracing::error!("poisoned connection {:?} was reused!", &remote_addr); - panic!("poisoned connection was reused!"); - } - let next_event = events.clone().lock().unwrap().pop(); - let wire_log = wire_log.clone(); - let poisoned_conns = poisoned_conns.clone(); - async move { - let next_event = next_event - .unwrap_or_else(|| panic!("no more events! Log: {:?}", wire_log)); - wire_log - .lock() - .unwrap() - .push(RecordedEvent::Response(next_event.clone())); - if next_event == ReplayedEvent::Timeout { - tracing::info!("{} is poisoned", remote_addr); - poisoned_conns.lock().unwrap().insert(remote_addr); - } - tracing::debug!("replying with {:?}", next_event); - let event = generate_response_event(next_event).await; - dbg!(event) - } - })) - } - }); - let server = Server::from_tcp(listener) - .unwrap() - .serve(make_service) - .with_graceful_shutdown(async { - rx.await.ok(); - tracing::info!("server shutdown!"); - }); - spawn(server); - Self { - event_log: wire_events, - bind_addr: listener_addr, - shutdown_hook: tx, - } - } - - /// Retrieve the events recorded by this connection - pub fn events(&self) -> Vec { - self.event_log.lock().unwrap().clone() - } - - fn bind_addr(&self) -> SocketAddr { - self.bind_addr - } - - pub fn dns_resolver(&self) -> LoggingDnsResolver { - let event_log = self.event_log.clone(); - let bind_addr = self.bind_addr; - LoggingDnsResolver { - log: event_log, - socket_addr: bind_addr, - } - } - - /// Prebuilt HTTP connector with correctly wired DNS resolver - /// - /// **Note**: This must be used in tandem with [`Self::dns_resolver`] - pub fn http_connector(&self) -> HttpConnector { - let http_connector = HyperHttpConnector::new_with_resolver(self.dns_resolver()); - hyper_ext::Adapter::builder().build(http_connector).into() - } - - /// Endpoint to use when connecting - /// - /// This works in tandem with the [`Self::dns_resolver`] to bind to the correct local IP Address - pub fn endpoint_url(&self) -> String { - format!( - "http://this-url-is-converted-to-localhost.com:{}", - self.bind_addr().port() - ) - } - - pub fn shutdown(self) { - let _ = self.shutdown_hook.send(()); - } - } - - async fn generate_response_event(event: ReplayedEvent) -> Result, Infallible> { - let resp = match event { - ReplayedEvent::HttpResponse { status, body } => http::Response::builder() - .status(status) - .body(hyper::Body::from(body)) - .unwrap(), - ReplayedEvent::Timeout => { - Never::new().await; - unreachable!() - } - }; - Ok::<_, Infallible>(resp) - } - - /// DNS resolver that keeps a log of all lookups - /// - /// Regardless of what hostname is requested, it will always return the same socket address. - #[derive(Clone, Debug)] - pub struct LoggingDnsResolver { - log: Arc>>, - socket_addr: SocketAddr, - } - - impl Service for LoggingDnsResolver { - type Response = Once; - type Error = Infallible; - type Future = BoxFuture; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: Name) -> Self::Future { - let sock_addr = self.socket_addr; - let log = self.log.clone(); - Box::pin(async move { - println!("looking up {:?}, replying with {:?}", req, sock_addr); - log.lock() - .unwrap() - .push(RecordedEvent::DnsLookup(req.to_string())); - Ok(iter::once(sock_addr)) - }) - } - } -} - -#[cfg(test)] -mod tests { - use tower::Service; - - use aws_smithy_http::body::SdkBody; - use aws_smithy_http::result::ConnectorError; - - use crate::bounds::SmithyConnector; - use crate::test_connection::{capture_request, never::NeverService, TestConnection}; - use crate::Client; - - fn is_send_sync(_: T) {} - - #[test] - fn construct_test_client() { - let test_conn = TestConnection::::new(vec![]); - let client: Client<_, _, _> = test_conn.into(); - is_send_sync(client); - } - - fn is_a_connector(_: &T) - where - T: SmithyConnector, - { - } - - fn quacks_like_a_connector(_: &T) - where - T: Service, Response = http::Response> - + Send - + Sync - + Clone - + 'static, - T::Error: Into + Send + Sync + 'static, - T::Future: Send + 'static, - { - } - - #[test] - fn oneshot_client() { - let (tx, _rx) = capture_request(None); - quacks_like_a_connector(&tx); - is_a_connector(&tx) - } - - #[test] - fn never_test() { - is_a_connector(&NeverService::< - http::Request, - http::Response, - ConnectorError, - >::new()) - } -} diff --git a/rust-runtime/aws-smithy-client/src/timeout.rs b/rust-runtime/aws-smithy-client/src/timeout.rs deleted file mode 100644 index f9a03a41f6..0000000000 --- a/rust-runtime/aws-smithy-client/src/timeout.rs +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Timeout Configuration - -use crate::SdkError; -use aws_smithy_async::future::timeout::Timeout; -use aws_smithy_async::rt::sleep::{AsyncSleep, SharedAsyncSleep, Sleep}; -use aws_smithy_http::operation::Operation; -use aws_smithy_types::timeout::OperationTimeoutConfig; -use pin_project_lite::pin_project; -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::time::Duration; -use tower::Layer; - -#[derive(Debug)] -struct RequestTimeoutError { - kind: &'static str, - duration: Duration, -} - -impl RequestTimeoutError { - fn new(kind: &'static str, duration: Duration) -> Self { - Self { kind, duration } - } -} - -impl std::fmt::Display for RequestTimeoutError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{} occurred after {:?}", self.kind, self.duration) - } -} - -impl std::error::Error for RequestTimeoutError {} - -#[derive(Clone, Debug)] -/// A struct containing everything needed to create a new [`TimeoutService`] -pub struct TimeoutServiceParams { - /// The duration of timeouts created from these params - duration: Duration, - /// The kind of timeouts created from these params - kind: &'static str, - /// The AsyncSleep impl that will be used to create time-limited futures - async_sleep: SharedAsyncSleep, -} - -#[derive(Clone, Debug, Default)] -/// A struct of structs containing everything needed to create new [`TimeoutService`]s -pub(crate) struct ClientTimeoutParams { - /// Params used to create a new API call [`TimeoutService`] - pub(crate) operation_timeout: Option, - /// Params used to create a new API call attempt [`TimeoutService`] - pub(crate) operation_attempt_timeout: Option, -} - -impl ClientTimeoutParams { - pub(crate) fn new( - timeout_config: &OperationTimeoutConfig, - async_sleep: Option, - ) -> Self { - if let Some(async_sleep) = async_sleep { - Self { - operation_timeout: timeout_config.operation_timeout().map(|duration| { - TimeoutServiceParams { - duration, - kind: "operation timeout (all attempts including retries)", - async_sleep: async_sleep.clone(), - } - }), - operation_attempt_timeout: timeout_config.operation_attempt_timeout().map( - |duration| TimeoutServiceParams { - duration, - kind: "operation attempt timeout (single attempt)", - async_sleep: async_sleep.clone(), - }, - ), - } - } else { - Default::default() - } - } -} - -/// A service that wraps another service, adding the ability to set a timeout for requests -/// handled by the inner service. -#[derive(Clone, Debug)] -pub struct TimeoutService { - inner: S, - params: Option, -} - -impl TimeoutService { - /// Create a new `TimeoutService` that will timeout after the duration specified in `params` elapses - pub fn new(inner: S, params: Option) -> Self { - Self { inner, params } - } - - /// Create a new `TimeoutService` that will never timeout - pub fn no_timeout(inner: S) -> Self { - Self { - inner, - params: None, - } - } -} - -/// A layer that wraps services in a timeout service -#[non_exhaustive] -#[derive(Debug)] -pub struct TimeoutLayer(Option); - -impl TimeoutLayer { - /// Create a new `TimeoutLayer` - pub fn new(params: Option) -> Self { - TimeoutLayer(params) - } -} - -impl Layer for TimeoutLayer { - type Service = TimeoutService; - - fn layer(&self, inner: S) -> Self::Service { - TimeoutService { - inner, - params: self.0.clone(), - } - } -} - -pin_project! { - #[non_exhaustive] - #[must_use = "futures do nothing unless you `.await` or poll them"] - // This allow is needed because otherwise Clippy will get mad we didn't document the - // generated TimeoutServiceFutureProj - #[allow(missing_docs)] - #[project = TimeoutServiceFutureProj] - /// A future generated by a [`TimeoutService`] that may or may not have a timeout depending on - /// whether or not one was set. Because `TimeoutService` can be used at multiple levels of the - /// service stack, a `kind` can be set so that when a timeout occurs, you can know which kind of - /// timeout it was. - pub enum TimeoutServiceFuture { - /// A wrapper around an inner future that will output an [`SdkError`] if it runs longer than - /// the given duration - Timeout { - #[pin] - future: Timeout, - kind: &'static str, - duration: Duration, - }, - /// A thin wrapper around an inner future that will never time out - NoTimeout { - #[pin] - future: F - } - } -} - -impl TimeoutServiceFuture { - /// Given a `future`, an implementor of `AsyncSleep`, a `kind` for this timeout, and a `duration`, - /// wrap the `future` inside a [`Timeout`] future and create a new [`TimeoutServiceFuture`] that - /// will output an [`SdkError`] if `future` doesn't complete before `duration` has elapsed. - pub fn new(future: F, params: &TimeoutServiceParams) -> Self { - Self::Timeout { - future: Timeout::new(future, params.async_sleep.sleep(params.duration)), - kind: params.kind, - duration: params.duration, - } - } - - /// Create a [`TimeoutServiceFuture`] that will never time out. - pub fn no_timeout(future: F) -> Self { - Self::NoTimeout { future } - } -} - -impl Future for TimeoutServiceFuture -where - InnerFuture: Future>>, -{ - type Output = Result>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let (future, kind, duration) = match self.project() { - TimeoutServiceFutureProj::NoTimeout { future } => return future.poll(cx), - TimeoutServiceFutureProj::Timeout { - future, - kind, - duration, - } => (future, kind, duration), - }; - match future.poll(cx) { - Poll::Ready(Ok(response)) => Poll::Ready(response), - Poll::Ready(Err(_timeout)) => Poll::Ready(Err(SdkError::timeout_error( - RequestTimeoutError::new(kind, *duration), - ))), - Poll::Pending => Poll::Pending, - } - } -} - -impl tower::Service> for TimeoutService -where - InnerService: tower::Service, Error = SdkError>, -{ - type Response = InnerService::Response; - type Error = SdkError; - type Future = TimeoutServiceFuture; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, req: Operation) -> Self::Future { - let future = self.inner.call(req); - - if let Some(params) = &self.params { - Self::Future::new(future, params) - } else { - Self::Future::no_timeout(future) - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::never::NeverService; - use crate::{SdkError, TimeoutLayer}; - use aws_smithy_async::assert_elapsed; - use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep}; - use aws_smithy_http::body::SdkBody; - use aws_smithy_http::operation::{Operation, Request}; - use aws_smithy_types::timeout::TimeoutConfig; - use std::time::Duration; - use tower::{Service, ServiceBuilder, ServiceExt}; - - #[tokio::test] - async fn test_timeout_service_ends_request_that_never_completes() { - let req = Request::new(http::Request::new(SdkBody::empty())); - let op = Operation::new(req, ()); - let never_service: NeverService<_, (), _> = NeverService::new(); - let timeout_config = OperationTimeoutConfig::from( - TimeoutConfig::builder() - .operation_timeout(Duration::from_secs_f32(0.25)) - .build(), - ); - let sleep_impl = SharedAsyncSleep::new(TokioSleep::new()); - let timeout_service_params = ClientTimeoutParams::new(&timeout_config, Some(sleep_impl)); - let mut svc = ServiceBuilder::new() - .layer(TimeoutLayer::new(timeout_service_params.operation_timeout)) - .service(never_service); - - let now = tokio::time::Instant::now(); - tokio::time::pause(); - - let err: SdkError> = - svc.ready().await.unwrap().call(op).await.unwrap_err(); - - assert_eq!(format!("{:?}", err), "TimeoutError(TimeoutError { source: RequestTimeoutError { kind: \"operation timeout (all attempts including retries)\", duration: 250ms } })"); - assert_elapsed!(now, Duration::from_secs_f32(0.25)); - } -} diff --git a/rust-runtime/aws-smithy-client/test-data/example.com.json b/rust-runtime/aws-smithy-client/test-data/example.com.json deleted file mode 100644 index 821548cc21..0000000000 --- a/rust-runtime/aws-smithy-client/test-data/example.com.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "events": [ - { - "connection_id": 0, - "action": { - "Request": { - "request": { - "uri": "https://www.example.com/", - "headers": {}, - "method": "POST" - } - } - } - }, - { - "connection_id": 0, - "action": { - "Data": { - "data": { - "Utf8": "hello world" - }, - "direction": "Request" - } - } - }, - { - "connection_id": 0, - "action": { - "Eof": { - "ok": true, - "direction": "Request" - } - } - }, - { - "connection_id": 0, - "action": { - "Response": { - "response": { - "Ok": { - "status": 200, - "version": "HTTP/2.0", - "headers": { - "etag": [ - "\"3147526947+ident\"" - ], - "vary": [ - "Accept-Encoding" - ], - "server": [ - "ECS (bsa/EB20)" - ], - "x-cache": [ - "HIT" - ], - "age": [ - "355292" - ], - "content-length": [ - "1256" - ], - "cache-control": [ - "max-age=604800" - ], - "expires": [ - "Mon, 16 Aug 2021 18:51:30 GMT" - ], - "content-type": [ - "text/html; charset=UTF-8" - ], - "date": [ - "Mon, 09 Aug 2021 18:51:30 GMT" - ], - "last-modified": [ - "Thu, 17 Oct 2019 07:18:26 GMT" - ] - } - } - } - } - } - }, - { - "connection_id": 0, - "action": { - "Data": { - "data": { - "Utf8": "hello from example.com" - }, - "direction": "Response" - } - } - }, - { - "connection_id": 0, - "action": { - "Eof": { - "ok": true, - "direction": "Response" - } - } - } - ], - "docs": "test of example.com. response body has been manually changed", - "version": "V0" -} diff --git a/rust-runtime/aws-smithy-client/tests/e2e_test.rs b/rust-runtime/aws-smithy-client/tests/e2e_test.rs deleted file mode 100644 index f18a084bb5..0000000000 --- a/rust-runtime/aws-smithy-client/tests/e2e_test.rs +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -mod test_operation; -use crate::test_operation::{TestOperationParser, TestRetryClassifier}; -use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep}; -use aws_smithy_client::test_connection::TestConnection; -use aws_smithy_client::Client; -use aws_smithy_http::body::SdkBody; -use aws_smithy_http::operation; -use aws_smithy_http::operation::Operation; -use aws_smithy_http::result::SdkError; -use std::time::Duration; -use tower::layer::util::Identity; - -fn test_operation() -> Operation { - let req = operation::Request::new( - http::Request::builder() - .uri("https://test-service.test-region.amazonaws.com/") - .body(SdkBody::from("request body")) - .unwrap(), - ); - Operation::new(req, TestOperationParser).with_retry_classifier(TestRetryClassifier) -} - -#[tokio::test] -async fn end_to_end_retry_test() { - fn req() -> http::Request { - http::Request::builder() - .body(SdkBody::from("request body")) - .unwrap() - } - - fn ok() -> http::Response<&'static str> { - http::Response::builder() - .status(200) - .body("Hello!") - .unwrap() - } - - fn err() -> http::Response<&'static str> { - http::Response::builder() - .status(500) - .body("This was an error") - .unwrap() - } - // 1 failing response followed by 1 successful response - let events = vec![ - // First operation - (req(), err()), - (req(), err()), - (req(), ok()), - // Second operation - (req(), err()), - (req(), ok()), - // Third operation will fail, only errors - (req(), err()), - (req(), err()), - (req(), err()), - (req(), err()), - ]; - let conn = TestConnection::new(events); - let retry_config = aws_smithy_client::retry::Config::default() - .with_max_attempts(4) - // This is the default, just setting it to be explicit - .with_initial_backoff(Duration::from_secs(1)) - .with_base(|| 1_f64); - let client = Client::builder() - .connector(conn.clone()) - .middleware(Identity::new()) - .retry_config(retry_config) - .sleep_impl(SharedAsyncSleep::new(TokioSleep::new())) - .build(); - tokio::time::pause(); - let initial = tokio::time::Instant::now(); - let resp = client - .call(test_operation()) - .await - .expect("successful operation"); - assert_time_passed(initial, Duration::from_secs(3)); - assert_eq!(resp, "Hello!"); - // 3 requests should have been made, 2 failing & one success - assert_eq!(conn.requests().len(), 3); - - let initial = tokio::time::Instant::now(); - client - .call(test_operation()) - .await - .expect("successful operation"); - assert_time_passed(initial, Duration::from_secs(1)); - assert_eq!(conn.requests().len(), 5); - let initial = tokio::time::Instant::now(); - let err = client - .call(test_operation()) - .await - .expect_err("all responses failed"); - // 4 more tries followed by failure - assert_eq!(conn.requests().len(), 9); - assert!(matches!(err, SdkError::ServiceError { .. })); - assert_time_passed(initial, Duration::from_secs(7)); -} - -/// Validate that time has passed with a 5ms tolerance -/// -/// This is to account for some non-determinism in the Tokio timer -fn assert_time_passed(initial: tokio::time::Instant, passed: Duration) { - let now = tokio::time::Instant::now(); - let delta = now - initial; - if (delta.as_millis() as i128 - passed.as_millis() as i128).abs() > 5 { - assert_eq!(delta, passed) - } -} diff --git a/rust-runtime/aws-smithy-client/tests/reconnect_on_transient_error.rs b/rust-runtime/aws-smithy-client/tests/reconnect_on_transient_error.rs deleted file mode 100644 index 695e069cf1..0000000000 --- a/rust-runtime/aws-smithy-client/tests/reconnect_on_transient_error.rs +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#![cfg(feature = "wiremock")] - -mod test_operation; - -use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep}; -use aws_smithy_client::test_connection::wire_mock; -use aws_smithy_client::test_connection::wire_mock::{check_matches, RecordedEvent, ReplayedEvent}; -use aws_smithy_client::{hyper_ext, Builder}; -use aws_smithy_client::{match_events, Client}; -use aws_smithy_http::body::SdkBody; -use aws_smithy_http::operation; -use aws_smithy_http::operation::Operation; -use aws_smithy_types::retry::ReconnectMode; -use aws_smithy_types::timeout::{OperationTimeoutConfig, TimeoutConfig}; -use http::Uri; -use http_body::combinators::BoxBody; -use hyper::client::{Builder as HyperBuilder, HttpConnector}; -use std::convert::Infallible; -use std::time::Duration; -use test_operation::{TestOperationParser, TestRetryClassifier}; -use tower::layer::util::Identity; -use wire_mock::ev; - -fn end_of_test() -> &'static str { - "end_of_test" -} - -fn test_operation( - uri: Uri, - retryable: bool, -) -> Operation { - let mut req = operation::Request::new( - http::Request::builder() - .uri(uri) - .body(SdkBody::from("request body")) - .unwrap(), - ); - if !retryable { - req = req - .augment(|req, _conf| { - Ok::<_, Infallible>( - req.map(|_| SdkBody::from_dyn(BoxBody::new(SdkBody::from("body")))), - ) - }) - .unwrap(); - } - Operation::new(req, TestOperationParser).with_retry_classifier(TestRetryClassifier) -} - -async fn h1_and_h2(events: Vec, match_clause: impl Fn(&[RecordedEvent])) { - wire_level_test(events.clone(), |_b| {}, |b| b, &match_clause).await; - wire_level_test( - events, - |b| { - b.http2_only(true); - }, - |b| b, - match_clause, - ) - .await; - println!("h2 ok!"); -} - -/// Repeatedly send test operation until `end_of_test` is received -/// -/// When the test is over, match_clause is evaluated -async fn wire_level_test( - events: Vec, - hyper_builder_settings: impl Fn(&mut HyperBuilder), - client_builder_settings: impl Fn(Builder) -> Builder, - match_clause: impl Fn(&[RecordedEvent]), -) { - let connection = wire_mock::WireLevelTestConnection::spinup(events).await; - - let http_connector = HttpConnector::new_with_resolver(connection.dns_resolver()); - let mut hyper_builder = hyper::Client::builder(); - hyper_builder_settings(&mut hyper_builder); - let hyper_adapter = hyper_ext::Adapter::builder() - .hyper_builder(hyper_builder) - .build(http_connector); - let client = client_builder_settings( - Client::builder().reconnect_mode(ReconnectMode::ReconnectOnTransientError), - ) - .connector(hyper_adapter) - .middleware(Identity::new()) - .operation_timeout_config(OperationTimeoutConfig::from( - &TimeoutConfig::builder() - .operation_attempt_timeout(Duration::from_millis(100)) - .build(), - )) - .sleep_impl(SharedAsyncSleep::new(TokioSleep::new())) - .build(); - loop { - match client - .call(test_operation( - connection.endpoint_url().parse().unwrap(), - false, - )) - .await - { - Ok(resp) => { - tracing::info!("response: {:?}", resp); - if resp == end_of_test() { - break; - } - } - Err(e) => tracing::info!("error: {:?}", e), - } - } - let events = connection.events(); - match_clause(&events); -} - -#[tokio::test] -async fn non_transient_errors_no_reconect() { - h1_and_h2( - vec![ - ReplayedEvent::status(400), - ReplayedEvent::with_body(end_of_test()), - ], - match_events!(ev!(dns), ev!(connect), ev!(http(400)), ev!(http(200))), - ) - .await -} - -#[tokio::test] -async fn reestablish_dns_on_503() { - h1_and_h2( - vec![ - ReplayedEvent::status(503), - ReplayedEvent::status(503), - ReplayedEvent::status(503), - ReplayedEvent::with_body(end_of_test()), - ], - match_events!( - // first request - ev!(dns), - ev!(connect), - ev!(http(503)), - // second request - ev!(dns), - ev!(connect), - ev!(http(503)), - // third request - ev!(dns), - ev!(connect), - ev!(http(503)), - // all good - ev!(dns), - ev!(connect), - ev!(http(200)) - ), - ) - .await; -} - -#[tokio::test] -async fn connection_shared_on_success() { - h1_and_h2( - vec![ - ReplayedEvent::ok(), - ReplayedEvent::ok(), - ReplayedEvent::status(503), - ReplayedEvent::with_body(end_of_test()), - ], - match_events!( - ev!(dns), - ev!(connect), - ev!(http(200)), - ev!(http(200)), - ev!(http(503)), - ev!(dns), - ev!(connect), - ev!(http(200)) - ), - ) - .await; -} - -#[tokio::test] -async fn no_reconnect_when_disabled() { - use wire_mock::ev; - wire_level_test( - vec![ - ReplayedEvent::status(503), - ReplayedEvent::with_body(end_of_test()), - ], - |_b| {}, - |b| b.reconnect_mode(ReconnectMode::ReuseAllConnections), - match_events!(ev!(dns), ev!(connect), ev!(http(503)), ev!(http(200))), - ) - .await; -} - -#[tokio::test] -async fn connection_reestablished_after_timeout() { - use wire_mock::ev; - h1_and_h2( - vec![ - ReplayedEvent::ok(), - ReplayedEvent::Timeout, - ReplayedEvent::ok(), - ReplayedEvent::Timeout, - ReplayedEvent::with_body(end_of_test()), - ], - match_events!( - // first connection - ev!(dns), - ev!(connect), - ev!(http(200)), - // reuse but got a timeout - ev!(timeout), - // so we reconnect - ev!(dns), - ev!(connect), - ev!(http(200)), - ev!(timeout), - ev!(dns), - ev!(connect), - ev!(http(200)) - ), - ) - .await; -} diff --git a/rust-runtime/aws-smithy-client/tests/test_operation/mod.rs b/rust-runtime/aws-smithy-client/tests/test_operation/mod.rs deleted file mode 100644 index db193e4bd9..0000000000 --- a/rust-runtime/aws-smithy-client/tests/test_operation/mod.rs +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use aws_smithy_http::operation; -use aws_smithy_http::response::ParseHttpResponse; -use aws_smithy_http::result::SdkError; -use aws_smithy_http::retry::ClassifyRetry; -use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind, RetryKind}; -use bytes::Bytes; -use std::error::Error; -use std::fmt::{self, Debug, Display, Formatter}; -use std::str; - -#[derive(Clone)] -pub(super) struct TestOperationParser; - -#[derive(Debug)] -pub(super) struct OperationError(ErrorKind); - -impl Display for OperationError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl Error for OperationError {} - -impl ProvideErrorKind for OperationError { - fn retryable_error_kind(&self) -> Option { - Some(self.0) - } - - fn code(&self) -> Option<&str> { - None - } -} - -impl ParseHttpResponse for TestOperationParser { - type Output = Result; - - fn parse_unloaded(&self, response: &mut operation::Response) -> Option { - tracing::debug!("got response: {:?}", response); - match response.http().status() { - s if s.is_success() => None, - s if s.is_client_error() => Some(Err(OperationError(ErrorKind::ServerError))), - s if s.is_server_error() => Some(Err(OperationError(ErrorKind::TransientError))), - _ => panic!("unexpected status: {}", response.http().status()), - } - } - - fn parse_loaded(&self, response: &http::Response) -> Self::Output { - Ok(str::from_utf8(response.body().as_ref()) - .unwrap() - .to_string()) - } -} - -#[derive(Clone)] -pub(super) struct TestRetryClassifier; - -impl ClassifyRetry> for TestRetryClassifier -where - E: ProvideErrorKind + Debug, - T: Debug, -{ - fn classify_retry(&self, err: Result<&T, &SdkError>) -> RetryKind { - tracing::info!("got response: {:?}", err); - let kind = match err { - Err(SdkError::ServiceError(context)) => context.err().retryable_error_kind(), - Err(SdkError::DispatchFailure(err)) if err.is_timeout() => { - Some(ErrorKind::TransientError) - } - Err(SdkError::TimeoutError(_)) => Some(ErrorKind::TransientError), - Ok(_) => return RetryKind::Unnecessary, - _ => panic!("test handler only handles modeled errors got: {:?}", err), - }; - match kind { - Some(kind) => RetryKind::Error(kind), - None => RetryKind::UnretryableFailure, - } - } -} diff --git a/rust-runtime/aws-smithy-http-auth/Cargo.toml b/rust-runtime/aws-smithy-http-auth/Cargo.toml index 0d70b25eec..4fef70bc0d 100644 --- a/rust-runtime/aws-smithy-http-auth/Cargo.toml +++ b/rust-runtime/aws-smithy-http-auth/Cargo.toml @@ -5,13 +5,12 @@ authors = [ "AWS Rust SDK Team ", "Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com>", ] -description = "Smithy HTTP logic for smithy-rs." +description = "This crate is no longer used by smithy-rs and is deprecated." edition = "2021" license = "Apache-2.0" repository = "https://github.com/awslabs/smithy-rs" [dependencies] -zeroize = "1" [package.metadata.docs.rs] all-features = true diff --git a/rust-runtime/aws-smithy-http-auth/README.md b/rust-runtime/aws-smithy-http-auth/README.md index 1d963cafce..38d5d2dc0b 100644 --- a/rust-runtime/aws-smithy-http-auth/README.md +++ b/rust-runtime/aws-smithy-http-auth/README.md @@ -1,6 +1,6 @@ # aws-smithy-http-auth -HTTP Auth implementation for service clients generated by [smithy-rs](https://github.com/awslabs/smithy-rs). +This crate is no longer used by smithy-rs and is deprecated. Its equivalent logic is now in aws-smithy-runtime-api and aws-smithy-runtime. This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/awslabs/smithy-rs) code generator. In most cases, it should not be used directly. diff --git a/rust-runtime/aws-smithy-http-auth/src/api_key.rs b/rust-runtime/aws-smithy-http-auth/src/api_key.rs deleted file mode 100644 index bb2ab65b3c..0000000000 --- a/rust-runtime/aws-smithy-http-auth/src/api_key.rs +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! HTTP Auth API Key - -use std::cmp::PartialEq; -use std::fmt::Debug; -use std::sync::Arc; -use zeroize::Zeroizing; - -/// Authentication configuration to connect to a Smithy Service -#[derive(Clone, Eq, PartialEq)] -pub struct AuthApiKey(Arc); - -#[derive(Clone, Eq, PartialEq)] -struct Inner { - api_key: Zeroizing, -} - -impl Debug for AuthApiKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut auth_api_key = f.debug_struct("AuthApiKey"); - auth_api_key.field("api_key", &"** redacted **").finish() - } -} - -impl AuthApiKey { - /// Constructs a new API key. - pub fn new(api_key: impl Into) -> Self { - Self(Arc::new(Inner { - api_key: Zeroizing::new(api_key.into()), - })) - } - - /// Returns the underlying api key. - pub fn api_key(&self) -> &str { - &self.0.api_key - } -} - -impl From<&str> for AuthApiKey { - fn from(api_key: &str) -> Self { - Self::from(api_key.to_owned()) - } -} - -impl From for AuthApiKey { - fn from(api_key: String) -> Self { - Self(Arc::new(Inner { - api_key: Zeroizing::new(api_key), - })) - } -} - -#[cfg(test)] -mod tests { - use super::AuthApiKey; - - #[test] - fn api_key_is_equal() { - let api_key_a: AuthApiKey = "some-api-key".into(); - let api_key_b = AuthApiKey::new("some-api-key"); - assert_eq!(api_key_a, api_key_b); - } - - #[test] - fn api_key_is_different() { - let api_key_a = AuthApiKey::new("some-api-key"); - let api_key_b: AuthApiKey = String::from("another-api-key").into(); - assert_ne!(api_key_a, api_key_b); - } -} diff --git a/rust-runtime/aws-smithy-http-auth/src/definition.rs b/rust-runtime/aws-smithy-http-auth/src/definition.rs deleted file mode 100644 index 918f6aae8f..0000000000 --- a/rust-runtime/aws-smithy-http-auth/src/definition.rs +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! HTTP Auth Definition - -use crate::location::HttpAuthLocation; -use std::cmp::PartialEq; -use std::fmt::Debug; - -/// A HTTP-specific authentication scheme that sends an arbitrary -/// auth value in a header or query string parameter. -// As described in the Smithy documentation: -// https://github.com/awslabs/smithy/blob/main/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy -#[derive(Clone, Debug, Default, PartialEq)] -pub struct HttpAuthDefinition { - /// Defines the location of where the Auth is serialized. - location: HttpAuthLocation, - - /// Defines the name of the HTTP header or query string parameter - /// that contains the Auth. - name: String, - - /// Defines the security scheme to use on the `Authorization` header value. - /// This can only be set if the "location" property is set to [`HttpAuthLocation::Header`]. - scheme: Option, -} - -impl HttpAuthDefinition { - /// Returns a builder for `HttpAuthDefinition`. - pub fn builder() -> http_auth_definition::Builder { - http_auth_definition::Builder::default() - } - - /// Constructs a new HTTP auth definition in header. - pub fn header(header_name: N, scheme: S) -> Self - where - N: Into, - S: Into>, - { - let mut builder = Self::builder() - .location(HttpAuthLocation::Header) - .name(header_name); - let scheme: Option = scheme.into(); - if scheme.is_some() { - builder.set_scheme(scheme); - } - builder.build() - } - - /// Constructs a new HTTP auth definition following the RFC 2617 for Basic Auth. - pub fn basic_auth() -> Self { - Self::builder() - .location(HttpAuthLocation::Header) - .name("Authorization".to_owned()) - .scheme("Basic".to_owned()) - .build() - } - - /// Constructs a new HTTP auth definition following the RFC 2617 for Digest Auth. - pub fn digest_auth() -> Self { - Self::builder() - .location(HttpAuthLocation::Header) - .name("Authorization".to_owned()) - .scheme("Digest".to_owned()) - .build() - } - - /// Constructs a new HTTP auth definition following the RFC 6750 for Bearer Auth. - pub fn bearer_auth() -> Self { - Self::builder() - .location(HttpAuthLocation::Header) - .name("Authorization".to_owned()) - .scheme("Bearer".to_owned()) - .build() - } - - /// Constructs a new HTTP auth definition in query string. - pub fn query(name: impl Into) -> Self { - Self::builder() - .location(HttpAuthLocation::Query) - .name(name.into()) - .build() - } - - /// Returns the HTTP auth location. - pub fn location(&self) -> HttpAuthLocation { - self.location - } - - /// Returns the HTTP auth name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the HTTP auth scheme. - pub fn scheme(&self) -> Option<&str> { - self.scheme.as_deref() - } -} - -/// Types associated with [`HttpAuthDefinition`]. -pub mod http_auth_definition { - use super::HttpAuthDefinition; - use crate::{ - definition::HttpAuthLocation, - error::{AuthError, AuthErrorKind}, - }; - - /// A builder for [`HttpAuthDefinition`]. - #[derive(Debug, Default)] - pub struct Builder { - location: Option, - name: Option, - scheme: Option, - } - - impl Builder { - /// Sets the HTTP auth location. - pub fn location(mut self, location: HttpAuthLocation) -> Self { - self.location = Some(location); - self - } - - /// Sets the HTTP auth location. - pub fn set_location(&mut self, location: Option) -> &mut Self { - self.location = location; - self - } - - /// Sets the the HTTP auth name. - pub fn name(mut self, name: impl Into) -> Self { - self.name = Some(name.into()); - self - } - - /// Sets the the HTTP auth name. - pub fn set_name(&mut self, name: Option) -> &mut Self { - self.name = name; - self - } - - /// Sets the HTTP auth scheme. - pub fn scheme(mut self, scheme: impl Into) -> Self { - self.scheme = Some(scheme.into()); - self - } - - /// Sets the HTTP auth scheme. - pub fn set_scheme(&mut self, scheme: Option) -> &mut Self { - self.scheme = scheme; - self - } - - /// Constructs a [`HttpAuthDefinition`] from the builder. - pub fn build(self) -> HttpAuthDefinition { - if self.scheme.is_some() - && self - .name - .as_deref() - .map_or("".to_string(), |s| s.to_ascii_lowercase()) - != "authorization" - { - // Stop execution because the Smithy model should not contain such combination. - // Otherwise, this would cause unexpected behavior in the SDK. - panic!("{}", AuthError::from(AuthErrorKind::SchemeNotAllowed)); - } - HttpAuthDefinition { - location: self.location.unwrap_or_else(|| { - panic!( - "{}", - AuthError::from(AuthErrorKind::MissingRequiredField("location")) - ) - }), - name: self.name.unwrap_or_else(|| { - panic!( - "{}", - AuthError::from(AuthErrorKind::MissingRequiredField("name")) - ) - }), - scheme: self.scheme, - } - } - } -} - -#[cfg(test)] -mod tests { - use super::HttpAuthDefinition; - use crate::location::HttpAuthLocation; - - #[test] - fn definition_for_header_without_scheme() { - let definition = HttpAuthDefinition::header("Header", None); - assert_eq!(definition.location, HttpAuthLocation::Header); - assert_eq!(definition.name, "Header"); - assert_eq!(definition.scheme, None); - } - - #[test] - fn definition_for_authorization_header_with_scheme() { - let definition = HttpAuthDefinition::header("authorization", "Scheme".to_owned()); - assert_eq!(definition.location(), HttpAuthLocation::Header); - assert_eq!(definition.name(), "authorization"); - assert_eq!(definition.scheme(), Some("Scheme")); - } - - #[test] - #[should_panic] - fn definition_fails_with_scheme_not_allowed() { - let _ = HttpAuthDefinition::header("Invalid".to_owned(), "Scheme".to_owned()); - } - - #[test] - fn definition_for_basic() { - let definition = HttpAuthDefinition::basic_auth(); - assert_eq!( - definition, - HttpAuthDefinition { - location: HttpAuthLocation::Header, - name: "Authorization".to_owned(), - scheme: Some("Basic".to_owned()), - } - ); - } - - #[test] - fn definition_for_digest() { - let definition = HttpAuthDefinition::digest_auth(); - assert_eq!(definition.location(), HttpAuthLocation::Header); - assert_eq!(definition.name(), "Authorization"); - assert_eq!(definition.scheme(), Some("Digest")); - } - - #[test] - fn definition_for_bearer_token() { - let definition = HttpAuthDefinition::bearer_auth(); - assert_eq!(definition.location(), HttpAuthLocation::Header); - assert_eq!(definition.name(), "Authorization"); - assert_eq!(definition.scheme(), Some("Bearer")); - } - - #[test] - fn definition_for_query() { - let definition = HttpAuthDefinition::query("query_key"); - assert_eq!(definition.location(), HttpAuthLocation::Query); - assert_eq!(definition.name(), "query_key"); - assert_eq!(definition.scheme(), None); - } -} diff --git a/rust-runtime/aws-smithy-http-auth/src/error.rs b/rust-runtime/aws-smithy-http-auth/src/error.rs deleted file mode 100644 index 227dbe1cf2..0000000000 --- a/rust-runtime/aws-smithy-http-auth/src/error.rs +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! HTTP Auth Error - -use std::cmp::PartialEq; -use std::fmt::Debug; - -#[derive(Debug, Eq, PartialEq)] -pub(crate) enum AuthErrorKind { - InvalidLocation, - MissingRequiredField(&'static str), - SchemeNotAllowed, -} - -/// Error for Smithy authentication -#[derive(Debug, Eq, PartialEq)] -pub struct AuthError { - kind: AuthErrorKind, -} - -impl std::fmt::Display for AuthError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use AuthErrorKind::*; - match &self.kind { - InvalidLocation => write!(f, "invalid location: expected `header` or `query`"), - MissingRequiredField(field) => write!(f, "missing required field: {}", field), - SchemeNotAllowed => write!( - f, - "scheme only allowed when it is set into the `Authorization` header" - ), - } - } -} - -impl From for AuthError { - fn from(kind: AuthErrorKind) -> Self { - Self { kind } - } -} diff --git a/rust-runtime/aws-smithy-http-auth/src/lib.rs b/rust-runtime/aws-smithy-http-auth/src/lib.rs index 8f5f956ced..f54985f0c9 100644 --- a/rust-runtime/aws-smithy-http-auth/src/lib.rs +++ b/rust-runtime/aws-smithy-http-auth/src/lib.rs @@ -3,11 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -// TODO(enableNewSmithyRuntimeCleanup): The contents of this crate are moving into aws-smithy-runtime. -// This crate is kept to continue sorting the middleware implementation until it is removed. -// When removing the old implementation, clear out this crate and deprecate it. - -#![allow(clippy::derive_partial_eq_without_eq)] #![warn( missing_docs, rustdoc::missing_crate_level_docs, @@ -15,9 +10,4 @@ rust_2018_idioms )] -//! Smithy HTTP Auth Types - -pub mod api_key; -pub mod definition; -pub mod error; -pub mod location; +//! This crate is no longer used by smithy-rs and is deprecated. diff --git a/rust-runtime/aws-smithy-http-auth/src/location.rs b/rust-runtime/aws-smithy-http-auth/src/location.rs deleted file mode 100644 index 5b044cf904..0000000000 --- a/rust-runtime/aws-smithy-http-auth/src/location.rs +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! HTTP Auth Location - -use std::cmp::PartialEq; -use std::fmt::Debug; - -use crate::error::{AuthError, AuthErrorKind}; - -/// Enum for describing where the HTTP Auth can be placed. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub enum HttpAuthLocation { - /// In the HTTP header. - #[default] - Header, - /// In the query string of the URL. - Query, -} - -impl HttpAuthLocation { - fn as_str(&self) -> &'static str { - match self { - Self::Header => "header", - Self::Query => "query", - } - } -} - -impl TryFrom<&str> for HttpAuthLocation { - type Error = AuthError; - fn try_from(value: &str) -> Result { - match value { - "header" => Ok(Self::Header), - "query" => Ok(Self::Query), - _ => Err(AuthError::from(AuthErrorKind::InvalidLocation)), - } - } -} - -impl TryFrom for HttpAuthLocation { - type Error = AuthError; - fn try_from(value: String) -> Result { - Self::try_from(value.as_str()) - } -} - -impl AsRef for HttpAuthLocation { - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl std::fmt::Display for HttpAuthLocation { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.as_str(), f) - } -} - -#[cfg(test)] -mod tests { - use super::HttpAuthLocation; - use crate::error::{AuthError, AuthErrorKind}; - - #[test] - fn fails_if_location_is_invalid() { - let actual = HttpAuthLocation::try_from("invalid").unwrap_err(); - let expected = AuthError::from(AuthErrorKind::InvalidLocation); - assert_eq!(actual, expected); - } -} diff --git a/rust-runtime/aws-smithy-http-tower/Cargo.toml b/rust-runtime/aws-smithy-http-tower/Cargo.toml index 048a96f922..51d1d9acca 100644 --- a/rust-runtime/aws-smithy-http-tower/Cargo.toml +++ b/rust-runtime/aws-smithy-http-tower/Cargo.toml @@ -2,25 +2,11 @@ name = "aws-smithy-http-tower" version = "0.0.0-smithy-rs-head" authors = ["AWS Rust SDK Team ", "Russell Cohen "] -description = "Tower-compatible shims for smithy-rs middleware." +description = "This crate is no longer used by smithy-rs and is deprecated." edition = "2021" license = "Apache-2.0" repository = "https://github.com/awslabs/smithy-rs" -[dependencies] -aws-smithy-types = { path = "../aws-smithy-types" } -aws-smithy-http = { path = "../aws-smithy-http" } -tower = { version = "0.4.4" } -pin-project-lite = "0.2.9" -http = "0.2.3" -bytes = "1" -http-body = "0.4.4" -tracing = "0.1" - -[dev-dependencies] -tower = { version = "0.4.4", features = ["util"] } -tokio = { version = "1.23.1", features = ["full"]} - [package.metadata.docs.rs] all-features = true targets = ["x86_64-unknown-linux-gnu"] diff --git a/rust-runtime/aws-smithy-http-tower/README.md b/rust-runtime/aws-smithy-http-tower/README.md index a8ce891921..a76b83783e 100644 --- a/rust-runtime/aws-smithy-http-tower/README.md +++ b/rust-runtime/aws-smithy-http-tower/README.md @@ -1,6 +1,6 @@ # aws-smithy-http-tower -Bindings between the framework-agnostic traits in `aws-smithy-http` and Tower. +This crate is no longer used by smithy-rs 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/awslabs/smithy-rs) code generator. In most cases, it should not be used directly. diff --git a/rust-runtime/aws-smithy-http-tower/external-types.toml b/rust-runtime/aws-smithy-http-tower/external-types.toml index 552b7f76eb..7fa182b39d 100644 --- a/rust-runtime/aws-smithy-http-tower/external-types.toml +++ b/rust-runtime/aws-smithy-http-tower/external-types.toml @@ -1,12 +1 @@ -allowed_external_types = [ - "aws_smithy_http::*", - "http::request::Request", - "http::response::Response", - "tower_service::Service", - - # TODO(https://github.com/awslabs/smithy-rs/issues/1193): Don't expose on `tower::BoxError` - "tower::BoxError", - - # TODO(https://github.com/awslabs/smithy-rs/issues/1193): Decide if we want to continue exposing tower_layer - "tower_layer::Layer", -] +allowed_external_types = [] diff --git a/rust-runtime/aws-smithy-http-tower/src/dispatch.rs b/rust-runtime/aws-smithy-http-tower/src/dispatch.rs deleted file mode 100644 index a10693a62b..0000000000 --- a/rust-runtime/aws-smithy-http-tower/src/dispatch.rs +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use crate::SendOperationError; -use aws_smithy_http::body::SdkBody; -use aws_smithy_http::connection::CaptureSmithyConnection; -use aws_smithy_http::operation; -use aws_smithy_http::result::ConnectorError; -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; -use tower::{Layer, Service}; -use tracing::{debug_span, trace, Instrument}; - -/// Connects Operation driven middleware to an HTTP implementation. -/// -/// It will also wrap the error type in OperationError to enable operation middleware -/// reporting specific errors -#[derive(Clone)] -pub struct DispatchService { - inner: S, -} - -type BoxedResultFuture = Pin> + Send>>; - -impl Service for DispatchService -where - S: Service, Response = http::Response> + Clone + Send + 'static, - S::Error: Into, - S::Future: Send + 'static, -{ - type Response = operation::Response; - type Error = SendOperationError; - type Future = BoxedResultFuture; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner - .poll_ready(cx) - .map_err(|e| SendOperationError::RequestDispatchError(e.into())) - } - - fn call(&mut self, req: operation::Request) -> Self::Future { - let (mut req, property_bag) = req.into_parts(); - // copy the smithy connection - if let Some(smithy_conn) = property_bag.acquire().get::() { - req.extensions_mut().insert(smithy_conn.clone()); - } else { - println!("nothing to copy!"); - } - let mut inner = self.inner.clone(); - let future = async move { - trace!(request = ?req, "dispatching request"); - inner - .call(req) - .await - .map(|resp| operation::Response::from_parts(resp, property_bag)) - .map_err(|e| SendOperationError::RequestDispatchError(e.into())) - } - .instrument(debug_span!("dispatch")); - Box::pin(future) - } -} - -#[derive(Clone, Default)] -#[non_exhaustive] -pub struct DispatchLayer; - -impl DispatchLayer { - pub fn new() -> Self { - DispatchLayer - } -} - -impl Layer for DispatchLayer -where - S: Service>, -{ - type Service = DispatchService; - - fn layer(&self, inner: S) -> Self::Service { - DispatchService { inner } - } -} diff --git a/rust-runtime/aws-smithy-http-tower/src/lib.rs b/rust-runtime/aws-smithy-http-tower/src/lib.rs index 8a41796114..32e46abdb4 100644 --- a/rust-runtime/aws-smithy-http-tower/src/lib.rs +++ b/rust-runtime/aws-smithy-http-tower/src/lib.rs @@ -3,130 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -#![allow(clippy::derive_partial_eq_without_eq)] +//! This crate is no longer used by smithy-rs and is deprecated. + #![warn( - // missing_docs, - // rustdoc::missing_crate_level_docs, + missing_docs, + rustdoc::missing_crate_level_docs, unreachable_pub, rust_2018_idioms )] - -pub mod dispatch; -pub mod map_request; -pub mod parse_response; - -use aws_smithy_http::result::{ConnectorError, SdkError}; -use tower::BoxError; - -/// An Error Occurred During the process of sending an Operation -/// -/// The variants are split to enable the final [SdkError](`aws_smithy_http::result::SdkError`) to differentiate -/// between two types of errors: -/// 1. [`RequestConstructionError`](SendOperationError::RequestConstructionError): Errors where the -/// SDK never attempted to dispatch the underlying `http::Request`. These represent errors that -/// occurred during the request construction pipeline. These generally stem from configuration issues. -/// 2. [`RequestDispatchError`](SendOperationError::RequestDispatchError): Errors where the inner -/// tower service failed (e.g. because the hostname couldn't be resolved, connection errors, -/// socket hangup etc.). In this case, we don't know how much of the request was _actually_ sent -/// to the client. We only know that we never got back an `http::Response` (and instead got an error). -/// -/// `SendOperationError` is currently defined only in `aws-smithy-http-tower` because it may be removed -/// or replaced with `SdkError` in the future. -/// -/// `SendOperationError` MAY be moved to a private module in the future. -#[derive(Debug)] -pub enum SendOperationError { - /// The request could not be constructed - /// - /// These errors usually stem from configuration issues (e.g. no region, bad credential provider, etc.) - RequestConstructionError(BoxError), - - /// The request could not be dispatched - RequestDispatchError(ConnectorError), -} - -/// Convert a `SendOperationError` into an `SdkError` -impl From for SdkError { - fn from(err: SendOperationError) -> Self { - match err { - SendOperationError::RequestDispatchError(e) => { - aws_smithy_http::result::SdkError::dispatch_failure(e) - } - SendOperationError::RequestConstructionError(e) => { - aws_smithy_http::result::SdkError::construction_failure(e) - } - } - } -} - -#[cfg(test)] -mod tests { - use crate::dispatch::DispatchLayer; - use crate::map_request::MapRequestLayer; - use crate::parse_response::ParseResponseLayer; - use aws_smithy_http::body::SdkBody; - use aws_smithy_http::middleware::MapRequest; - use aws_smithy_http::operation; - use aws_smithy_http::operation::{Operation, Request}; - use aws_smithy_http::response::ParseStrictResponse; - use aws_smithy_http::result::ConnectorError; - use aws_smithy_http::retry::DefaultResponseRetryClassifier; - use bytes::Bytes; - use http::Response; - use std::convert::{Infallible, TryInto}; - use tower::{service_fn, Service, ServiceBuilder}; - - /// Creates a stubbed service stack and runs it to validate that all the types line up & - /// everything is properly wired - #[tokio::test] - async fn service_stack() { - #[derive(Clone)] - struct AddHeader; - impl MapRequest for AddHeader { - type Error = Infallible; - - fn name(&self) -> &'static str { - "add_header" - } - - fn apply(&self, request: Request) -> Result { - request.augment(|mut req, _| { - req.headers_mut() - .insert("X-Test", "Value".try_into().unwrap()); - Ok(req) - }) - } - } - - struct TestParseResponse; - impl ParseStrictResponse for TestParseResponse { - type Output = Result; - - fn parse(&self, _response: &Response) -> Self::Output { - Ok("OK".to_string()) - } - } - - let http_layer = service_fn(|_request: http::Request| async move { - if _request.headers().contains_key("X-Test") { - Ok(http::Response::new(SdkBody::from("ok"))) - } else { - Err(ConnectorError::user("header not set".into())) - } - }); - - let mut svc = ServiceBuilder::new() - .layer(ParseResponseLayer::< - TestParseResponse, - DefaultResponseRetryClassifier, - >::new()) - .layer(MapRequestLayer::for_mapper(AddHeader)) - .layer(DispatchLayer) - .service(http_layer); - let req = http::Request::new(SdkBody::from("hello")); - let req = operation::Request::new(req); - let req = Operation::new(req, TestParseResponse); - let resp = svc.call(req).await.expect("Response should succeed"); - assert_eq!(resp.parsed, "OK".to_string()) - } -} diff --git a/rust-runtime/aws-smithy-http-tower/src/map_request.rs b/rust-runtime/aws-smithy-http-tower/src/map_request.rs deleted file mode 100644 index 5dd72a0c89..0000000000 --- a/rust-runtime/aws-smithy-http-tower/src/map_request.rs +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use crate::SendOperationError; -use aws_smithy_http::middleware::{AsyncMapRequest, MapRequest}; -use aws_smithy_http::operation; -use pin_project_lite::pin_project; -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; -use tower::{Layer, Service}; -use tracing::{debug_span, Instrument}; - -#[derive(Debug)] -pub struct AsyncMapRequestLayer { - mapper: M, -} - -impl AsyncMapRequestLayer { - pub fn for_mapper(mapper: M) -> Self { - AsyncMapRequestLayer { mapper } - } -} - -impl Layer for AsyncMapRequestLayer -where - M: Clone, -{ - type Service = AsyncMapRequestService; - - fn layer(&self, inner: S) -> Self::Service { - AsyncMapRequestService { - inner, - mapper: self.mapper.clone(), - } - } -} - -/// Tower service for [`AsyncMapRequest`](aws_smithy_http::middleware::AsyncMapRequest) -#[derive(Clone)] -pub struct AsyncMapRequestService { - inner: S, - mapper: M, -} - -type BoxFuture = Pin + Send>>; - -impl Service for AsyncMapRequestService -where - S: Service + Clone + Send + 'static, - M: AsyncMapRequest, - S::Future: Send + 'static, -{ - type Response = S::Response; - type Error = S::Error; - type Future = BoxFuture>; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, req: operation::Request) -> Self::Future { - let mapper_name = self.mapper.name(); - let mut inner = self.inner.clone(); - let future = self.mapper.apply(req); - Box::pin(async move { - let span = debug_span!("async_map_request", name = mapper_name); - let mapped_request = future - .instrument(span) - .await - .map_err(|e| SendOperationError::RequestConstructionError(e.into()))?; - inner.call(mapped_request).await - }) - } -} - -#[derive(Debug, Default)] -pub struct MapRequestLayer { - mapper: M, -} - -impl MapRequestLayer { - pub fn for_mapper(mapper: M) -> Self { - MapRequestLayer { mapper } - } -} - -impl Layer for MapRequestLayer -where - M: Clone, -{ - type Service = MapRequestService; - - fn layer(&self, inner: S) -> Self::Service { - MapRequestService { - inner, - mapper: self.mapper.clone(), - } - } -} - -pin_project! { - #[project = EnumProj] - pub enum MapRequestFuture { - Inner { - #[pin] - inner: F - }, - Ready { inner: Option }, - } -} - -impl Future for MapRequestFuture -where - F: Future>, -{ - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match self.project() { - EnumProj::Inner { inner: f } => f.poll(cx), - EnumProj::Ready { inner: e } => Poll::Ready(Err(e.take().unwrap())), - } - } -} - -/// Tower service for [`MapRequest`](aws_smithy_http::middleware::MapRequest) -#[derive(Clone)] -pub struct MapRequestService { - inner: S, - mapper: M, -} - -impl Service for MapRequestService -where - S: Service, - M: MapRequest, -{ - type Response = S::Response; - type Error = S::Error; - type Future = MapRequestFuture; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, req: operation::Request) -> Self::Future { - let span = debug_span!("map_request", name = self.mapper.name()); - let mapper = &self.mapper; - match span - .in_scope(|| mapper.apply(req)) - .map_err(|e| SendOperationError::RequestConstructionError(e.into())) - { - Err(e) => MapRequestFuture::Ready { inner: Some(e) }, - Ok(req) => MapRequestFuture::Inner { - inner: self.inner.call(req), - }, - } - } -} diff --git a/rust-runtime/aws-smithy-http-tower/src/parse_response.rs b/rust-runtime/aws-smithy-http-tower/src/parse_response.rs deleted file mode 100644 index 1f6767c661..0000000000 --- a/rust-runtime/aws-smithy-http-tower/src/parse_response.rs +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -use crate::SendOperationError; -use aws_smithy_http::middleware::load_response; -use aws_smithy_http::operation; -use aws_smithy_http::operation::Operation; -use aws_smithy_http::response::ParseHttpResponse; -use aws_smithy_http::result::{SdkError, SdkSuccess}; -use std::future::Future; -use std::marker::PhantomData; -use std::pin::Pin; -use std::task::{Context, Poll}; -use tower::{Layer, Service}; -use tracing::{debug_span, Instrument}; - -/// `ParseResponseService` dispatches [`Operation`](aws_smithy_http::operation::Operation)s and parses them. -/// -/// `ParseResponseService` is intended to wrap a `DispatchService` which will handle the interface between -/// services that operate on [`operation::Request`](operation::Request) and services that operate -/// on [`http::Request`](http::Request). -#[derive(Clone)] -pub struct ParseResponseService { - inner: S, - _output_type: PhantomData<(O, R)>, -} - -#[derive(Default)] -pub struct ParseResponseLayer { - _output_type: PhantomData<(O, R)>, -} - -/// `ParseResponseLayer` dispatches [`Operation`](aws_smithy_http::operation::Operation)s and parses them. -impl ParseResponseLayer { - pub fn new() -> Self { - ParseResponseLayer { - _output_type: Default::default(), - } - } -} - -impl Layer for ParseResponseLayer -where - S: Service, -{ - type Service = ParseResponseService; - - fn layer(&self, inner: S) -> Self::Service { - ParseResponseService { - inner, - _output_type: Default::default(), - } - } -} - -type BoxedResultFuture = Pin> + Send>>; - -/// ParseResponseService -/// -/// Generic Parameter Listing: -/// `S`: The inner service -/// `O`: The type of the response parser whose output type is `Result` -/// `T`: The happy path return of the response parser -/// `E`: The error path return of the response parser -/// `R`: The type of the retry policy -impl - Service> - for ParseResponseService -where - InnerService: - Service, - InnerService::Future: Send + 'static, - ResponseHandler: ParseHttpResponse> - + Send - + Sync - + 'static, - FailureResponse: std::error::Error + 'static, -{ - type Response = SdkSuccess; - type Error = SdkError; - type Future = BoxedResultFuture; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx).map_err(|err| err.into()) - } - - fn call(&mut self, req: Operation) -> Self::Future { - let (req, parts) = req.into_request_response(); - let handler = parts.response_handler; - let resp = self.inner.call(req); - Box::pin(async move { - match resp.await { - Err(e) => Err(e.into()), - Ok(resp) => { - load_response(resp, &handler) - // load_response contains reading the body as far as is required & parsing the response - .instrument(debug_span!("load_response")) - .await - } - } - }) - } -} diff --git a/rust-runtime/aws-smithy-http/src/endpoint.rs b/rust-runtime/aws-smithy-http/src/endpoint.rs index 3a4939434e..8496239787 100644 --- a/rust-runtime/aws-smithy-http/src/endpoint.rs +++ b/rust-runtime/aws-smithy-http/src/endpoint.rs @@ -16,7 +16,6 @@ use std::str::FromStr; use std::sync::Arc; pub mod error; -pub mod middleware; pub use error::ResolveEndpointError; diff --git a/rust-runtime/aws-smithy-http/src/endpoint/middleware.rs b/rust-runtime/aws-smithy-http/src/endpoint/middleware.rs deleted file mode 100644 index 1fbb099b11..0000000000 --- a/rust-runtime/aws-smithy-http/src/endpoint/middleware.rs +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! [`MapRequest`]-based middleware for resolving and applying a request's endpoint. - -use crate::endpoint; -use crate::endpoint::{apply_endpoint, EndpointPrefix, ResolveEndpointError}; -use crate::middleware::MapRequest; -use crate::operation::Request; -use http::header::HeaderName; -use http::{HeaderValue, Uri}; -use std::str::FromStr; - -// TODO(enableNewSmithyRuntimeCleanup): Delete this module - -/// Middleware to apply an HTTP endpoint to the request -/// -/// This middleware reads [`aws_smithy_types::endpoint::Endpoint`] out of the request properties and applies -/// it to the HTTP request. -#[non_exhaustive] -#[derive(Default, Debug, Clone)] -pub struct SmithyEndpointStage; -impl SmithyEndpointStage { - /// Create a new `SmithyEndpointStage`. - pub fn new() -> Self { - Self::default() - } -} - -impl MapRequest for SmithyEndpointStage { - type Error = ResolveEndpointError; - - fn name(&self) -> &'static str { - "resolve_endpoint" - } - - fn apply(&self, request: Request) -> Result { - request.augment(|mut http_req, props| { - // we need to do a little dance so that this works with retries. - // the first pass through, we convert the result into just an endpoint, early returning - // the error. Put the endpoint back in the bag in case this request gets retried. - // - // the next pass through, there is no result, so in that case, we'll look for the - // endpoint directly. - // - // In an ideal world, we would do this in make_operation, but it's much easier for - // certain protocol tests if we allow requests with invalid endpoint to be constructed. - if let Some(endpoint) = props.remove::().transpose()? { - props.insert(endpoint); - }; - let endpoint = props.get::(); - let endpoint = - endpoint.ok_or_else(|| ResolveEndpointError::message("no endpoint present"))?; - - let uri: Uri = endpoint.url().parse().map_err(|err| { - ResolveEndpointError::from_source("endpoint did not have a valid uri", err) - })?; - apply_endpoint(http_req.uri_mut(), &uri, props.get::()).map_err( - |err| { - ResolveEndpointError::message(format!( - "failed to apply endpoint `{:?}` to request `{:?}`", - uri, http_req - )) - .with_source(Some(err.into())) - }, - )?; - for (header_name, header_values) in endpoint.headers() { - http_req.headers_mut().remove(header_name); - for value in header_values { - http_req.headers_mut().insert( - HeaderName::from_str(header_name).map_err(|err| { - ResolveEndpointError::message("invalid header name") - .with_source(Some(err.into())) - })?, - HeaderValue::from_str(value).map_err(|err| { - ResolveEndpointError::message("invalid header value") - .with_source(Some(err.into())) - })?, - ); - } - } - Ok(http_req) - }) - } -} diff --git a/rust-runtime/aws-smithy-http/src/http.rs b/rust-runtime/aws-smithy-http/src/http.rs index ad77e951c3..2a988f00f2 100644 --- a/rust-runtime/aws-smithy-http/src/http.rs +++ b/rust-runtime/aws-smithy-http/src/http.rs @@ -27,13 +27,3 @@ impl HttpHeaders for http::Response { self.headers_mut() } } - -impl HttpHeaders for crate::operation::Response { - fn http_headers(&self) -> &HeaderMap { - self.http().http_headers() - } - - fn http_headers_mut(&mut self) -> &mut HeaderMap { - self.http_mut().http_headers_mut() - } -} diff --git a/rust-runtime/aws-smithy-http/src/lib.rs b/rust-runtime/aws-smithy-http/src/lib.rs index 52a259b422..2601a8e762 100644 --- a/rust-runtime/aws-smithy-http/src/lib.rs +++ b/rust-runtime/aws-smithy-http/src/lib.rs @@ -36,15 +36,11 @@ pub mod futures_stream_adapter; pub mod header; pub mod http; pub mod label; -pub mod middleware; pub mod operation; -pub mod property_bag; pub mod query; #[doc(hidden)] pub mod query_writer; -pub mod response; pub mod result; -pub mod retry; #[cfg(feature = "event-stream")] pub mod event_stream; diff --git a/rust-runtime/aws-smithy-http/src/middleware.rs b/rust-runtime/aws-smithy-http/src/middleware.rs deleted file mode 100644 index 3285dec872..0000000000 --- a/rust-runtime/aws-smithy-http/src/middleware.rs +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! This modules defines the core, framework agnostic, HTTP middleware interface -//! used by the SDK -//! -//! smithy-middleware-tower provides Tower-specific middleware utilities (todo) - -use crate::body::SdkBody; -use crate::operation; -use crate::response::ParseHttpResponse; -use crate::result::{SdkError, SdkSuccess}; -use bytes::{Buf, Bytes}; -use http_body::Body; -use pin_utils::pin_mut; -use std::error::Error; -use std::future::Future; -use tracing::{debug_span, trace, Instrument}; - -type BoxError = Box; - -const LOG_SENSITIVE_BODIES: &str = "LOG_SENSITIVE_BODIES"; - -/// [`AsyncMapRequest`] defines an asynchronous middleware that transforms an [`operation::Request`]. -/// -/// Typically, these middleware will read configuration from the `PropertyBag` and use it to -/// augment the request. -/// -/// Most fundamental middleware is expressed as `AsyncMapRequest`'s synchronous cousin, `MapRequest`, -/// including signing & endpoint resolution. `AsyncMapRequest` is used for async credential -/// retrieval (e.g., from AWS STS's AssumeRole operation). -pub trait AsyncMapRequest { - /// The type returned when this [`AsyncMapRequest`] encounters an error. - type Error: Into + 'static; - /// The type returned when [`AsyncMapRequest::apply`] is called. - type Future: Future> + Send + 'static; - - /// Returns the name of this map request operation for inclusion in a tracing span. - fn name(&self) -> &'static str; - - /// Call this middleware, returning a future that resolves to a request or an error. - fn apply(&self, request: operation::Request) -> Self::Future; -} - -/// [`MapRequest`] defines a synchronous middleware that transforms an [`operation::Request`]. -/// -/// Typically, these middleware will read configuration from the `PropertyBag` and use it to -/// augment the request. Most fundamental middleware is expressed as `MapRequest`, including -/// signing & endpoint resolution. -/// -/// ## Examples -/// -/// ```rust -/// # use aws_smithy_http::middleware::MapRequest; -/// # use std::convert::Infallible; -/// # use aws_smithy_http::operation; -/// use http::header::{HeaderName, HeaderValue}; -/// -/// /// Signaling struct added to the request property bag if a header should be added -/// struct NeedsHeader; -/// -/// struct AddHeader(HeaderName, HeaderValue); -/// -/// impl MapRequest for AddHeader { -/// type Error = Infallible; -/// -/// fn name(&self) -> &'static str { -/// "add_header" -/// } -/// -/// fn apply(&self, request: operation::Request) -> Result { -/// request.augment(|mut request, properties| { -/// if properties.get::().is_some() { -/// request.headers_mut().append(self.0.clone(), self.1.clone()); -/// } -/// Ok(request) -/// }) -/// } -/// } -/// ``` -pub trait MapRequest { - /// The Error type returned by this operation. - /// - /// If this middleware never fails use [std::convert::Infallible] or similar. - type Error: Into; - - /// Returns the name of this map request operation for inclusion in a tracing span. - fn name(&self) -> &'static str; - - /// Apply this middleware to a request. - /// - /// Typically, implementations will use [`request.augment`](crate::operation::Request::augment) - /// to be able to transform an owned `http::Request`. - fn apply(&self, request: operation::Request) -> Result; -} - -/// Load a response using `handler` to parse the results. -/// -/// This function is intended to be used on the response side of a middleware chain. -/// -/// Success and failure will be split and mapped into `SdkSuccess` and `SdkError`. -/// Generic Parameters: -/// - `O`: The Http response handler that returns `Result` -/// - `T`/`E`: `Result` returned by `handler`. -pub async fn load_response( - mut response: operation::Response, - handler: &O, -) -> Result, SdkError> -where - O: ParseHttpResponse>, -{ - if let Some(parsed_response) = - debug_span!("parse_unloaded").in_scope(|| handler.parse_unloaded(&mut response)) - { - trace!(response = ?response, "read HTTP headers for streaming response"); - return sdk_result(parsed_response, response); - } - - let (http_response, properties) = response.into_parts(); - let (parts, body) = http_response.into_parts(); - let body = match read_body(body).instrument(debug_span!("read_body")).await { - Ok(body) => body, - Err(err) => { - return Err(SdkError::response_error( - err, - operation::Response::from_parts( - http::Response::from_parts(parts, SdkBody::taken()), - properties, - ), - )); - } - }; - - let http_response = http::Response::from_parts(parts, Bytes::from(body)); - if !handler.sensitive() - || std::env::var(LOG_SENSITIVE_BODIES) - .map(|v| v.eq_ignore_ascii_case("true")) - .unwrap_or_default() - { - trace!(http_response = ?http_response, "read HTTP response body"); - } else { - trace!(http_response = "** REDACTED **. To print, set LOG_SENSITIVE_BODIES=true") - } - debug_span!("parse_loaded").in_scope(move || { - let parsed = handler.parse_loaded(&http_response); - sdk_result( - parsed, - operation::Response::from_parts(http_response.map(SdkBody::from), properties), - ) - }) -} - -async fn read_body(body: B) -> Result, B::Error> { - let mut output = Vec::new(); - pin_mut!(body); - while let Some(buf) = body.data().await { - let mut buf = buf?; - while buf.has_remaining() { - output.extend_from_slice(buf.chunk()); - buf.advance(buf.chunk().len()) - } - } - Ok(output) -} - -/// Convert a `Result` into an `SdkResult` that includes the operation response -fn sdk_result( - parsed: Result, - raw: operation::Response, -) -> Result, SdkError> { - match parsed { - Ok(parsed) => Ok(SdkSuccess { raw, parsed }), - Err(err) => Err(SdkError::service_error(err, raw)), - } -} diff --git a/rust-runtime/aws-smithy-http/src/operation.rs b/rust-runtime/aws-smithy-http/src/operation.rs index 2ddbd28070..d31997c85e 100644 --- a/rust-runtime/aws-smithy-http/src/operation.rs +++ b/rust-runtime/aws-smithy-http/src/operation.rs @@ -6,16 +6,12 @@ //! Types for representing the interaction between a service an a client, referred to as an "operation" in smithy. //! Clients "send" operations to services, which are composed of 1 or more HTTP requests. -use crate::body::SdkBody; -use crate::property_bag::{PropertyBag, SharedPropertyBag}; -use crate::retry::DefaultResponseRetryClassifier; use aws_smithy_types::config_bag::{Storable, StoreReplace}; use std::borrow::Cow; -use std::ops::{Deref, DerefMut}; pub mod error; -/// Metadata attached to an [`Operation`] that identifies the API being called. +/// Metadata added to the [`ConfigBag`](aws_smithy_types::config_bag::ConfigBag) that identifies the API being called. #[derive(Clone, Debug)] pub struct Metadata { operation: Cow<'static, str>, @@ -48,309 +44,3 @@ impl Metadata { impl Storable for Metadata { type Storer = StoreReplace; } - -/// Non-request parts of an [`Operation`]. -/// -/// Generics: -/// - `H`: Response handler -/// - `R`: Implementation of `ClassifyRetry` -#[non_exhaustive] -#[derive(Clone, Debug)] -pub struct Parts { - /// The response deserializer that will convert the connector's response into an `operation::Response` - pub response_handler: H, - /// The classifier that will determine if an HTTP response indicates that a request failed for a retryable reason. - pub retry_classifier: R, - /// Metadata describing this operation and the service it relates to. - pub metadata: Option, -} - -// TODO(enableNewSmithyRuntimeCleanup): Delete `operation::Operation` when cleaning up middleware -/// An [`Operation`] is a request paired with a response handler, retry classifier, -/// and metadata that identifies the API being called. -/// -/// Generics: -/// - `H`: Response handler -/// - `R`: Implementation of `ClassifyRetry` -#[derive(Debug)] -pub struct Operation { - request: Request, - parts: Parts, -} - -impl Operation { - /// Converts this operation into its parts. - pub fn into_request_response(self) -> (Request, Parts) { - (self.request, self.parts) - } - - /// Constructs an [`Operation`] from a request and [`Parts`] - pub fn from_parts(request: Request, parts: Parts) -> Self { - Self { request, parts } - } - - /// Returns a mutable reference to the request's property bag. - pub fn properties_mut(&mut self) -> impl DerefMut + '_ { - self.request.properties_mut() - } - - /// Returns an immutable reference to the request's property bag. - pub fn properties(&self) -> impl Deref + '_ { - self.request.properties() - } - - /// Gives mutable access to the underlying HTTP request. - pub fn request_mut(&mut self) -> &mut http::Request { - self.request.http_mut() - } - - /// Gives readonly access to the underlying HTTP request. - pub fn request(&self) -> &http::Request { - self.request.http() - } - - /// Attaches metadata to the operation. - pub fn with_metadata(mut self, metadata: Metadata) -> Self { - self.parts.metadata = Some(metadata); - self - } - - /// Replaces the retry classifier on the operation. - pub fn with_retry_classifier(self, retry_classifier: R2) -> Operation { - Operation { - request: self.request, - parts: Parts { - response_handler: self.parts.response_handler, - retry_classifier, - metadata: self.parts.metadata, - }, - } - } - - /// Returns the retry classifier for this operation. - pub fn retry_classifier(&self) -> &R { - &self.parts.retry_classifier - } - - /// Attempts to clone the operation. - /// - /// Will return `None` if the request body is already consumed and can't be replayed. - pub fn try_clone(&self) -> Option - where - H: Clone, - R: Clone, - { - let request = self.request.try_clone()?; - Some(Self { - request, - parts: self.parts.clone(), - }) - } -} - -impl Operation { - /// Creates a new [`Operation`]. - pub fn new( - request: Request, - response_handler: H, - ) -> Operation { - Operation { - request, - parts: Parts { - response_handler, - retry_classifier: DefaultResponseRetryClassifier::new(), - metadata: None, - }, - } - } -} - -// TODO(enableNewSmithyRuntimeCleanup): Delete `operation::Request` when cleaning up middleware -/// Operation request type that associates a property bag with an underlying HTTP request. -/// This type represents the request in the Tower `Service` in middleware so that middleware -/// can share information with each other via the properties. -#[derive(Debug)] -pub struct Request { - /// The underlying HTTP Request - inner: http::Request, - - /// Property bag of configuration options - /// - /// Middleware can read and write from the property bag and use its - /// contents to augment the request (see [`Request::augment`](Request::augment)) - properties: SharedPropertyBag, -} - -impl Request { - /// Creates a new operation `Request` with the given `inner` HTTP request. - pub fn new(inner: http::Request) -> Self { - Request { - inner, - properties: SharedPropertyBag::new(), - } - } - - /// Creates a new operation `Request` from its parts. - pub fn from_parts(inner: http::Request, properties: SharedPropertyBag) -> Self { - Request { inner, properties } - } - - /// Allows modification of the HTTP request and associated properties with a fallible closure. - pub fn augment( - self, - f: impl FnOnce(http::Request, &mut PropertyBag) -> Result, T>, - ) -> Result { - let inner = { - let properties: &mut PropertyBag = &mut self.properties.acquire_mut(); - f(self.inner, properties)? - }; - Ok(Request { - inner, - properties: self.properties, - }) - } - - /// Gives mutable access to the properties. - pub fn properties_mut(&mut self) -> impl DerefMut + '_ { - self.properties.acquire_mut() - } - - /// Gives readonly access to the properties. - pub fn properties(&self) -> impl Deref + '_ { - self.properties.acquire() - } - - /// Gives mutable access to the underlying HTTP request. - pub fn http_mut(&mut self) -> &mut http::Request { - &mut self.inner - } - - /// Gives readonly access to the underlying HTTP request. - pub fn http(&self) -> &http::Request { - &self.inner - } - - /// Attempts to clone the operation `Request`. This can fail if the - /// request body can't be cloned, such as if it is being streamed and the - /// stream can't be recreated. - pub fn try_clone(&self) -> Option { - let cloned_body = self.inner.body().try_clone()?; - let mut cloned_request = http::Request::builder() - .uri(self.inner.uri().clone()) - .method(self.inner.method()); - *cloned_request - .headers_mut() - .expect("builder has not been modified, headers must be valid") = - self.inner.headers().clone(); - let inner = cloned_request - .body(cloned_body) - .expect("a clone of a valid request should be a valid request"); - Some(Request { - inner, - properties: self.properties.clone(), - }) - } - - /// Consumes the operation `Request` and returns the underlying HTTP request and properties. - pub fn into_parts(self) -> (http::Request, SharedPropertyBag) { - (self.inner, self.properties) - } -} - -// TODO(enableNewSmithyRuntimeCleanup): Delete `operation::Response` when cleaning up middleware -/// Operation response type that associates a property bag with an underlying HTTP response. -/// This type represents the response in the Tower `Service` in middleware so that middleware -/// can share information with each other via the properties. -#[derive(Debug)] -pub struct Response { - /// The underlying HTTP Response - inner: http::Response, - - /// Property bag of configuration options - properties: SharedPropertyBag, -} - -impl Response { - /// Creates a new operation `Response` with the given `inner` HTTP response. - pub fn new(inner: http::Response) -> Self { - Response { - inner, - properties: SharedPropertyBag::new(), - } - } - - /// Gives mutable access to the properties. - pub fn properties_mut(&mut self) -> impl DerefMut + '_ { - self.properties.acquire_mut() - } - - /// Gives readonly access to the properties. - pub fn properties(&self) -> impl Deref + '_ { - self.properties.acquire() - } - - /// Gives mutable access to the underlying HTTP response. - pub fn http_mut(&mut self) -> &mut http::Response { - &mut self.inner - } - - /// Gives readonly access to the underlying HTTP response. - pub fn http(&self) -> &http::Response { - &self.inner - } - - /// Consumes the operation `Request` and returns the underlying HTTP response and properties. - pub fn into_parts(self) -> (http::Response, SharedPropertyBag) { - (self.inner, self.properties) - } - - /// Return mutable references to the response and property bag contained within this `operation::Response` - pub fn parts_mut( - &mut self, - ) -> ( - &mut http::Response, - impl DerefMut + '_, - ) { - (&mut self.inner, self.properties.acquire_mut()) - } - - /// Creates a new operation `Response` from an HTTP response and property bag. - pub fn from_parts(inner: http::Response, properties: SharedPropertyBag) -> Self { - Response { inner, properties } - } -} - -#[cfg(test)] -mod test { - use crate::body::SdkBody; - use crate::operation::Request; - use http::header::{AUTHORIZATION, CONTENT_LENGTH}; - use http::Uri; - - #[test] - fn try_clone_clones_all_data() { - let mut request = Request::new( - http::Request::builder() - .uri(Uri::from_static("http://www.amazon.com")) - .method("POST") - .header(CONTENT_LENGTH, 456) - .header(AUTHORIZATION, "Token: hello") - .body(SdkBody::from("hello world!")) - .expect("valid request"), - ); - request.properties_mut().insert("hello"); - let cloned = request.try_clone().expect("request is cloneable"); - - let (request, config) = cloned.into_parts(); - assert_eq!(request.uri(), &Uri::from_static("http://www.amazon.com")); - assert_eq!(request.method(), "POST"); - assert_eq!(request.headers().len(), 2); - assert_eq!( - request.headers().get(AUTHORIZATION).unwrap(), - "Token: hello" - ); - assert_eq!(request.headers().get(CONTENT_LENGTH).unwrap(), "456"); - assert_eq!(request.body().bytes().unwrap(), "hello world!".as_bytes()); - assert_eq!(config.acquire().get::<&str>(), Some(&"hello")); - } -} diff --git a/rust-runtime/aws-smithy-http/src/property_bag.rs b/rust-runtime/aws-smithy-http/src/property_bag.rs deleted file mode 100644 index 1ac6adc989..0000000000 --- a/rust-runtime/aws-smithy-http/src/property_bag.rs +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! A typemap used to store configuration for smithy operations. -//! -//! This code is functionally equivalent to `Extensions` in the `http` crate. Examples -//! have been updated to be more relevant for smithy use, the interface has been made public, -//! and the doc comments have been updated to reflect how the property bag is used in the SDK. -//! Additionally, optimizations around the HTTP use case have been removed in favor or simpler code. - -use std::any::{Any, TypeId}; -use std::collections::HashMap; -use std::fmt; -use std::fmt::{Debug, Formatter}; -use std::hash::{BuildHasherDefault, Hasher}; -use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, Mutex}; - -type AnyMap = HashMap>; - -struct NamedType { - name: &'static str, - value: Box, -} - -impl NamedType { - fn as_mut(&mut self) -> Option<&mut T> { - self.value.downcast_mut() - } - - fn as_ref(&self) -> Option<&T> { - self.value.downcast_ref() - } - - fn assume(self) -> Option { - self.value.downcast().map(|t| *t).ok() - } - - fn new(value: T) -> Self { - Self { - name: std::any::type_name::(), - value: Box::new(value), - } - } -} - -// With TypeIds as keys, there's no need to hash them. They are already hashes -// themselves, coming from the compiler. The IdHasher just holds the u64 of -// the TypeId, and then returns it, instead of doing any bit fiddling. -#[derive(Default)] -struct IdHasher(u64); - -impl Hasher for IdHasher { - #[inline] - fn finish(&self) -> u64 { - self.0 - } - - fn write(&mut self, _: &[u8]) { - unreachable!("TypeId calls write_u64"); - } - - #[inline] - fn write_u64(&mut self, id: u64) { - self.0 = id; - } -} - -/// A type-map of configuration data. -/// -/// `PropertyBag` can be used by `Request` and `Response` to store -/// data used to configure the SDK request pipeline. -#[derive(Default)] -pub struct PropertyBag { - // In http where this property bag is usually empty, this makes sense. We will almost always put - // something in the bag, so we could consider removing the layer of indirection. - map: AnyMap, -} - -impl PropertyBag { - /// Create an empty `PropertyBag`. - #[inline] - pub fn new() -> PropertyBag { - PropertyBag { - map: AnyMap::default(), - } - } - - /// Insert a type into this `PropertyBag`. - /// - /// If a value of this type already existed, it will be returned. - /// - /// # Examples - /// - /// ``` - /// # use aws_smithy_http::property_bag::PropertyBag; - /// let mut props = PropertyBag::new(); - /// - /// #[derive(Debug, Eq, PartialEq)] - /// struct Endpoint(&'static str); - /// assert!(props.insert(Endpoint("dynamo.amazon.com")).is_none()); - /// assert_eq!( - /// props.insert(Endpoint("kinesis.amazon.com")), - /// Some(Endpoint("dynamo.amazon.com")) - /// ); - /// ``` - pub fn insert(&mut self, val: T) -> Option { - self.map - .insert(TypeId::of::(), NamedType::new(val)) - .and_then(|val| val.assume()) - } - - /// Get a reference to a type previously inserted on this `PropertyBag`. - /// - /// # Examples - /// - /// ``` - /// # use aws_smithy_http::property_bag::PropertyBag; - /// let mut props = PropertyBag::new(); - /// assert!(props.get::().is_none()); - /// props.insert(5i32); - /// - /// assert_eq!(props.get::(), Some(&5i32)); - /// ``` - pub fn get(&self) -> Option<&T> { - self.map - .get(&TypeId::of::()) - .and_then(|val| val.as_ref()) - } - - /// Returns an iterator of the types contained in this PropertyBag - /// - /// # Stability - /// This method is unstable and may be removed or changed in a future release. The exact - /// format of the returned types may also change. - pub fn contents(&self) -> impl Iterator + '_ { - self.map.values().map(|tpe| tpe.name) - } - - /// Get a mutable reference to a type previously inserted on this `PropertyBag`. - /// - /// # Examples - /// - /// ``` - /// # use aws_smithy_http::property_bag::PropertyBag; - /// let mut props = PropertyBag::new(); - /// props.insert(String::from("Hello")); - /// props.get_mut::().unwrap().push_str(" World"); - /// - /// assert_eq!(props.get::().unwrap(), "Hello World"); - /// ``` - pub fn get_mut(&mut self) -> Option<&mut T> { - self.map - .get_mut(&TypeId::of::()) - .map(|val| val.as_mut().expect("type mismatch!")) - } - - /// Remove a type from this `PropertyBag`. - /// - /// If a value of this type existed, it will be returned. - /// - /// # Examples - /// - /// ``` - /// # use aws_smithy_http::property_bag::PropertyBag; - /// let mut props = PropertyBag::new(); - /// props.insert(5i32); - /// assert_eq!(props.remove::(), Some(5i32)); - /// assert!(props.get::().is_none()); - /// ``` - pub fn remove(&mut self) -> Option { - self.map.remove(&TypeId::of::()).and_then(|tpe| { - (tpe.value as Box) - .downcast() - .ok() - .map(|boxed| *boxed) - }) - } - - /// Clear the `PropertyBag` of all inserted extensions. - /// - /// # Examples - /// - /// ``` - /// # use aws_smithy_http::property_bag::PropertyBag; - /// let mut props = PropertyBag::new(); - /// props.insert(5i32); - /// props.clear(); - /// - /// assert!(props.get::().is_none()); - #[inline] - pub fn clear(&mut self) { - self.map.clear(); - } -} - -impl fmt::Debug for PropertyBag { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut fmt = f.debug_struct("PropertyBag"); - - struct Contents<'a>(&'a PropertyBag); - impl<'a> Debug for Contents<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_list().entries(self.0.contents()).finish() - } - } - fmt.field("contents", &Contents(self)); - fmt.finish() - } -} - -/// A wrapper of [`PropertyBag`] that can be safely shared across threads and cheaply cloned. -/// -/// To access properties, use either `acquire` or `acquire_mut`. This can be one line for -/// single property accesses, for example: -/// ```rust -/// # use aws_smithy_http::property_bag::SharedPropertyBag; -/// # let properties = SharedPropertyBag::new(); -/// let my_string = properties.acquire().get::(); -/// ``` -/// -/// For multiple accesses, the acquire result should be stored as a local since calling -/// acquire repeatedly will be slower than calling it once: -/// ```rust -/// # use aws_smithy_http::property_bag::SharedPropertyBag; -/// # let properties = SharedPropertyBag::new(); -/// let props = properties.acquire(); -/// let my_string = props.get::(); -/// let my_vec = props.get::>(); -/// ``` -/// -/// Use `acquire_mut` to insert properties into the bag: -/// ```rust -/// # use aws_smithy_http::property_bag::SharedPropertyBag; -/// # let properties = SharedPropertyBag::new(); -/// properties.acquire_mut().insert("example".to_string()); -/// ``` -#[derive(Clone, Debug, Default)] -pub struct SharedPropertyBag(Arc>); - -impl SharedPropertyBag { - /// Create an empty `SharedPropertyBag`. - pub fn new() -> Self { - SharedPropertyBag(Arc::new(Mutex::new(PropertyBag::new()))) - } - - /// Acquire an immutable reference to the property bag. - pub fn acquire(&self) -> impl Deref + '_ { - self.0.lock().unwrap() - } - - /// Acquire a mutable reference to the property bag. - pub fn acquire_mut(&self) -> impl DerefMut + '_ { - self.0.lock().unwrap() - } -} - -impl From for SharedPropertyBag { - fn from(bag: PropertyBag) -> Self { - SharedPropertyBag(Arc::new(Mutex::new(bag))) - } -} - -#[cfg(test)] -mod test { - use crate::property_bag::PropertyBag; - - #[test] - fn test_extensions() { - #[derive(Debug, PartialEq)] - struct MyType(i32); - - let mut property_bag = PropertyBag::new(); - - property_bag.insert(5i32); - property_bag.insert(MyType(10)); - - assert_eq!(property_bag.get(), Some(&5i32)); - assert_eq!(property_bag.get_mut(), Some(&mut 5i32)); - - assert_eq!(property_bag.remove::(), Some(5i32)); - assert!(property_bag.get::().is_none()); - - assert_eq!(property_bag.get::(), None); - assert_eq!(property_bag.get(), Some(&MyType(10))); - assert_eq!( - format!("{:?}", property_bag), - r#"PropertyBag { contents: ["aws_smithy_http::property_bag::test::test_extensions::MyType"] }"# - ); - } -} diff --git a/rust-runtime/aws-smithy-http/src/response.rs b/rust-runtime/aws-smithy-http/src/response.rs deleted file mode 100644 index 7ba039f419..0000000000 --- a/rust-runtime/aws-smithy-http/src/response.rs +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! Types for response parsing. - -use crate::operation; -use bytes::Bytes; - -/// `ParseHttpResponse` is a generic trait for parsing structured data from HTTP responses. -/// -/// It is designed to be nearly infinitely flexible, because `Output` is unconstrained, it can be used to support -/// event streams, S3 streaming responses, regular request-response style operations, as well -/// as any other HTTP-based protocol that we manage to come up with. -/// -/// The split between `parse_unloaded` and `parse_loaded` enables keeping the parsing code pure and sync -/// whenever possible and delegating the process of actually reading the HTTP response to the caller when -/// the required behavior is simply "read to the end." -/// -/// It also enables this critical and core trait to avoid being async, and it makes code that uses -/// the trait easier to test. -pub trait ParseHttpResponse { - /// Output type of the HttpResponse. - /// - /// For request/response style operations, this is typically something like: - /// `Result` - /// - /// For streaming operations, this is something like: - /// `Result, TranscribeStreamingError>` - type Output; - - /// Parse an HTTP request without reading the body. If the body must be provided to proceed, - /// return `None` - /// - /// This exists to serve APIs like S3::GetObject where the body is passed directly into the - /// response and consumed by the client. However, even in the case of S3::GetObject, errors - /// require reading the entire body. - /// - /// This also facilitates `EventStream` and other streaming HTTP protocols by enabling the - /// handler to take ownership of the HTTP response directly. - /// - /// Currently `parse_unloaded` operates on a borrowed HTTP request to enable - /// the caller to provide a raw HTTP response to the caller for inspection after the response is - /// returned. For EventStream-like use cases, the caller can use `mem::swap` to replace - /// the streaming body with an empty body as long as the body implements default. - /// - /// We should consider if this is too limiting & if this should take an owned response instead. - fn parse_unloaded(&self, response: &mut operation::Response) -> Option; - - /// Parse an HTTP request from a fully loaded body. This is for standard request/response style - /// APIs like AwsJson 1.0/1.1 and the error path of most streaming APIs - /// - /// Using an explicit body type of Bytes here is a conscious decision—If you _really_ need - /// to precisely control how the data is loaded into memory (e.g. by using `bytes::Buf`), implement - /// your handler in `parse_unloaded`. - /// - /// Production code will never call `parse_loaded` without first calling `parse_unloaded`. However, - /// in tests it may be easier to use `parse_loaded` directly. It is OK to panic in `parse_loaded` - /// if `parse_unloaded` will never return `None`, however, it may make your code easier to test if an - /// implementation is provided. - fn parse_loaded(&self, response: &http::Response) -> Self::Output; - - /// Returns whether the contents of this response are sensitive - /// - /// When this is set to true, wire logging will be disabled - fn sensitive(&self) -> bool { - false - } -} - -/// Convenience Trait for non-streaming APIs -/// -/// `ParseStrictResponse` enables operations that _never_ need to stream the body incrementally to -/// have cleaner implementations. There is a blanket implementation -pub trait ParseStrictResponse { - /// The type returned by this parser. - type Output; - - /// Parse an [`http::Response`] into `Self::Output`. - fn parse(&self, response: &http::Response) -> Self::Output; - - /// Returns whether the contents of this response are sensitive - /// - /// When this is set to true, wire logging will be disabled - fn sensitive(&self) -> bool { - false - } -} - -impl ParseHttpResponse for T { - type Output = T::Output; - - fn parse_unloaded(&self, _response: &mut operation::Response) -> Option { - None - } - - fn parse_loaded(&self, response: &http::Response) -> Self::Output { - self.parse(response) - } - - fn sensitive(&self) -> bool { - ParseStrictResponse::sensitive(self) - } -} - -#[cfg(test)] -mod test { - use crate::body::SdkBody; - use crate::operation; - use crate::response::ParseHttpResponse; - use bytes::Bytes; - use std::mem; - - #[test] - fn supports_streaming_body() { - struct S3GetObject { - _body: SdkBody, - } - - struct S3GetObjectParser; - - impl ParseHttpResponse for S3GetObjectParser { - type Output = S3GetObject; - - fn parse_unloaded(&self, response: &mut operation::Response) -> Option { - // For responses that pass on the body, use mem::take to leave behind an empty body - let body = mem::replace(response.http_mut().body_mut(), SdkBody::taken()); - Some(S3GetObject { _body: body }) - } - - fn parse_loaded(&self, _response: &http::Response) -> Self::Output { - unimplemented!() - } - } - } -} diff --git a/rust-runtime/aws-smithy-http/src/result.rs b/rust-runtime/aws-smithy-http/src/result.rs index c86e9c5f88..03119ef64f 100644 --- a/rust-runtime/aws-smithy-http/src/result.rs +++ b/rust-runtime/aws-smithy-http/src/result.rs @@ -3,31 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -//! `Result` wrapper types for [success](SdkSuccess) and [failure](SdkError) responses. - -use std::error::Error; -use std::fmt; -use std::fmt::{Debug, Display, Formatter}; +//! Types for [error](SdkError) responses. +use crate::body::SdkBody; +use crate::connection::ConnectionMetadata; use aws_smithy_types::error::metadata::{ProvideErrorMetadata, EMPTY_ERROR_METADATA}; use aws_smithy_types::error::ErrorMetadata; use aws_smithy_types::retry::ErrorKind; - -use crate::connection::ConnectionMetadata; -use crate::operation; +use std::error::Error; +use std::fmt; +use std::fmt::{Debug, Display, Formatter}; type BoxError = Box; -/// Successful SDK Result -#[derive(Debug)] -pub struct SdkSuccess { - /// Raw Response from the service. (e.g. Http Response) - pub raw: operation::Response, - - /// Parsed response from the service - pub parsed: O, -} - /// Builders for `SdkError` variant context. pub mod builders { use super::*; @@ -329,7 +317,7 @@ pub trait CreateUnhandledError { /// [`Error::source`](std::error::Error::source) for more details about the underlying cause. #[non_exhaustive] #[derive(Debug)] -pub enum SdkError { +pub enum SdkError> { /// The request failed during construction. It was not dispatched over the network. ConstructionFailure(ConstructionFailure), diff --git a/rust-runtime/aws-smithy-http/src/retry.rs b/rust-runtime/aws-smithy-http/src/retry.rs deleted file mode 100644 index eaf7d0e093..0000000000 --- a/rust-runtime/aws-smithy-http/src/retry.rs +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -//! HTTP specific retry behaviors -//! -//! For protocol agnostic retries, see `aws_smithy_types::Retry`. - -use crate::operation::Response; -use crate::result::SdkError; -use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind, RetryKind}; - -/// Classifies what kind of retry is needed for a given `response`. -pub trait ClassifyRetry: Clone { - /// Run this classifier against a response to determine if it should be retried. - fn classify_retry(&self, response: Result<&T, &E>) -> RetryKind; -} - -const TRANSIENT_ERROR_STATUS_CODES: &[u16] = &[500, 502, 503, 504]; - -/// The default implementation of [`ClassifyRetry`] for generated clients. -#[derive(Clone, Debug, Default)] -pub struct DefaultResponseRetryClassifier; - -impl DefaultResponseRetryClassifier { - /// Creates a new `DefaultResponseRetryClassifier` - pub fn new() -> Self { - Default::default() - } - - /// Matches on the given `result` and, if possible, returns the underlying cause and the operation response - /// that can be used for further classification logic. Otherwise, it returns a `RetryKind` that should be used - /// for the result. - // - // IMPORTANT: This function is used by the AWS SDK in `aws-http` for the SDK's own response classification logic - #[doc(hidden)] - pub fn try_extract_err_response<'a, T, E>( - result: Result<&T, &'a SdkError>, - ) -> Result<(&'a E, &'a Response), RetryKind> { - match result { - Ok(_) => Err(RetryKind::Unnecessary), - Err(SdkError::ServiceError(context)) => Ok((context.err(), context.raw())), - Err(SdkError::TimeoutError(_err)) => Err(RetryKind::Error(ErrorKind::TransientError)), - Err(SdkError::DispatchFailure(err)) => { - if err.is_timeout() || err.is_io() { - Err(RetryKind::Error(ErrorKind::TransientError)) - } else if let Some(ek) = err.as_other() { - Err(RetryKind::Error(ek)) - } else { - Err(RetryKind::UnretryableFailure) - } - } - Err(SdkError::ResponseError { .. }) => Err(RetryKind::Error(ErrorKind::TransientError)), - Err(SdkError::ConstructionFailure(_)) => Err(RetryKind::UnretryableFailure), - } - } -} - -impl ClassifyRetry> for DefaultResponseRetryClassifier -where - E: ProvideErrorKind, -{ - fn classify_retry(&self, result: Result<&T, &SdkError>) -> RetryKind { - let (err, response) = match Self::try_extract_err_response(result) { - Ok(extracted) => extracted, - Err(retry_kind) => return retry_kind, - }; - if let Some(kind) = err.retryable_error_kind() { - return RetryKind::Error(kind); - }; - if TRANSIENT_ERROR_STATUS_CODES.contains(&response.http().status().as_u16()) { - return RetryKind::Error(ErrorKind::TransientError); - }; - RetryKind::UnretryableFailure - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::body::SdkBody; - use crate::operation; - use crate::result::{SdkError, SdkSuccess}; - use crate::retry::ClassifyRetry; - use aws_smithy_types::retry::{ErrorKind, ProvideErrorKind, RetryKind}; - use std::fmt; - - #[derive(Debug)] - struct UnmodeledError; - impl fmt::Display for UnmodeledError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "UnmodeledError") - } - } - impl std::error::Error for UnmodeledError {} - - struct CodedError { - code: &'static str, - } - - impl ProvideErrorKind for UnmodeledError { - fn retryable_error_kind(&self) -> Option { - None - } - - fn code(&self) -> Option<&str> { - None - } - } - - impl ProvideErrorKind for CodedError { - fn retryable_error_kind(&self) -> Option { - None - } - - fn code(&self) -> Option<&str> { - Some(self.code) - } - } - - fn make_err( - err: E, - raw: http::Response<&'static str>, - ) -> Result, SdkError> { - Err(SdkError::service_error( - err, - operation::Response::new(raw.map(SdkBody::from)), - )) - } - - #[test] - fn not_an_error() { - let policy = DefaultResponseRetryClassifier::new(); - let test_response = http::Response::new("OK"); - assert_eq!( - policy.classify_retry(make_err(UnmodeledError, test_response).as_ref()), - RetryKind::UnretryableFailure - ); - } - - #[test] - fn classify_by_response_status() { - let policy = DefaultResponseRetryClassifier::new(); - let test_resp = http::Response::builder() - .status(500) - .body("error!") - .unwrap(); - assert_eq!( - policy.classify_retry(make_err(UnmodeledError, test_resp).as_ref()), - RetryKind::Error(ErrorKind::TransientError) - ); - } - - #[test] - fn classify_by_response_status_not_retryable() { - let policy = DefaultResponseRetryClassifier::new(); - let test_resp = http::Response::builder() - .status(408) - .body("error!") - .unwrap(); - assert_eq!( - policy.classify_retry(make_err(UnmodeledError, test_resp).as_ref()), - RetryKind::UnretryableFailure - ); - } - - #[test] - fn classify_by_error_kind() { - struct ModeledRetries; - let test_response = http::Response::new("OK"); - impl ProvideErrorKind for ModeledRetries { - fn retryable_error_kind(&self) -> Option { - Some(ErrorKind::ClientError) - } - - fn code(&self) -> Option<&str> { - // code should not be called when `error_kind` is provided - unimplemented!() - } - } - - let policy = DefaultResponseRetryClassifier::new(); - - assert_eq!( - policy.classify_retry(make_err(ModeledRetries, test_response).as_ref()), - RetryKind::Error(ErrorKind::ClientError) - ); - } - - #[test] - fn classify_response_error() { - let policy = DefaultResponseRetryClassifier::new(); - assert_eq!( - policy.classify_retry( - Result::, SdkError>::Err(SdkError::response_error( - UnmodeledError, - operation::Response::new(http::Response::new("OK").map(SdkBody::from)), - )) - .as_ref() - ), - RetryKind::Error(ErrorKind::TransientError) - ); - } - - #[test] - fn test_timeout_error() { - let policy = DefaultResponseRetryClassifier::new(); - let err: Result<(), SdkError> = Err(SdkError::timeout_error("blah")); - assert_eq!( - policy.classify_retry(err.as_ref()), - RetryKind::Error(ErrorKind::TransientError) - ); - } -} diff --git a/rust-runtime/aws-smithy-runtime/src/client/http/connection_poisoning.rs b/rust-runtime/aws-smithy-runtime/src/client/http/connection_poisoning.rs index fe26f38520..d15d8230a0 100644 --- a/rust-runtime/aws-smithy-runtime/src/client/http/connection_poisoning.rs +++ b/rust-runtime/aws-smithy-runtime/src/client/http/connection_poisoning.rs @@ -16,7 +16,7 @@ use aws_smithy_types::retry::{ErrorKind, ReconnectMode, RetryConfig}; use std::fmt; use tracing::{debug, error}; -/// A interceptor for poisoning connections in response to certain events. +/// An interceptor for poisoning connections in response to certain events. /// /// This interceptor, when paired with a compatible connection, allows the connection to be /// poisoned in reaction to certain events *(like receiving a transient error.)* This allows users @@ -25,10 +25,11 @@ use tracing::{debug, error}; /// /// **In order for this interceptor to work,** the configured connection must interact with the /// "connection retriever" stored in an HTTP request's `extensions` map. For an example of this, -/// see [aws_smithy_client::hyper_ext::Adapter](https://github.com/awslabs/smithy-rs/blob/47b3d23ff3cabd67e797af616101f5a4ea6be5e8/rust-runtime/aws-smithy-client/src/hyper_ext.rs#L155). -/// When a connection is made available to the retriever, this interceptor will call a `.poison` -/// method on it, signalling that the connection should be dropped. It is up to the connection -/// implementer to handle this. +/// see [`HyperConnector`]. When a connection is made available to the retriever, this interceptor +/// will call a `.poison` method on it, signalling that the connection should be dropped. It is +/// up to the connection implementer to handle this. +/// +/// [`HyperConnector`]: https://github.com/awslabs/smithy-rs/blob/26a914ece072bba2dd9b5b49003204b70e7666ac/rust-runtime/aws-smithy-runtime/src/client/http/hyper_014.rs#L347 #[non_exhaustive] #[derive(Debug, Default)] pub struct ConnectionPoisoningInterceptor {}