diff --git a/aws/rust-runtime/auth/Cargo.toml b/aws/rust-runtime/aws-auth/Cargo.toml similarity index 91% rename from aws/rust-runtime/auth/Cargo.toml rename to aws/rust-runtime/aws-auth/Cargo.toml index 58b767fe12..878c8e4c8a 100644 --- a/aws/rust-runtime/auth/Cargo.toml +++ b/aws/rust-runtime/aws-auth/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "auth" +name = "aws-auth" version = "0.1.0" authors = ["Russell Cohen "] edition = "2018" diff --git a/aws/rust-runtime/auth/src/lib.rs b/aws/rust-runtime/aws-auth/src/lib.rs similarity index 94% rename from aws/rust-runtime/auth/src/lib.rs rename to aws/rust-runtime/aws-auth/src/lib.rs index 0ea4c3e6cd..8f752580ce 100644 --- a/aws/rust-runtime/auth/src/lib.rs +++ b/aws/rust-runtime/aws-auth/src/lib.rs @@ -1,9 +1,9 @@ pub mod provider; -use std::time::SystemTime; use std::error::Error; -use std::fmt::{Display, Formatter, Debug}; use std::fmt; +use std::fmt::{Debug, Display, Formatter}; +use std::time::SystemTime; /// AWS SDK Credentials /// @@ -50,7 +50,7 @@ impl Credentials { session_token, expires_after: None, - provider_name: STATIC_CREDENTIALS + provider_name: STATIC_CREDENTIALS, } } } @@ -59,14 +59,14 @@ impl Credentials { #[non_exhaustive] pub enum CredentialsError { CredentialsNotLoaded, - Unhandled(Box) + Unhandled(Box), } impl Display for CredentialsError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { CredentialsError::CredentialsNotLoaded => write!(f, "CredentialsNotLoaded"), - CredentialsError::Unhandled(err) => write!(f, "{}", err) + CredentialsError::Unhandled(err) => write!(f, "{}", err), } } } @@ -75,7 +75,7 @@ impl Error for CredentialsError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { CredentialsError::Unhandled(e) => Some(e.as_ref() as _), - _ => None + _ => None, } } } diff --git a/aws/rust-runtime/auth/src/provider.rs b/aws/rust-runtime/aws-auth/src/provider.rs similarity index 87% rename from aws/rust-runtime/auth/src/provider.rs rename to aws/rust-runtime/aws-auth/src/provider.rs index 066929f4b2..1cb088acb8 100644 --- a/aws/rust-runtime/auth/src/provider.rs +++ b/aws/rust-runtime/aws-auth/src/provider.rs @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0. */ -use crate::{ProvideCredentials, Credentials, CredentialsError}; -use std::env::VarError; +use crate::{Credentials, CredentialsError, ProvideCredentials}; use std::collections::HashMap; +use std::env::VarError; /// Load Credentials from Environment Variables pub struct EnvironmentVariableCredentialsProvider { - env: Box Result + Send + Sync> + env: Box Result + Send + Sync>, } impl EnvironmentVariableCredentialsProvider { @@ -21,8 +21,10 @@ impl EnvironmentVariableCredentialsProvider { fn for_map(env: HashMap) -> Self { EnvironmentVariableCredentialsProvider { env: Box::new(move |key: &str| { - env.get(key).ok_or(VarError::NotPresent).map(|k|k.to_string()) - }) + env.get(key) + .ok_or(VarError::NotPresent) + .map(|k| k.to_string()) + }), } } } @@ -36,15 +38,16 @@ const ENV_PROVIDER: &'static str = "EnvironmentVariable"; impl ProvideCredentials for EnvironmentVariableCredentialsProvider { fn credentials(&self) -> Result { let access_key = (self.env)("AWS_ACCESS_KEY_ID").map_err(to_cred_error)?; - let secret_key = - (self.env)("AWS_SECRET_ACCESS_KEY").or_else(|_|(self.env)("SECRET_ACCESS_KEY")).map_err(to_cred_error)?; + let secret_key = (self.env)("AWS_SECRET_ACCESS_KEY") + .or_else(|_| (self.env)("SECRET_ACCESS_KEY")) + .map_err(to_cred_error)?; let session_token = (self.env)("AWS_SESSION_TOKEN").ok(); Ok(Credentials { access_key_id: access_key, secret_access_key: secret_key, session_token, expires_after: None, - provider_name: ENV_PROVIDER + provider_name: ENV_PROVIDER, }) } } @@ -52,15 +55,15 @@ impl ProvideCredentials for EnvironmentVariableCredentialsProvider { fn to_cred_error(err: VarError) -> CredentialsError { match err { VarError::NotPresent => CredentialsError::CredentialsNotLoaded, - e @ VarError::NotUnicode(_) => CredentialsError::Unhandled(Box::new(e)) + e @ VarError::NotUnicode(_) => CredentialsError::Unhandled(Box::new(e)), } } #[cfg(test)] mod test { use crate::provider::EnvironmentVariableCredentialsProvider; + use crate::{CredentialsError, ProvideCredentials}; use std::collections::HashMap; - use crate::{ProvideCredentials, CredentialsError}; #[test] fn valid_no_token() { @@ -101,7 +104,6 @@ mod test { assert_eq!(creds.session_token.unwrap(), "token"); assert_eq!(creds.access_key_id, "access"); assert_eq!(creds.secret_access_key, "secret"); - } #[test] @@ -110,8 +112,8 @@ mod test { let provider = EnvironmentVariableCredentialsProvider::for_map(env); let err = provider.credentials().expect_err("no credentials defined"); match err { - CredentialsError::Unhandled(_ ) => panic!("wrong error type"), - _ => () + CredentialsError::Unhandled(_) => panic!("wrong error type"), + _ => (), }; } diff --git a/aws/rust-runtime/aws-endpoint/Cargo.toml b/aws/rust-runtime/aws-endpoint/Cargo.toml new file mode 100644 index 0000000000..e7f842e0bf --- /dev/null +++ b/aws/rust-runtime/aws-endpoint/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "aws-endpoint" +version = "0.1.0" +authors = ["Russell Cohen "] +edition = "2018" +description = "AWS Endpoint Support" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +smithy-http = { path = "../../../rust-runtime/smithy-http"} +aws-types = { path = "../aws-types" } +http = "0.2.3" diff --git a/aws/rust-runtime/aws-endpoint/src/lib.rs b/aws/rust-runtime/aws-endpoint/src/lib.rs new file mode 100644 index 0000000000..d49ef0c3bf --- /dev/null +++ b/aws/rust-runtime/aws-endpoint/src/lib.rs @@ -0,0 +1,202 @@ +use aws_types::{Region, SigningRegion, SigningService}; +use http::Uri; +use smithy_http::endpoint::{Endpoint, EndpointPrefix}; +use smithy_http::middleware::MapRequest; +use smithy_http::operation::Request; +use smithy_http::property_bag::PropertyBag; +use std::error::Error; +use std::fmt; +use std::fmt::{Debug, Display, Formatter}; +use std::str::FromStr; +use std::sync::Arc; + +/// Endpoint to connect to an AWS Service +/// +/// An `AwsEndpoint` captures all necessary information needed to connect to an AWS service, including: +/// - The URI of the endpoint (needed to actually send the request) +/// - The name of the service (needed downstream for signing) +/// - The signing region (which may differ from the actual region) +#[derive(Clone)] +pub struct AwsEndpoint { + endpoint: Endpoint, + signing_service: Option, + signing_region: Option, +} + +pub type BoxError = Box; + +/// Resolve the AWS Endpoint for a given region +/// +/// To provide a static endpoint, [`Endpoint`](smithy_http::endpoint::Endpoint) implements this trait. +/// Example usage: +/// ```rust +/// # mod dynamodb { +/// # use aws_endpoint::ResolveAwsEndpoint; +/// # pub struct ConfigBuilder; +/// # impl ConfigBuilder { +/// # pub fn endpoint(&mut self, resolver: impl ResolveAwsEndpoint + 'static) { +/// # // ... +/// # } +/// # } +/// # pub struct Config; +/// # impl Config { +/// # pub fn builder() -> ConfigBuilder { +/// # ConfigBuilder +/// # } +/// # } +/// # } +/// use smithy_http::endpoint::Endpoint; +/// use http::Uri; +/// let config = dynamodb::Config::builder() +/// .endpoint( +/// Endpoint::immutable(Uri::from_static("http://localhost:8080")) +/// ); +/// ``` +/// In the future, each AWS service will generate their own implementation of `ResolveAwsEndpoint`. This implementation +/// may use endpoint discovery. The list of supported regions for a given service +/// will be codegenerated from `endpoints.json`. +pub trait ResolveAwsEndpoint: Send + Sync { + // TODO: consider if we want modeled error variants here + fn endpoint(&self, region: &Region) -> Result; +} + +/// Default AWS Endpoint Implementation +/// +/// This is used as a temporary stub. Prior to GA, this will be replaced with specifically generated endpoint +/// resolvers for each service that model the endpoints for each service correctly. Some services differ +/// from the standard endpoint pattern. +pub struct DefaultAwsEndpointResolver { + service: &'static str, +} + +impl DefaultAwsEndpointResolver { + pub fn for_service(service: &'static str) -> Self { + Self { service } + } +} + +/// An `Endpoint` can be its own resolver to support static endpoints +impl ResolveAwsEndpoint for Endpoint { + fn endpoint(&self, _region: &Region) -> Result { + Ok(AwsEndpoint { + endpoint: self.clone(), + signing_service: None, + signing_region: None, + }) + } +} + +impl ResolveAwsEndpoint for DefaultAwsEndpointResolver { + fn endpoint(&self, region: &Region) -> Result { + let uri = Uri::from_str(&format!( + "https://{}.{}.amazonaws.com", + region.as_ref(), + self.service + ))?; + Ok(AwsEndpoint { + endpoint: Endpoint::mutable(uri), + signing_region: Some(region.clone().into()), + signing_service: Some(SigningService::from_static(self.service)), + }) + } +} + +type AwsEndpointResolver = Arc; +fn get_endpoint_resolver(config: &PropertyBag) -> Option<&AwsEndpointResolver> { + config.get() +} + +pub fn set_endpoint_resolver(provider: AwsEndpointResolver, config: &mut PropertyBag) { + config.insert(provider); +} + +/// Middleware Stage to Add an Endpoint to a Request +/// +/// AwsEndpointStage implements [`MapRequest`](smithy_http::middleware::MapRequest). It will: +/// 1. Load an endpoint provider from the property bag. +/// 2. Load an endpoint given the [`Region`](aws_types::Region) in the property bag. +/// 3. Apply the endpoint to the URI in the request +/// 4. Set the `SigningRegion` and `SigningService` in the property bag to drive downstream +/// signing middleware. +pub struct AwsEndpointStage; + +#[derive(Debug)] +pub enum AwsEndpointStageError { + NoEndpointResolver, + NoRegion, + EndpointResolutionError(BoxError), +} + +impl Display for AwsEndpointStageError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(self, f) + } +} +impl Error for AwsEndpointStageError {} + +impl MapRequest for AwsEndpointStage { + type Error = AwsEndpointStageError; + + fn apply(&self, request: Request) -> Result { + request.augment(|mut http_req, config| { + let provider = + get_endpoint_resolver(config).ok_or(AwsEndpointStageError::NoEndpointResolver)?; + let region = config + .get::() + .ok_or(AwsEndpointStageError::NoRegion)?; + let endpoint = provider + .endpoint(region) + .map_err(AwsEndpointStageError::EndpointResolutionError)?; + let signing_region = endpoint + .signing_region + .unwrap_or_else(|| region.clone().into()); + config.insert::(signing_region); + if let Some(signing_service) = endpoint.signing_service { + config.insert::(signing_service); + } + endpoint + .endpoint + .set_endpoint(http_req.uri_mut(), config.get::()); + Ok(http_req) + }) + } +} + +#[cfg(test)] +mod test { + use crate::{set_endpoint_resolver, AwsEndpointStage, DefaultAwsEndpointResolver}; + use aws_types::{Region, SigningRegion, SigningService}; + use http::Uri; + use smithy_http::body::SdkBody; + use smithy_http::middleware::MapRequest; + use smithy_http::operation; + use std::sync::Arc; + + #[test] + fn default_endpoint_updates_request() { + let provider = Arc::new(DefaultAwsEndpointResolver::for_service("kinesis")); + let req = http::Request::new(SdkBody::from("")); + let region = Region::new("us-east-1"); + let mut req = operation::Request::new(req); + { + let mut conf = req.config_mut(); + conf.insert(region.clone()); + set_endpoint_resolver(provider, &mut conf); + }; + let req = AwsEndpointStage.apply(req).expect("should succeed"); + assert_eq!( + req.config().get(), + Some(&SigningRegion::from(region.clone())) + ); + assert_eq!( + req.config().get(), + Some(&SigningService::from_static("kinesis")) + ); + + let (req, _conf) = req.into_parts(); + assert_eq!( + req.uri(), + &Uri::from_static("https://us-east-1.kinesis.amazonaws.com") + ); + } +} diff --git a/aws/rust-runtime/aws-types/Cargo.toml b/aws/rust-runtime/aws-types/Cargo.toml new file mode 100644 index 0000000000..3011c31d09 --- /dev/null +++ b/aws/rust-runtime/aws-types/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "aws-types" +version = "0.1.0" +authors = ["Russell Cohen "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/aws/rust-runtime/aws-types/src/lib.rs b/aws/rust-runtime/aws-types/src/lib.rs new file mode 100644 index 0000000000..cedfd81dcf --- /dev/null +++ b/aws/rust-runtime/aws-types/src/lib.rs @@ -0,0 +1,58 @@ +use std::borrow::Cow; +use std::sync::Arc; + +/// The region to send requests to. +/// +/// The region MUST be specified on a request. It may be configured globally or on a +/// per-client basis unless otherwise noted. A full list of regions is found in the +/// "Regions and Endpoints" document. +/// +/// See http://docs.aws.amazon.com/general/latest/gr/rande.html for +/// information on AWS regions. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Region(Arc); +impl AsRef for Region { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl Region { + pub fn new(region: impl Into) -> Self { + Self(Arc::new(region.into())) + } +} + +/// The region to use when signing requests +/// +/// Generally, user code will not need to interact with `SigningRegion`. See `[Region](crate::Region)`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SigningRegion(Arc); +impl AsRef for SigningRegion { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl From for SigningRegion { + fn from(inp: Region) -> Self { + SigningRegion(inp.0) + } +} + +/// The name of the service used to sign this request +/// +/// Generally, user code should never interact with `SigningService` directly +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SigningService(Cow<'static, str>); +impl AsRef for SigningService { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl SigningService { + pub fn from_static(service: &'static str) -> Self { + SigningService(Cow::Borrowed(service)) + } +} diff --git a/aws/rust-runtime/test.sh b/aws/rust-runtime/test.sh old mode 100644 new mode 100755 diff --git a/rust-runtime/smithy-http/src/endpoint.rs b/rust-runtime/smithy-http/src/endpoint.rs index c4eb3fd8fd..0272181e09 100644 --- a/rust-runtime/smithy-http/src/endpoint.rs +++ b/rust-runtime/smithy-http/src/endpoint.rs @@ -10,6 +10,7 @@ use std::str::FromStr; /// /// This implements an API endpoint as specified in the /// [Smithy Endpoint Specification](https://awslabs.github.io/smithy/1.0/spec/core/endpoint-traits.html) +#[derive(Clone)] pub struct Endpoint { uri: http::Uri, @@ -41,8 +42,11 @@ impl Endpoint { /// /// Certain protocols will attempt to prefix additional information onto an endpoint. If you /// wish to ignore these prefixes (for example, when communicating with localhost), set `immutable` to `true`. - pub fn new(uri: Uri, immutable: bool) -> Result { - Ok(Endpoint { uri, immutable }) + pub fn mutable(uri: Uri) -> Self { + Endpoint { + uri, + immutable: false, + } } /// Create a new immutable endpoint from a URI @@ -50,9 +54,9 @@ impl Endpoint { /// ```rust /// # use smithy_http::endpoint::Endpoint; /// use http::Uri; - /// let endpoint = Endpoint::from_uri(Uri::from_static("http://localhost:8000")); + /// let endpoint = Endpoint::immutable(Uri::from_static("http://localhost:8000")); /// ``` - pub fn from_uri(uri: Uri) -> Self { + pub fn immutable(uri: Uri) -> Self { Endpoint { uri, immutable: true, @@ -91,11 +95,7 @@ mod test { #[test] fn prefix_endpoint() { - let ep = Endpoint::new( - Uri::from_static("https://us-east-1.dynamo.amazonaws.com"), - false, - ) - .expect("valid endpoint"); + let ep = Endpoint::mutable(Uri::from_static("https://us-east-1.dynamo.amazonaws.com")); let mut uri = Uri::from_static("/list_tables?k=v"); ep.set_endpoint( &mut uri, @@ -109,11 +109,9 @@ mod test { #[test] fn prefix_endpoint_custom_port() { - let ep = Endpoint::new( - Uri::from_static("https://us-east-1.dynamo.amazonaws.com:6443"), - false, - ) - .expect("valid endpoint"); + let ep = Endpoint::mutable(Uri::from_static( + "https://us-east-1.dynamo.amazonaws.com:6443", + )); let mut uri = Uri::from_static("/list_tables?k=v"); ep.set_endpoint( &mut uri, @@ -129,11 +127,7 @@ mod test { #[test] fn prefix_immutable_endpoint() { - let ep = Endpoint::new( - Uri::from_static("https://us-east-1.dynamo.amazonaws.com"), - true, - ) - .expect("valid endpoint"); + let ep = Endpoint::immutable(Uri::from_static("https://us-east-1.dynamo.amazonaws.com")); let mut uri = Uri::from_static("/list_tables?k=v"); ep.set_endpoint( &mut uri, @@ -147,8 +141,7 @@ mod test { #[test] fn set_endpoint_empty_path() { - let ep = - Endpoint::new(Uri::from_static("http://localhost:8000"), true).expect("valid endpoint"); + let ep = Endpoint::immutable(Uri::from_static("http://localhost:8000")); let mut uri = Uri::from_static("/"); ep.set_endpoint(&mut uri, None); assert_eq!(uri, Uri::from_static("http://localhost:8000/"))