From 4563849dc7df8d5b11bce45bad32570cd5d41840 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Tue, 15 Nov 2022 16:47:37 -0800 Subject: [PATCH] Revamp errors in `aws-config` (#1934) --- aws/rust-runtime/aws-config/src/ecs.rs | 126 +++++--- .../aws-config/src/imds/client.rs | 254 +++------------- .../aws-config/src/imds/client/error.rs | 281 ++++++++++++++++++ .../aws-config/src/imds/client/token.rs | 34 +-- .../aws-config/src/imds/credentials.rs | 12 +- .../imds_token_fail/test-case.json | 2 +- 6 files changed, 422 insertions(+), 287 deletions(-) create mode 100644 aws/rust-runtime/aws-config/src/imds/client/error.rs diff --git a/aws/rust-runtime/aws-config/src/ecs.rs b/aws/rust-runtime/aws-config/src/ecs.rs index 377bfe80b2..b31a0664c3 100644 --- a/aws/rust-runtime/aws-config/src/ecs.rs +++ b/aws/rust-runtime/aws-config/src/ecs.rs @@ -100,7 +100,7 @@ impl EcsCredentialsProvider { let auth = match self.env.get(ENV_AUTHORIZATION).ok() { Some(auth) => Some(HeaderValue::from_str(&auth).map_err(|err| { tracing::warn!(token = %auth, "invalid auth token"); - CredentialsError::invalid_configuration(EcsConfigurationErr::InvalidAuthToken { + CredentialsError::invalid_configuration(EcsConfigurationError::InvalidAuthToken { err, value: auth, }) @@ -140,11 +140,11 @@ impl ProvideCredentials for EcsCredentialsProvider { enum Provider { Configured(HttpCredentialProvider), NotConfigured, - InvalidConfiguration(EcsConfigurationErr), + InvalidConfiguration(EcsConfigurationError), } impl Provider { - async fn uri(env: Env, dns: Option) -> Result { + async fn uri(env: Env, dns: Option) -> Result { let relative_uri = env.get(ENV_RELATIVE_URI).ok(); let full_uri = env.get(ENV_FULL_URI).ok(); if let Some(relative_uri) = relative_uri { @@ -153,9 +153,9 @@ impl Provider { let mut dns = dns.or_else(tokio_dns); validate_full_uri(&full_uri, dns.as_mut()) .await - .map_err(|err| EcsConfigurationErr::InvalidFullUri { err, uri: full_uri }) + .map_err(|err| EcsConfigurationError::InvalidFullUri { err, uri: full_uri }) } else { - Err(EcsConfigurationErr::NotConfigured) + Err(EcsConfigurationError::NotConfigured) } } @@ -164,7 +164,7 @@ impl Provider { let env = provider_config.env(); let uri = match Self::uri(env, builder.dns).await { Ok(uri) => uri, - Err(EcsConfigurationErr::NotConfigured) => return Provider::NotConfigured, + Err(EcsConfigurationError::NotConfigured) => return Provider::NotConfigured, Err(err) => return Provider::InvalidConfiguration(err), }; let http_provider = HttpCredentialProvider::builder() @@ -179,12 +179,12 @@ impl Provider { Provider::Configured(http_provider) } - fn build_full_uri(relative_uri: String) -> Result { + fn build_full_uri(relative_uri: String) -> Result { let mut relative_uri = match relative_uri.parse::() { Ok(uri) => uri, Err(invalid_uri) => { tracing::warn!(uri = %DisplayErrorContext(&invalid_uri), "invalid URI loaded from environment"); - return Err(EcsConfigurationErr::InvalidRelativeUri { + return Err(EcsConfigurationError::InvalidRelativeUri { err: invalid_uri, uri: relative_uri, }); @@ -197,7 +197,7 @@ impl Provider { } #[derive(Debug)] -enum EcsConfigurationErr { +enum EcsConfigurationError { InvalidRelativeUri { err: InvalidUri, uri: String, @@ -213,22 +213,22 @@ enum EcsConfigurationErr { NotConfigured, } -impl Display for EcsConfigurationErr { +impl Display for EcsConfigurationError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - EcsConfigurationErr::InvalidRelativeUri { err, uri } => write!( + EcsConfigurationError::InvalidRelativeUri { err, uri } => write!( f, "invalid relative URI for ECS provider ({}): {}", err, uri ), - EcsConfigurationErr::InvalidFullUri { err, uri } => { + EcsConfigurationError::InvalidFullUri { err, uri } => { write!(f, "invalid full URI for ECS provider ({}): {}", err, uri) } - EcsConfigurationErr::NotConfigured => write!( + EcsConfigurationError::NotConfigured => write!( f, "No environment variables were set to configure ECS provider" ), - EcsConfigurationErr::InvalidAuthToken { err, value } => write!( + EcsConfigurationError::InvalidAuthToken { err, value } => write!( f, "`{}` could not be used as a header value for the auth token. {}", value, err @@ -237,12 +237,13 @@ impl Display for EcsConfigurationErr { } } -impl Error for EcsConfigurationErr { +impl Error for EcsConfigurationError { fn source(&self) -> Option<&(dyn Error + 'static)> { match &self { - EcsConfigurationErr::InvalidRelativeUri { err, .. } => Some(err), - EcsConfigurationErr::InvalidFullUri { err, .. } => Some(err), - _ => None, + EcsConfigurationError::InvalidRelativeUri { err, .. } => Some(err), + EcsConfigurationError::InvalidFullUri { err, .. } => Some(err), + EcsConfigurationError::InvalidAuthToken { err, .. } => Some(err), + EcsConfigurationError::NotConfigured => None, } } } @@ -303,12 +304,8 @@ impl Builder { } } -/// Invalid Full URI -/// -/// When the full URI setting is used, the URI must either be HTTPS or point to a loopback interface. #[derive(Debug)] -#[non_exhaustive] -pub enum InvalidFullUriError { +enum InvalidFullUriErrorKind { /// The provided URI could not be parsed as a URI #[non_exhaustive] InvalidUri(InvalidUri), @@ -329,36 +326,51 @@ pub enum InvalidFullUriError { DnsLookupFailed(io::Error), } +/// Invalid Full URI +/// +/// When the full URI setting is used, the URI must either be HTTPS or point to a loopback interface. +#[derive(Debug)] +pub struct InvalidFullUriError { + kind: InvalidFullUriErrorKind, +} + impl Display for InvalidFullUriError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - InvalidFullUriError::InvalidUri(err) => write!(f, "URI was invalid: {}", err), - InvalidFullUriError::MissingHost => write!(f, "URI did not specify a host"), - InvalidFullUriError::NotLoopback => { + use InvalidFullUriErrorKind::*; + match self.kind { + InvalidUri(_) => write!(f, "URI was invalid"), + MissingHost => write!(f, "URI did not specify a host"), + NotLoopback => { write!(f, "URI did not refer to the loopback interface") } - InvalidFullUriError::DnsLookupFailed(err) => { + DnsLookupFailed(_) => { write!( f, - "failed to perform DNS lookup while validating URI: {}", - err + "failed to perform DNS lookup while validating URI" ) } - InvalidFullUriError::NoDnsService => write!(f, "No DNS service was provided. Enable `rt-tokio` or provide a `dns` service to the builder.") + NoDnsService => write!(f, "no DNS service was provided. Enable `rt-tokio` or provide a `dns` service to the builder.") } } } impl Error for InvalidFullUriError { fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - InvalidFullUriError::InvalidUri(err) => Some(err), - InvalidFullUriError::DnsLookupFailed(err) => Some(err), + use InvalidFullUriErrorKind::*; + match &self.kind { + InvalidUri(err) => Some(err), + DnsLookupFailed(err) => Some(err), _ => None, } } } +impl From for InvalidFullUriError { + fn from(kind: InvalidFullUriErrorKind) -> Self { + Self { kind } + } +} + /// Dns resolver interface pub type DnsService = BoxCloneService, io::Error>; @@ -374,20 +386,20 @@ async fn validate_full_uri( ) -> Result { let uri = uri .parse::() - .map_err(InvalidFullUriError::InvalidUri)?; + .map_err(InvalidFullUriErrorKind::InvalidUri)?; if uri.scheme() == Some(&Scheme::HTTPS) { return Ok(uri); } // For HTTP URIs, we need to validate that it points to a loopback address - let host = uri.host().ok_or(InvalidFullUriError::MissingHost)?; + let host = uri.host().ok_or(InvalidFullUriErrorKind::MissingHost)?; let is_loopback = match host.parse::() { Ok(addr) => addr.is_loopback(), Err(_domain_name) => { - let dns = dns.ok_or(InvalidFullUriError::NoDnsService)?; - dns.ready().await.map_err(InvalidFullUriError::DnsLookupFailed)? + let dns = dns.ok_or(InvalidFullUriErrorKind::NoDnsService)?; + dns.ready().await.map_err(InvalidFullUriErrorKind::DnsLookupFailed)? .call(host.to_owned()) .await - .map_err(InvalidFullUriError::DnsLookupFailed)? + .map_err(InvalidFullUriErrorKind::DnsLookupFailed)? .iter() .all(|addr| { if !addr.is_loopback() { @@ -402,7 +414,7 @@ async fn validate_full_uri( }; match is_loopback { true => Ok(uri), - false => Err(InvalidFullUriError::NotLoopback), + false => Err(InvalidFullUriErrorKind::NotLoopback.into()), } } @@ -459,7 +471,7 @@ mod test { use crate::ecs::{ tokio_dns, validate_full_uri, Builder, EcsCredentialsProvider, InvalidFullUriError, - Provider, + InvalidFullUriErrorKind, Provider, }; use crate::provider_config::ProviderConfig; use crate::test_case::GenericTestResult; @@ -547,7 +559,12 @@ mod test { .unwrap() .expect_err("DNS service is required"); assert!( - matches!(no_dns_error, InvalidFullUriError::NoDnsService), + matches!( + no_dns_error, + InvalidFullUriError { + kind: InvalidFullUriErrorKind::NoDnsService + } + ), "expected no dns service, got: {}", no_dns_error ); @@ -567,7 +584,12 @@ mod test { .now_or_never() .unwrap() .expect_err("not a loopback"); - assert!(matches!(err, InvalidFullUriError::NotLoopback)); + assert!(matches!( + err, + InvalidFullUriError { + kind: InvalidFullUriErrorKind::NotLoopback + } + )); } #[test] @@ -594,7 +616,12 @@ mod test { .now_or_never() .unwrap(); assert!( - matches!(resp, Err(InvalidFullUriError::NotLoopback)), + matches!( + resp, + Err(InvalidFullUriError { + kind: InvalidFullUriErrorKind::NotLoopback + }) + ), "Should be invalid: {:?}", resp ); @@ -703,7 +730,16 @@ mod test { let err = validate_full_uri("http://www.amazon.com/creds", dns.as_mut()) .await .expect_err("not a loopback"); - assert!(matches!(err, InvalidFullUriError::NotLoopback), "{:?}", err); + assert!( + matches!( + err, + InvalidFullUriError { + kind: InvalidFullUriErrorKind::NotLoopback + } + ), + "{:?}", + err + ); assert!(logs_contain( "Address does not resolve to the loopback interface" )); diff --git a/aws/rust-runtime/aws-config/src/imds/client.rs b/aws/rust-runtime/aws-config/src/imds/client.rs index 5343781971..f25998c531 100644 --- a/aws/rust-runtime/aws-config/src/imds/client.rs +++ b/aws/rust-runtime/aws-config/src/imds/client.rs @@ -7,15 +7,16 @@ //! //! Client for direct access to IMDSv2. -use std::borrow::Cow; -use std::convert::TryFrom; -use std::error::Error; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; -use std::sync::Arc; -use std::time::Duration; - +use crate::connector::expect_connector; +use crate::imds::client::error::{ + BuildError, BuildErrorKind, ImdsError, InnerImdsError, InvalidEndpointMode, +}; +use crate::imds::client::token::TokenMiddleware; +use crate::provider_config::ProviderConfig; +use crate::{profile, PKG_VERSION}; use aws_http::user_agent::{ApiMetadata, AwsUserAgent, UserAgentStage}; +use aws_sdk_sso::config::timeout::TimeoutConfig; +use aws_smithy_client::http_connector::ConnectorSettings; use aws_smithy_client::{erase::DynConnector, SdkSuccess}; use aws_smithy_client::{retry, SdkError}; use aws_smithy_http::body::SdkBody; @@ -30,20 +31,16 @@ use aws_smithy_http_tower::map_request::{ use aws_smithy_types::error::display::DisplayErrorContext; use aws_smithy_types::retry::{ErrorKind, RetryKind}; use aws_types::os_shim_internal::{Env, Fs}; - use bytes::Bytes; -use http::uri::InvalidUri; use http::{Response, Uri}; +use std::borrow::Cow; +use std::error::Error; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; use tokio::sync::OnceCell; -use crate::connector::expect_connector; -use crate::imds::client::token::TokenMiddleware; -use crate::profile::credentials::ProfileFileError; -use crate::provider_config::ProviderConfig; -use crate::{profile, PKG_VERSION}; -use aws_sdk_sso::config::timeout::TimeoutConfig; -use aws_smithy_client::http_connector::ConnectorSettings; - +pub mod error; mod token; // 6 hours @@ -209,23 +206,23 @@ impl Client { SdkError::ConstructionFailure(_) if err.source().is_some() => { match err.into_source().map(|e| e.downcast::()) { Ok(Ok(token_failure)) => *token_failure, - Ok(Err(err)) => ImdsError::Unexpected(err), - Err(err) => ImdsError::Unexpected(err.into()), + Ok(Err(err)) => ImdsError::unexpected(err), + Err(err) => ImdsError::unexpected(err), } } - SdkError::ConstructionFailure(_) => ImdsError::Unexpected(err.into()), + SdkError::ConstructionFailure(_) => ImdsError::unexpected(err), SdkError::ServiceError(context) => match context.err() { InnerImdsError::InvalidUtf8 => { - ImdsError::Unexpected("IMDS returned invalid UTF-8".into()) + ImdsError::unexpected("IMDS returned invalid UTF-8") + } + InnerImdsError::BadStatus => { + ImdsError::error_response(context.into_raw().into_parts().0) } - InnerImdsError::BadStatus => ImdsError::ErrorResponse { - response: context.into_raw().into_parts().0, - }, }, SdkError::TimeoutError(_) | SdkError::DispatchFailure(_) - | SdkError::ResponseError(_) => ImdsError::IoError(err.into()), - _ => ImdsError::Unexpected(err.into()), + | SdkError::ResponseError(_) => ImdsError::io_error(err), + _ => ImdsError::unexpected(err), }) } @@ -237,7 +234,9 @@ impl Client { &self, path: &str, ) -> Result, ImdsError> { - let mut base_uri: Uri = path.parse().map_err(|_| ImdsError::InvalidPath)?; + let mut base_uri: Uri = path.parse().map_err(|_| { + ImdsError::unexpected("IMDS path was not a valid URI. Hint: does it begin with `/`?") + })?; self.inner.endpoint.set_endpoint(&mut base_uri, None); let request = http::Request::builder() .uri(base_uri) @@ -251,74 +250,6 @@ impl Client { } } -/// An error retrieving metadata from IMDS -#[derive(Debug)] -#[non_exhaustive] -pub enum ImdsError { - /// An IMDSv2 Token could not be loaded - /// - /// Requests to IMDS must be accompanied by a token obtained via a `PUT` request. This is handled - /// transparently by the [`Client`]. - FailedToLoadToken(SdkError), - - /// The `path` was invalid for an IMDS request - /// - /// The `path` parameter must be a valid URI path segment, and it must begin with `/`. - InvalidPath, - - /// An error response was returned from IMDS - #[non_exhaustive] - ErrorResponse { - /// The returned raw response - response: http::Response, - }, - - /// IO Error - /// - /// An error occurred communication with IMDS - IoError(Box), - - /// An unexpected error occurred communicating with IMDS - Unexpected(Box), -} - -impl Display for ImdsError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ImdsError::FailedToLoadToken(inner) => { - write!(f, "Failed to load session token: {}", inner) - } - ImdsError::InvalidPath => write!( - f, - "IMDS path was not a valid URI. Hint: Does it begin with `/`?" - ), - ImdsError::ErrorResponse { response } => write!( - f, - "Error response from IMDS (code: {}). {:?}", - response.status().as_u16(), - response - ), - ImdsError::IoError(err) => { - write!(f, "An IO error occurred communicating with IMDS: {}", err) - } - ImdsError::Unexpected(err) => write!( - f, - "An unexpected error occurred communicating with IMDS: {}", - err - ), - } - } -} - -impl Error for ImdsError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match &self { - ImdsError::FailedToLoadToken(inner) => Some(inner), - _ => None, - } - } -} - /// IMDS Middleware /// /// The IMDS middleware includes a token-loader & a UserAgent stage @@ -339,23 +270,6 @@ impl tower::Layer for ImdsMiddleware { #[derive(Copy, Clone)] struct ImdsGetResponseHandler; -#[derive(Debug)] -enum InnerImdsError { - BadStatus, - InvalidUtf8, -} - -impl Display for InnerImdsError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - InnerImdsError::BadStatus => write!(f, "failing status code returned from IMDS"), - InnerImdsError::InvalidUtf8 => write!(f, "IMDS did not return valid UTF-8"), - } - } -} - -impl Error for InnerImdsError {} - impl ParseStrictResponse for ImdsGetResponseHandler { type Output = Result; @@ -386,22 +300,6 @@ pub enum EndpointMode { IpV6, } -/// Invalid Endpoint Mode -#[derive(Debug, Clone)] -pub struct InvalidEndpointMode(String); - -impl Display for InvalidEndpointMode { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "`{}` is not a valid endpoint mode. Valid values are [`IPv4`, `IPv6`]", - &self.0 - ) - } -} - -impl Error for InvalidEndpointMode {} - impl FromStr for EndpointMode { type Err = InvalidEndpointMode; @@ -409,7 +307,7 @@ impl FromStr for EndpointMode { match value { _ if value.eq_ignore_ascii_case("ipv4") => Ok(EndpointMode::IpV4), _ if value.eq_ignore_ascii_case("ipv6") => Ok(EndpointMode::IpV6), - other => Err(InvalidEndpointMode(other.to_owned())), + other => Err(InvalidEndpointMode::new(other.to_owned())), } } } @@ -436,40 +334,6 @@ pub struct Builder { config: Option, } -/// Error constructing IMDSv2 Client -#[derive(Debug)] -pub enum BuildError { - /// The endpoint mode was invalid - InvalidEndpointMode(InvalidEndpointMode), - - /// The AWS Profile (e.g. `~/.aws/config`) was invalid - InvalidProfile(ProfileFileError), - - /// The specified endpoint was not a valid URI - InvalidEndpointUri(InvalidUri), -} - -impl Display for BuildError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "failed to build IMDS client: ")?; - match self { - BuildError::InvalidEndpointMode(e) => write!(f, "{}", e), - BuildError::InvalidProfile(e) => write!(f, "{}", e), - BuildError::InvalidEndpointUri(e) => write!(f, "{}", e), - } - } -} - -impl Error for BuildError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - BuildError::InvalidEndpointMode(e) => Some(e), - BuildError::InvalidProfile(e) => Some(e), - BuildError::InvalidEndpointUri(e) => Some(e), - } - } -} - impl Builder { /// Override the number of retries for fetching tokens & metadata /// @@ -632,14 +496,16 @@ impl EndpointSource { // load an endpoint override from the environment let profile = profile::load(fs, env, &Default::default()) .await - .map_err(BuildError::InvalidProfile)?; + .map_err(BuildErrorKind::InvalidProfile)?; let uri_override = if let Ok(uri) = env.get(env::ENDPOINT) { Some(Cow::Owned(uri)) } else { profile.get(profile_keys::ENDPOINT).map(Cow::Borrowed) }; if let Some(uri) = uri_override { - return Uri::try_from(uri.as_ref()).map_err(BuildError::InvalidEndpointUri); + return Ok( + Uri::try_from(uri.as_ref()).map_err(BuildErrorKind::InvalidEndpointUri)? + ); } // if not, load a endpoint mode from the environment @@ -647,10 +513,10 @@ impl EndpointSource { mode } else if let Ok(mode) = env.get(env::ENDPOINT_MODE) { mode.parse::() - .map_err(BuildError::InvalidEndpointMode)? + .map_err(BuildErrorKind::InvalidEndpointMode)? } else if let Some(mode) = profile.get(profile_keys::ENDPOINT_MODE) { mode.parse::() - .map_err(BuildError::InvalidEndpointMode)? + .map_err(BuildErrorKind::InvalidEndpointMode)? } else { EndpointMode::IpV4 }; @@ -661,54 +527,6 @@ impl EndpointSource { } } -/// Error retrieving token from IMDS -#[derive(Debug)] -pub enum TokenError { - /// The token was invalid - /// - /// Because tokens must be eventually sent as a header, the token must be a valid header value. - InvalidToken, - - /// No TTL was sent - /// - /// The token response must include a time-to-live indicating the lifespan of the token. - NoTtl, - - /// The TTL was invalid - /// - /// The TTL must be a valid positive integer. - InvalidTtl, - - /// Invalid Parameters - /// - /// The request to load a token was malformed. This indicates an SDK bug. - InvalidParameters, - - /// Forbidden - /// - /// IMDS is disabled or has been disallowed via permissions. - Forbidden, -} - -impl Display for TokenError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - TokenError::InvalidToken => write!(f, "Invalid Token"), - TokenError::NoTtl => write!(f, "Token response did not contain a TTL header"), - TokenError::InvalidTtl => write!(f, "The returned TTL was invalid"), - TokenError::InvalidParameters => { - write!(f, "Invalid request parameters. This indicates an SDK bug.") - } - TokenError::Forbidden => write!( - f, - "Request forbidden: IMDS is disabled or the caller has insufficient permissions." - ), - } - } -} - -impl Error for TokenError {} - #[derive(Clone)] struct ImdsResponseRetryClassifier; @@ -1086,7 +904,7 @@ pub(crate) mod test { )]); let client = make_client(&connection).await; let err = client.get("/latest/metadata").await.expect_err("no token"); - assert_full_error_contains!(err, "Invalid Token"); + assert_full_error_contains!(err, "invalid token"); connection.assert_requests_match(&[]); } @@ -1142,7 +960,7 @@ pub(crate) mod test { time_elapsed ); match resp { - ImdsError::FailedToLoadToken(err) + err @ ImdsError::FailedToLoadToken(_) if format!("{}", DisplayErrorContext(&err)).contains("timeout") => {} // ok, other => panic!( "wrong error, expected construction failure with TimedOutError inside: {}", diff --git a/aws/rust-runtime/aws-config/src/imds/client/error.rs b/aws/rust-runtime/aws-config/src/imds/client/error.rs new file mode 100644 index 0000000000..f1845feb71 --- /dev/null +++ b/aws/rust-runtime/aws-config/src/imds/client/error.rs @@ -0,0 +1,281 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Error types for [`ImdsClient`](crate::imds::client::Client) + +use crate::profile::credentials::ProfileFileError; +use aws_smithy_client::SdkError; +use aws_smithy_http::body::SdkBody; +use http::uri::InvalidUri; +use std::error::Error; +use std::fmt; + +/// Error context for [`ImdsError::FailedToLoadToken`] +#[derive(Debug)] +pub struct FailedToLoadToken { + source: SdkError, +} + +impl FailedToLoadToken { + /// Returns `true` if a dispatch failure caused the token to fail to load + pub fn is_dispatch_failure(&self) -> bool { + matches!(self.source, SdkError::DispatchFailure(_)) + } + + pub(crate) fn into_source(self) -> SdkError { + self.source + } +} + +/// Error context for [`ImdsError::ErrorResponse`] +#[derive(Debug)] +pub struct ErrorResponse { + raw: http::Response, +} + +impl ErrorResponse { + /// Returns the raw response from IMDS + pub fn response(&self) -> &http::Response { + &self.raw + } +} + +/// Error context for [`ImdsError::IoError`] +#[derive(Debug)] +pub struct IoError { + source: Box, +} + +/// Error context for [`ImdsError::Unexpected`] +#[derive(Debug)] +pub struct Unexpected { + source: Box, +} + +/// An error retrieving metadata from IMDS +#[derive(Debug)] +#[non_exhaustive] +pub enum ImdsError { + /// An IMDSv2 Token could not be loaded + /// + /// Requests to IMDS must be accompanied by a token obtained via a `PUT` request. This is handled + /// transparently by the [`Client`](crate::imds::client::Client). + FailedToLoadToken(FailedToLoadToken), + + /// An error response was returned from IMDS + ErrorResponse(ErrorResponse), + + /// IO Error + /// + /// An error occurred communication with IMDS + IoError(IoError), + + /// An unexpected error occurred communicating with IMDS + Unexpected(Unexpected), +} + +impl ImdsError { + pub(super) fn failed_to_load_token(source: SdkError) -> Self { + Self::FailedToLoadToken(FailedToLoadToken { source }) + } + + pub(super) fn error_response(raw: http::Response) -> Self { + Self::ErrorResponse(ErrorResponse { raw }) + } + + pub(super) fn io_error(source: impl Into>) -> Self { + Self::IoError(IoError { + source: source.into(), + }) + } + + pub(super) fn unexpected(source: impl Into>) -> Self { + Self::Unexpected(Unexpected { + source: source.into(), + }) + } +} + +impl fmt::Display for ImdsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ImdsError::FailedToLoadToken(_) => { + write!(f, "failed to load IMDS session token") + } + ImdsError::ErrorResponse(context) => write!( + f, + "error response from IMDS (code: {}). {:?}", + context.raw.status().as_u16(), + context.raw + ), + ImdsError::IoError(_) => { + write!(f, "an IO error occurred communicating with IMDS") + } + ImdsError::Unexpected(_) => { + write!(f, "an unexpected error occurred communicating with IMDS",) + } + } + } +} + +impl Error for ImdsError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match &self { + ImdsError::FailedToLoadToken(context) => Some(&context.source), + ImdsError::IoError(context) => Some(context.source.as_ref()), + ImdsError::Unexpected(context) => Some(context.source.as_ref()), + ImdsError::ErrorResponse(_) => None, + } + } +} + +#[derive(Debug)] +pub(super) enum InnerImdsError { + BadStatus, + InvalidUtf8, +} + +impl fmt::Display for InnerImdsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InnerImdsError::BadStatus => write!(f, "failing status code returned from IMDS"), + InnerImdsError::InvalidUtf8 => write!(f, "IMDS did not return valid UTF-8"), + } + } +} + +impl Error for InnerImdsError {} + +/// Invalid Endpoint Mode +#[derive(Debug)] +pub struct InvalidEndpointMode { + mode: String, +} + +impl InvalidEndpointMode { + pub(super) fn new(mode: impl Into) -> Self { + Self { mode: mode.into() } + } +} + +impl fmt::Display for InvalidEndpointMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "`{}` is not a valid endpoint mode. Valid values are [`IPv4`, `IPv6`]", + &self.mode + ) + } +} + +impl Error for InvalidEndpointMode {} + +#[derive(Debug)] +#[allow(clippy::enum_variant_names)] +pub(super) enum BuildErrorKind { + /// The endpoint mode was invalid + InvalidEndpointMode(InvalidEndpointMode), + + /// The AWS Profile (e.g. `~/.aws/config`) was invalid + InvalidProfile(ProfileFileError), + + /// The specified endpoint was not a valid URI + InvalidEndpointUri(InvalidUri), +} + +/// Error constructing IMDSv2 Client +#[derive(Debug)] +pub struct BuildError { + kind: BuildErrorKind, +} + +impl fmt::Display for BuildError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + use BuildErrorKind::*; + write!(f, "failed to build IMDS client: ")?; + match self.kind { + InvalidEndpointMode(_) => write!(f, "invalid endpoint mode"), + InvalidProfile(_) => write!(f, "profile file error"), + InvalidEndpointUri(_) => write!(f, "invalid URI"), + } + } +} + +impl Error for BuildError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + use BuildErrorKind::*; + match &self.kind { + InvalidEndpointMode(e) => Some(e), + InvalidProfile(e) => Some(e), + InvalidEndpointUri(e) => Some(e), + } + } +} + +impl From for BuildError { + fn from(kind: BuildErrorKind) -> Self { + Self { kind } + } +} + +#[derive(Debug)] +pub(super) enum TokenErrorKind { + /// The token was invalid + /// + /// Because tokens must be eventually sent as a header, the token must be a valid header value. + InvalidToken, + + /// No TTL was sent + /// + /// The token response must include a time-to-live indicating the lifespan of the token. + NoTtl, + + /// The TTL was invalid + /// + /// The TTL must be a valid positive integer. + InvalidTtl, + + /// Invalid Parameters + /// + /// The request to load a token was malformed. This indicates an SDK bug. + InvalidParameters, + + /// Forbidden + /// + /// IMDS is disabled or has been disallowed via permissions. + Forbidden, +} + +/// Error retrieving token from IMDS +#[derive(Debug)] +pub struct TokenError { + kind: TokenErrorKind, +} + +impl fmt::Display for TokenError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use TokenErrorKind::*; + match self.kind { + InvalidToken => write!(f, "invalid token"), + NoTtl => write!(f, "token response did not contain a TTL header"), + InvalidTtl => write!(f, "the returned TTL was invalid"), + InvalidParameters => { + write!(f, "invalid request parameters. This indicates an SDK bug.") + } + Forbidden => write!( + f, + "request forbidden: IMDS is disabled or the caller has insufficient permissions." + ), + } + } +} + +impl Error for TokenError {} + +impl From for TokenError { + fn from(kind: TokenErrorKind) -> Self { + Self { kind } + } +} diff --git a/aws/rust-runtime/aws-config/src/imds/client/token.rs b/aws/rust-runtime/aws-config/src/imds/client/token.rs index 4cf6f299fa..e3f572e1e9 100644 --- a/aws/rust-runtime/aws-config/src/imds/client/token.rs +++ b/aws/rust-runtime/aws-config/src/imds/client/token.rs @@ -14,13 +14,11 @@ //! - Retry token loading when it fails //! - Attach the token to the request in the `x-aws-ec2-metadata-token` header -use std::fmt::{Debug, Formatter}; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; -use std::time::{Duration, SystemTime}; - +use crate::cache::ExpiringCache; +use crate::imds::client::error::{ImdsError, TokenError, TokenErrorKind}; +use crate::imds::client::ImdsResponseRetryClassifier; use aws_http::user_agent::UserAgentStage; +use aws_sdk_sso::config::timeout::TimeoutConfig; use aws_smithy_async::rt::sleep::AsyncSleep; use aws_smithy_client::erase::DynConnector; use aws_smithy_client::retry; @@ -33,12 +31,12 @@ use aws_smithy_http::operation::{Metadata, Request}; use aws_smithy_http::response::ParseStrictResponse; use aws_smithy_http_tower::map_request::MapRequestLayer; use aws_types::os_shim_internal::TimeSource; - use http::{HeaderValue, Uri}; - -use crate::cache::ExpiringCache; -use crate::imds::client::{ImdsError, ImdsResponseRetryClassifier, TokenError}; -use aws_sdk_sso::config::timeout::TimeoutConfig; +use std::fmt::{Debug, Formatter}; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::time::{Duration, SystemTime}; /// Token Refresh Buffer /// @@ -150,7 +148,7 @@ impl TokenMiddleware { .client .call(operation) .await - .map_err(ImdsError::FailedToLoadToken)?; + .map_err(ImdsError::failed_to_load_token)?; let expiry = response.expiry; Ok((response, expiry)) } @@ -176,20 +174,20 @@ impl ParseStrictResponse for GetTokenResponseHandler { fn parse(&self, response: &http::Response) -> Self::Output { match response.status().as_u16() { - 400 => return Err(TokenError::InvalidParameters), - 403 => return Err(TokenError::Forbidden), + 400 => return Err(TokenErrorKind::InvalidParameters.into()), + 403 => return Err(TokenErrorKind::Forbidden.into()), _ => {} } let value = HeaderValue::from_maybe_shared(response.body().clone()) - .map_err(|_| TokenError::InvalidToken)?; + .map_err(|_| TokenErrorKind::InvalidToken)?; let ttl: u64 = response .headers() .get(X_AWS_EC2_METADATA_TOKEN_TTL_SECONDS) - .ok_or(TokenError::NoTtl)? + .ok_or(TokenErrorKind::NoTtl)? .to_str() - .map_err(|_| TokenError::InvalidTtl)? + .map_err(|_| TokenErrorKind::InvalidTtl)? .parse() - .map_err(|_parse_error| TokenError::InvalidTtl)?; + .map_err(|_parse_error| TokenErrorKind::InvalidTtl)?; Ok(Token { value, expiry: self.time.now() + Duration::from_secs(ttl), diff --git a/aws/rust-runtime/aws-config/src/imds/credentials.rs b/aws/rust-runtime/aws-config/src/imds/credentials.rs index 3100fca568..7c4e3eb8e3 100644 --- a/aws/rust-runtime/aws-config/src/imds/credentials.rs +++ b/aws/rust-runtime/aws-config/src/imds/credentials.rs @@ -8,11 +8,11 @@ //! # Important //! This credential provider will NOT fallback to IMDSv1. Ensure that IMDSv2 is enabled on your instances. +use super::client::error::ImdsError; use crate::imds; -use crate::imds::client::{ImdsError, LazyClient}; +use crate::imds::client::LazyClient; use crate::json_credentials::{parse_json_credentials, JsonCredentials, RefreshableCredentials}; use crate::provider_config::ProviderConfig; -use aws_smithy_client::SdkError; use aws_types::credentials::{future, CredentialsError, ProvideCredentials}; use aws_types::os_shim_internal::Env; use aws_types::{credentials, Credentials}; @@ -149,16 +149,18 @@ impl ImdsCredentialsProvider { .await { Ok(profile) => Ok(profile), - Err(ImdsError::ErrorResponse { response, .. }) if response.status().as_u16() == 404 => { + Err(ImdsError::ErrorResponse(context)) + if context.response().status().as_u16() == 404 => + { tracing::info!( "received 404 from IMDS when loading profile information. \ Hint: This instance may not have an IAM role associated." ); Err(CredentialsError::not_loaded("received 404 from IMDS")) } - Err(ImdsError::FailedToLoadToken(err @ SdkError::DispatchFailure(_))) => { + Err(ImdsError::FailedToLoadToken(context)) if context.is_dispatch_failure() => { Err(CredentialsError::not_loaded(ImdsCommunicationError { - source: err.into(), + source: context.into_source().into(), })) } Err(other) => Err(CredentialsError::provider_error(other)), diff --git a/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_token_fail/test-case.json b/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_token_fail/test-case.json index d9af22e6d5..b4e7ca9090 100644 --- a/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_token_fail/test-case.json +++ b/aws/rust-runtime/aws-config/test-data/default-provider-chain/imds_token_fail/test-case.json @@ -2,6 +2,6 @@ "name": "imds-token-fail", "docs": "attempts to acquire an IMDS token, but a 403 is returned", "result": { - "ErrorContains": "Request forbidden: IMDS is disabled" + "ErrorContains": "request forbidden: IMDS is disabled" } }