Loading aws/rust-runtime/aws-endpoint/Cargo.toml +1 −0 Original line number Diff line number Diff line Loading @@ -12,3 +12,4 @@ description = "AWS Endpoint Support" smithy-http = { path = "../../../rust-runtime/smithy-http"} aws-types = { path = "../aws-types" } http = "0.2.3" regex = { version = "1", default-features = false, features = ["std"]} aws/rust-runtime/aws-endpoint/src/lib.rs +82 −54 Original line number Diff line number Diff line Loading @@ -3,13 +3,20 @@ * SPDX-License-Identifier: Apache-2.0. */ #[doc(hidden)] pub mod partition; #[doc(hidden)] pub use partition::Partition; #[doc(hidden)] pub use partition::PartitionResolver; use std::error::Error; use std::fmt; use std::fmt::{Debug, Display, Formatter}; use std::str::FromStr; use std::sync::Arc; use http::{HeaderValue, Uri}; use http::HeaderValue; use aws_types::region::{Region, SigningRegion}; use aws_types::SigningService; Loading @@ -29,8 +36,7 @@ use std::convert::TryFrom; #[derive(Clone)] pub struct AwsEndpoint { endpoint: Endpoint, signing_service: Option<SigningService>, signing_region: Option<SigningRegion>, credential_scope: CredentialScope, } impl AwsEndpoint { Loading Loading @@ -73,52 +79,73 @@ pub type BoxError = Box<dyn Error + Send + Sync + 'static>; /// 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<AwsEndpoint, BoxError>; fn resolve_endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError>; } /// 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, #[derive(Clone, Default, Debug)] pub struct CredentialScope { region: Option<SigningRegion>, service: Option<SigningService>, } impl DefaultAwsEndpointResolver { pub fn for_service(service: &'static str) -> Self { Self { service } impl CredentialScope { pub fn builder() -> credential_scope::Builder { credential_scope::Builder::default() } } /// An `Endpoint` can be its own resolver to support static endpoints impl ResolveAwsEndpoint for Endpoint { fn endpoint(&self, _region: &Region) -> Result<AwsEndpoint, BoxError> { Ok(AwsEndpoint { endpoint: self.clone(), signing_service: None, signing_region: None, }) pub mod credential_scope { use crate::CredentialScope; use aws_types::region::SigningRegion; use aws_types::SigningService; #[derive(Debug, Default)] pub struct Builder { region: Option<SigningRegion>, service: Option<SigningService>, } impl Builder { pub fn region(mut self, region: &'static str) -> Self { self.region = Some(SigningRegion::from_static(region)); self } pub fn service(mut self, service: &'static str) -> Self { self.service = Some(SigningService::from_static(service)); self } pub fn build(self) -> CredentialScope { CredentialScope { region: self.region, service: self.service, } } } } impl CredentialScope { pub fn merge(&self, other: &CredentialScope) -> CredentialScope { CredentialScope { region: self.region.clone().or_else(|| other.region.clone()), service: self.service.clone().or_else(|| other.service.clone()), } } } impl ResolveAwsEndpoint for DefaultAwsEndpointResolver { fn endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError> { let uri = Uri::from_str(&format!( "https://{}.{}.amazonaws.com", self.service, region.as_ref(), ))?; /// An `Endpoint` can be its own resolver to support static endpoints impl ResolveAwsEndpoint for Endpoint { fn resolve_endpoint(&self, _region: &Region) -> Result<AwsEndpoint, BoxError> { Ok(AwsEndpoint { endpoint: Endpoint::mutable(uri), signing_region: Some(region.clone().into()), signing_service: None, endpoint: self.clone(), credential_scope: Default::default(), }) } } type AwsEndpointResolver = Arc<dyn ResolveAwsEndpoint>; fn get_endpoint_resolver(config: &PropertyBag) -> Option<&AwsEndpointResolver> { pub fn get_endpoint_resolver(config: &PropertyBag) -> Option<&AwsEndpointResolver> { config.get() } Loading Loading @@ -162,13 +189,14 @@ impl MapRequest for AwsEndpointStage { .get::<Region>() .ok_or(AwsEndpointStageError::NoRegion)?; let endpoint = provider .endpoint(region) .resolve_endpoint(region) .map_err(AwsEndpointStageError::EndpointResolutionError)?; let signing_region = endpoint .signing_region .credential_scope .region .unwrap_or_else(|| region.clone().into()); config.insert::<SigningRegion>(signing_region); if let Some(signing_service) = endpoint.signing_service { if let Some(signing_service) = endpoint.credential_scope.service { config.insert::<SigningService>(signing_service); } endpoint Loading Loading @@ -199,16 +227,18 @@ mod test { use smithy_http::middleware::MapRequest; use smithy_http::operation; use crate::{ set_endpoint_resolver, AwsEndpoint, AwsEndpointStage, BoxError, DefaultAwsEndpointResolver, ResolveAwsEndpoint, }; use crate::partition::endpoint::{Metadata, Protocol, SignatureVersion}; use crate::{set_endpoint_resolver, AwsEndpointStage, CredentialScope}; use http::header::HOST; use smithy_http::endpoint::Endpoint; #[test] fn default_endpoint_updates_request() { let provider = Arc::new(DefaultAwsEndpointResolver::for_service("kinesis")); let provider = Arc::new(Metadata { uri_template: "kinesis.{region}.amazonaws.com", protocol: Protocol::Https, credential_scope: Default::default(), signature_versions: SignatureVersion::V4, }); let req = http::Request::new(SdkBody::from("")); let region = Region::new("us-east-1"); let mut req = operation::Request::new(req); Loading Loading @@ -241,17 +271,15 @@ mod test { #[test] fn sets_service_override_when_set() { struct ServiceOverrideResolver; impl ResolveAwsEndpoint for ServiceOverrideResolver { fn endpoint(&self, _region: &Region) -> Result<AwsEndpoint, BoxError> { Ok(AwsEndpoint { endpoint: Endpoint::immutable(Uri::from_static("http://www.service.com")), signing_service: Some(SigningService::from_static("qldb-override")), signing_region: Some(SigningRegion::from(Region::new("us-east-override"))), }) } } let provider = Arc::new(ServiceOverrideResolver); let provider = Arc::new(Metadata { uri_template: "www.service.com", protocol: Protocol::Http, credential_scope: CredentialScope::builder() .service("qldb-override") .region("us-east-override") .build(), signature_versions: SignatureVersion::V4, }); let req = http::Request::new(SdkBody::from("")); let region = Region::new("us-east-1"); let mut req = operation::Request::new(req); Loading aws/rust-runtime/aws-endpoint/src/partition/endpoint.rs 0 → 100644 +71 −0 Original line number Diff line number Diff line /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ use crate::{AwsEndpoint, BoxError, CredentialScope, ResolveAwsEndpoint}; use aws_types::region::Region; use smithy_http::endpoint::Endpoint; /// Endpoint metadata /// /// Unlike other endpoint implementations, no merging occurs in here. All Endpoint merging occurs /// during code generation allowing us to generate fully formed endpoints. #[derive(Debug)] pub struct Metadata { /// URI for the endpoint. /// /// May contain `{region}` which will replaced with the region during endpoint construction pub uri_template: &'static str, /// Protocol to use for this endpoint pub protocol: Protocol, /// Credential scope to set for requests to this endpoint pub credential_scope: CredentialScope, /// Signature versions supported by this endpoint. /// /// Currently unused since the SDK only supports SigV4 pub signature_versions: SignatureVersion, } #[derive(Eq, PartialEq, Copy, Clone, Debug)] pub enum Protocol { Http, Https, } impl Protocol { fn as_str(&self) -> &'static str { match self { Protocol::Http => "http", Protocol::Https => "https", } } } #[derive(Eq, PartialEq, Copy, Clone, Debug)] pub enum SignatureVersion { V4, } impl ResolveAwsEndpoint for Metadata { fn resolve_endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError> { let uri = self.uri_template.replace("{region}", region.as_ref()); let uri = format!("{}://{}", self.protocol.as_str(), uri); let endpoint = Endpoint::mutable(uri.parse()?); let ep = AwsEndpoint { endpoint, credential_scope: CredentialScope { service: self.credential_scope.service.clone(), region: self .credential_scope .region .clone() .or_else(|| Some(region.clone().into())), }, }; Ok(ep) } } aws/rust-runtime/aws-endpoint/src/partition/mod.rs 0 → 100644 +389 −0 Original line number Diff line number Diff line /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ pub mod endpoint; use crate::{AwsEndpoint, BoxError, ResolveAwsEndpoint}; use aws_types::region::Region; use regex::Regex; use std::collections::HashMap; use std::iter; /// Root level resolver for an AWS Service /// /// PartitionResolver resolves the endpoint for an AWS Service. Each partition will be checked /// in turn, checking if the partition [can resolve](Partition::can_resolve) the given region. If /// no regions match, `base` is used. /// /// Once a partition has been identified, endpoint resolution is delegated to the underlying /// partition. pub struct PartitionResolver { /// Base partition used if no partitions match the region regex base: Partition, // base and rest are split so that we can validate that at least 1 partition is defined // at compile time. rest: Vec<Partition>, } impl PartitionResolver { /// Construct a new `PartitionResolver` from a list of partitions pub fn new(base: Partition, rest: Vec<Partition>) -> Self { Self { base, rest } } fn partitions(&self) -> impl Iterator<Item = &Partition> { iter::once(&self.base).chain(self.rest.iter()) } } impl ResolveAwsEndpoint for PartitionResolver { fn resolve_endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError> { let matching_partition = self .partitions() .find(|partition| partition.can_resolve(region)) .unwrap_or(&self.base); matching_partition.resolve_endpoint(region) } } #[derive(Debug)] pub struct Partition { id: &'static str, region_regex: Regex, partition_endpoint: Option<Region>, regionalized: Regionalized, default_endpoint: endpoint::Metadata, endpoints: HashMap<Region, endpoint::Metadata>, } #[derive(Default)] pub struct Builder { id: Option<&'static str>, region_regex: Option<Regex>, partition_endpoint: Option<Region>, regionalized: Option<Regionalized>, default_endpoint: Option<endpoint::Metadata>, endpoints: HashMap<Region, endpoint::Metadata>, } impl Builder { pub fn id(mut self, id: &'static str) -> Self { self.id = Some(id); self } pub fn default_endpoint(mut self, default: endpoint::Metadata) -> Self { self.default_endpoint = Some(default); self } pub fn region_regex(mut self, regex: &'static str) -> Self { // We use a stripped down version of the regex crate without unicode support // To support `\d` and `\w`, we need to explicitly opt into the ascii-only version. let ascii_only = regex .replace("\\d", "(?-u:\\d)") .replace("\\w", "(?-u:\\w)"); self.region_regex = Some(Regex::new(&ascii_only).expect("invalid regex")); self } pub fn partition_endpoint(mut self, partition_endpoint: &'static str) -> Self { self.partition_endpoint = Some(Region::new(partition_endpoint)); self } pub fn regionalized(mut self, regionalized: Regionalized) -> Self { self.regionalized = Some(regionalized); self } pub fn endpoint(mut self, region: &'static str, endpoint: endpoint::Metadata) -> Self { self.endpoints.insert(Region::new(region), endpoint); self } /// Construct a Partition from the builder /// /// Returns `None` if: /// - DefaultEndpoint is not set /// - DefaultEndpoint has an empty list of supported signature versions pub fn build(self) -> Option<Partition> { let default_endpoint = self.default_endpoint?; let endpoints = self.endpoints.into_iter().collect(); Some(Partition { id: self.id?, region_regex: self.region_regex?, partition_endpoint: self.partition_endpoint, regionalized: self.regionalized.unwrap_or_default(), default_endpoint, endpoints, }) } } #[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum Regionalized { Regionalized, NotRegionalized, } impl Default for Regionalized { fn default() -> Self { Regionalized::Regionalized } } impl Partition { pub fn can_resolve(&self, region: &Region) -> bool { self.region_regex.is_match(region.as_ref()) } pub fn builder() -> Builder { Builder::default() } } impl ResolveAwsEndpoint for Partition { fn resolve_endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError> { if let Some(endpoint) = self.endpoints.get(region) { return endpoint.resolve_endpoint(region); } let resolved_region = match self.regionalized { Regionalized::NotRegionalized => self.partition_endpoint.as_ref(), Regionalized::Regionalized => Some(region), }; let endpoint_for_region = resolved_region .and_then(|region| self.endpoints.get(®ion)) .unwrap_or(&self.default_endpoint); endpoint_for_region.resolve_endpoint(region) } } #[cfg(test)] mod test { use crate::partition::endpoint::Metadata; use crate::partition::endpoint::Protocol::{Http, Https}; use crate::partition::endpoint::SignatureVersion::{self, V4}; use crate::partition::{endpoint, Partition}; use crate::partition::{PartitionResolver, Regionalized}; use crate::{CredentialScope, ResolveAwsEndpoint}; use aws_types::region::{Region, SigningRegion}; use aws_types::SigningService; use http::Uri; fn basic_partition() -> Partition { Partition::builder() .id("part-id-1") .region_regex(r#"^(us)-\w+-\d+$"#) .default_endpoint(endpoint::Metadata { uri_template: "service.{region}.amazonaws.com", protocol: Https, credential_scope: CredentialScope::default(), signature_versions: SignatureVersion::V4, }) .partition_endpoint("") .regionalized(Regionalized::Regionalized) .endpoint( "us-west-1", endpoint::Metadata { uri_template: "service.{region}.amazonaws.com", protocol: Https, credential_scope: CredentialScope::default(), signature_versions: SignatureVersion::V4, }, ) .endpoint( "us-west-1-alt", Metadata { uri_template: "service-alt.us-west-1.amazonaws.com", protocol: Http, credential_scope: CredentialScope { region: Some(SigningRegion::from_static("us-west-1")), service: Some(SigningService::from_static("foo")), }, signature_versions: V4, }, ) .build() .expect("valid partition") } fn global_partition() -> Partition { Partition::builder() .id("part-id-1") .region_regex(r#"^(cn)-\w+-\d+$"#) .default_endpoint(Metadata { uri_template: "service.{region}.amazonaws.com", protocol: Https, credential_scope: CredentialScope { service: Some(SigningService::from_static("foo")), ..Default::default() }, signature_versions: SignatureVersion::V4, }) .partition_endpoint("partition") .regionalized(Regionalized::NotRegionalized) .endpoint( "partition", Metadata { uri_template: "some-global-thing.amazonaws.cn", protocol: Https, credential_scope: CredentialScope { region: Some(SigningRegion::from_static("cn-east-1")), service: Some(SigningService::from_static("foo")), }, signature_versions: SignatureVersion::V4, }, ) .endpoint( "cn-fips-1", Metadata { uri_template: "fips.amazonaws.cn", protocol: Https, credential_scope: CredentialScope { region: Some(SigningRegion::from_static("cn-fips")), service: None, }, signature_versions: SignatureVersion::V4, }, ) .build() .expect("valid partition") } fn partition_resolver() -> PartitionResolver { PartitionResolver::new( basic_partition(), vec![global_partition(), default_partition()], ) } fn default_partition() -> Partition { Partition::builder() .id("part-id-3") .region_regex(r#"^(eu)-\w+-\d+$"#) .default_endpoint(Metadata { uri_template: "service.{region}.amazonaws.com", protocol: Https, signature_versions: V4, credential_scope: CredentialScope { service: Some(SigningService::from_static("foo")), ..Default::default() }, }) .build() .expect("valid partition") } struct TestCase { region: &'static str, uri: &'static str, signing_region: &'static str, signing_service: Option<&'static str>, } /// Modeled region with no endpoint overrides const MODELED_REGION: TestCase = TestCase { region: "us-west-1", uri: "https://service.us-west-1.amazonaws.com", signing_region: "us-west-1", signing_service: None, }; /// Modeled region with endpoint overrides const MODELED_REGION_OVERRIDE: TestCase = TestCase { region: "us-west-1-alt", uri: "http://service-alt.us-west-1.amazonaws.com", signing_region: "us-west-1", signing_service: Some("foo"), }; /// Validates falling back onto the default endpoint const FALLBACK_REGION: TestCase = TestCase { region: "us-east-1", uri: "https://service.us-east-1.amazonaws.com", signing_region: "us-east-1", signing_service: None, }; /// Validates "PartitionName" const PARTITION_NAME: TestCase = TestCase { region: "cn-central-1", uri: "https://some-global-thing.amazonaws.cn", signing_region: "cn-east-1", signing_service: Some("foo"), }; /// Validates non-regionalized endpoints still use endpoints const NON_REGIONALIZED_EXACT_MATCH: TestCase = TestCase { region: "cn-fips-1", uri: "https://fips.amazonaws.cn", signing_region: "cn-fips", signing_service: None, }; const DEFAULT_ENDPOINT: TestCase = TestCase { region: "eu-west-1", uri: "https://service.eu-west-1.amazonaws.com", signing_region: "eu-west-1", signing_service: Some("foo"), }; const TEST_CASES: &[TestCase] = &[ MODELED_REGION, MODELED_REGION_OVERRIDE, FALLBACK_REGION, PARTITION_NAME, DEFAULT_ENDPOINT, NON_REGIONALIZED_EXACT_MATCH, ]; #[test] fn validate_basic_partition() { let p10n = basic_partition(); check_endpoint(&p10n, &MODELED_REGION); check_endpoint(&p10n, &MODELED_REGION_OVERRIDE); check_endpoint(&p10n, &FALLBACK_REGION); } #[test] fn validate_global_partition() { let partition = global_partition(); check_endpoint(&partition, &PARTITION_NAME); check_endpoint(&partition, &NON_REGIONALIZED_EXACT_MATCH) } #[test] fn validate_default_endpoint() { check_endpoint(&default_partition(), &DEFAULT_ENDPOINT); } #[test] fn validate_partition_resolver() { let resolver = partition_resolver(); for test_case in TEST_CASES { check_endpoint(&resolver, test_case); } } #[track_caller] fn check_endpoint(resolver: &impl ResolveAwsEndpoint, test_case: &TestCase) { let endpoint = resolver .resolve_endpoint(&Region::new(test_case.region)) .expect("valid region"); let mut test_uri = Uri::from_static("/"); endpoint.set_endpoint(&mut test_uri, None); assert_eq!(test_uri, Uri::from_static(test_case.uri)); assert_eq!( endpoint.credential_scope.region, Some(SigningRegion::from_static(test_case.signing_region)) ); assert_eq!( endpoint.credential_scope.service, test_case.signing_service.map(SigningService::from_static) ) } } aws/rust-runtime/aws-hyper/tests/e2e_test.rs +8 −2 Original line number Diff line number Diff line Loading @@ -4,7 +4,8 @@ */ use aws_auth::Credentials; use aws_endpoint::{set_endpoint_resolver, DefaultAwsEndpointResolver}; use aws_endpoint::partition::endpoint::{Protocol, SignatureVersion}; use aws_endpoint::set_endpoint_resolver; use aws_http::user_agent::AwsUserAgent; use aws_http::AwsErrorRetryPolicy; use aws_hyper::test_connection::TestConnection; Loading Loading @@ -77,7 +78,12 @@ fn test_operation() -> Operation<TestOperationParser, AwsErrorRetryPolicy> { .augment(|req, mut conf| { set_endpoint_resolver( &mut conf, Arc::new(DefaultAwsEndpointResolver::for_service("test-service")), Arc::new(aws_endpoint::partition::endpoint::Metadata { uri_template: "test-service.{region}.amazonaws.com", protocol: Protocol::Https, credential_scope: Default::default(), signature_versions: SignatureVersion::V4, }), ); aws_auth::set_provider( &mut conf, Loading Loading
aws/rust-runtime/aws-endpoint/Cargo.toml +1 −0 Original line number Diff line number Diff line Loading @@ -12,3 +12,4 @@ description = "AWS Endpoint Support" smithy-http = { path = "../../../rust-runtime/smithy-http"} aws-types = { path = "../aws-types" } http = "0.2.3" regex = { version = "1", default-features = false, features = ["std"]}
aws/rust-runtime/aws-endpoint/src/lib.rs +82 −54 Original line number Diff line number Diff line Loading @@ -3,13 +3,20 @@ * SPDX-License-Identifier: Apache-2.0. */ #[doc(hidden)] pub mod partition; #[doc(hidden)] pub use partition::Partition; #[doc(hidden)] pub use partition::PartitionResolver; use std::error::Error; use std::fmt; use std::fmt::{Debug, Display, Formatter}; use std::str::FromStr; use std::sync::Arc; use http::{HeaderValue, Uri}; use http::HeaderValue; use aws_types::region::{Region, SigningRegion}; use aws_types::SigningService; Loading @@ -29,8 +36,7 @@ use std::convert::TryFrom; #[derive(Clone)] pub struct AwsEndpoint { endpoint: Endpoint, signing_service: Option<SigningService>, signing_region: Option<SigningRegion>, credential_scope: CredentialScope, } impl AwsEndpoint { Loading Loading @@ -73,52 +79,73 @@ pub type BoxError = Box<dyn Error + Send + Sync + 'static>; /// 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<AwsEndpoint, BoxError>; fn resolve_endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError>; } /// 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, #[derive(Clone, Default, Debug)] pub struct CredentialScope { region: Option<SigningRegion>, service: Option<SigningService>, } impl DefaultAwsEndpointResolver { pub fn for_service(service: &'static str) -> Self { Self { service } impl CredentialScope { pub fn builder() -> credential_scope::Builder { credential_scope::Builder::default() } } /// An `Endpoint` can be its own resolver to support static endpoints impl ResolveAwsEndpoint for Endpoint { fn endpoint(&self, _region: &Region) -> Result<AwsEndpoint, BoxError> { Ok(AwsEndpoint { endpoint: self.clone(), signing_service: None, signing_region: None, }) pub mod credential_scope { use crate::CredentialScope; use aws_types::region::SigningRegion; use aws_types::SigningService; #[derive(Debug, Default)] pub struct Builder { region: Option<SigningRegion>, service: Option<SigningService>, } impl Builder { pub fn region(mut self, region: &'static str) -> Self { self.region = Some(SigningRegion::from_static(region)); self } pub fn service(mut self, service: &'static str) -> Self { self.service = Some(SigningService::from_static(service)); self } pub fn build(self) -> CredentialScope { CredentialScope { region: self.region, service: self.service, } } } } impl CredentialScope { pub fn merge(&self, other: &CredentialScope) -> CredentialScope { CredentialScope { region: self.region.clone().or_else(|| other.region.clone()), service: self.service.clone().or_else(|| other.service.clone()), } } } impl ResolveAwsEndpoint for DefaultAwsEndpointResolver { fn endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError> { let uri = Uri::from_str(&format!( "https://{}.{}.amazonaws.com", self.service, region.as_ref(), ))?; /// An `Endpoint` can be its own resolver to support static endpoints impl ResolveAwsEndpoint for Endpoint { fn resolve_endpoint(&self, _region: &Region) -> Result<AwsEndpoint, BoxError> { Ok(AwsEndpoint { endpoint: Endpoint::mutable(uri), signing_region: Some(region.clone().into()), signing_service: None, endpoint: self.clone(), credential_scope: Default::default(), }) } } type AwsEndpointResolver = Arc<dyn ResolveAwsEndpoint>; fn get_endpoint_resolver(config: &PropertyBag) -> Option<&AwsEndpointResolver> { pub fn get_endpoint_resolver(config: &PropertyBag) -> Option<&AwsEndpointResolver> { config.get() } Loading Loading @@ -162,13 +189,14 @@ impl MapRequest for AwsEndpointStage { .get::<Region>() .ok_or(AwsEndpointStageError::NoRegion)?; let endpoint = provider .endpoint(region) .resolve_endpoint(region) .map_err(AwsEndpointStageError::EndpointResolutionError)?; let signing_region = endpoint .signing_region .credential_scope .region .unwrap_or_else(|| region.clone().into()); config.insert::<SigningRegion>(signing_region); if let Some(signing_service) = endpoint.signing_service { if let Some(signing_service) = endpoint.credential_scope.service { config.insert::<SigningService>(signing_service); } endpoint Loading Loading @@ -199,16 +227,18 @@ mod test { use smithy_http::middleware::MapRequest; use smithy_http::operation; use crate::{ set_endpoint_resolver, AwsEndpoint, AwsEndpointStage, BoxError, DefaultAwsEndpointResolver, ResolveAwsEndpoint, }; use crate::partition::endpoint::{Metadata, Protocol, SignatureVersion}; use crate::{set_endpoint_resolver, AwsEndpointStage, CredentialScope}; use http::header::HOST; use smithy_http::endpoint::Endpoint; #[test] fn default_endpoint_updates_request() { let provider = Arc::new(DefaultAwsEndpointResolver::for_service("kinesis")); let provider = Arc::new(Metadata { uri_template: "kinesis.{region}.amazonaws.com", protocol: Protocol::Https, credential_scope: Default::default(), signature_versions: SignatureVersion::V4, }); let req = http::Request::new(SdkBody::from("")); let region = Region::new("us-east-1"); let mut req = operation::Request::new(req); Loading Loading @@ -241,17 +271,15 @@ mod test { #[test] fn sets_service_override_when_set() { struct ServiceOverrideResolver; impl ResolveAwsEndpoint for ServiceOverrideResolver { fn endpoint(&self, _region: &Region) -> Result<AwsEndpoint, BoxError> { Ok(AwsEndpoint { endpoint: Endpoint::immutable(Uri::from_static("http://www.service.com")), signing_service: Some(SigningService::from_static("qldb-override")), signing_region: Some(SigningRegion::from(Region::new("us-east-override"))), }) } } let provider = Arc::new(ServiceOverrideResolver); let provider = Arc::new(Metadata { uri_template: "www.service.com", protocol: Protocol::Http, credential_scope: CredentialScope::builder() .service("qldb-override") .region("us-east-override") .build(), signature_versions: SignatureVersion::V4, }); let req = http::Request::new(SdkBody::from("")); let region = Region::new("us-east-1"); let mut req = operation::Request::new(req); Loading
aws/rust-runtime/aws-endpoint/src/partition/endpoint.rs 0 → 100644 +71 −0 Original line number Diff line number Diff line /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ use crate::{AwsEndpoint, BoxError, CredentialScope, ResolveAwsEndpoint}; use aws_types::region::Region; use smithy_http::endpoint::Endpoint; /// Endpoint metadata /// /// Unlike other endpoint implementations, no merging occurs in here. All Endpoint merging occurs /// during code generation allowing us to generate fully formed endpoints. #[derive(Debug)] pub struct Metadata { /// URI for the endpoint. /// /// May contain `{region}` which will replaced with the region during endpoint construction pub uri_template: &'static str, /// Protocol to use for this endpoint pub protocol: Protocol, /// Credential scope to set for requests to this endpoint pub credential_scope: CredentialScope, /// Signature versions supported by this endpoint. /// /// Currently unused since the SDK only supports SigV4 pub signature_versions: SignatureVersion, } #[derive(Eq, PartialEq, Copy, Clone, Debug)] pub enum Protocol { Http, Https, } impl Protocol { fn as_str(&self) -> &'static str { match self { Protocol::Http => "http", Protocol::Https => "https", } } } #[derive(Eq, PartialEq, Copy, Clone, Debug)] pub enum SignatureVersion { V4, } impl ResolveAwsEndpoint for Metadata { fn resolve_endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError> { let uri = self.uri_template.replace("{region}", region.as_ref()); let uri = format!("{}://{}", self.protocol.as_str(), uri); let endpoint = Endpoint::mutable(uri.parse()?); let ep = AwsEndpoint { endpoint, credential_scope: CredentialScope { service: self.credential_scope.service.clone(), region: self .credential_scope .region .clone() .or_else(|| Some(region.clone().into())), }, }; Ok(ep) } }
aws/rust-runtime/aws-endpoint/src/partition/mod.rs 0 → 100644 +389 −0 Original line number Diff line number Diff line /* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ pub mod endpoint; use crate::{AwsEndpoint, BoxError, ResolveAwsEndpoint}; use aws_types::region::Region; use regex::Regex; use std::collections::HashMap; use std::iter; /// Root level resolver for an AWS Service /// /// PartitionResolver resolves the endpoint for an AWS Service. Each partition will be checked /// in turn, checking if the partition [can resolve](Partition::can_resolve) the given region. If /// no regions match, `base` is used. /// /// Once a partition has been identified, endpoint resolution is delegated to the underlying /// partition. pub struct PartitionResolver { /// Base partition used if no partitions match the region regex base: Partition, // base and rest are split so that we can validate that at least 1 partition is defined // at compile time. rest: Vec<Partition>, } impl PartitionResolver { /// Construct a new `PartitionResolver` from a list of partitions pub fn new(base: Partition, rest: Vec<Partition>) -> Self { Self { base, rest } } fn partitions(&self) -> impl Iterator<Item = &Partition> { iter::once(&self.base).chain(self.rest.iter()) } } impl ResolveAwsEndpoint for PartitionResolver { fn resolve_endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError> { let matching_partition = self .partitions() .find(|partition| partition.can_resolve(region)) .unwrap_or(&self.base); matching_partition.resolve_endpoint(region) } } #[derive(Debug)] pub struct Partition { id: &'static str, region_regex: Regex, partition_endpoint: Option<Region>, regionalized: Regionalized, default_endpoint: endpoint::Metadata, endpoints: HashMap<Region, endpoint::Metadata>, } #[derive(Default)] pub struct Builder { id: Option<&'static str>, region_regex: Option<Regex>, partition_endpoint: Option<Region>, regionalized: Option<Regionalized>, default_endpoint: Option<endpoint::Metadata>, endpoints: HashMap<Region, endpoint::Metadata>, } impl Builder { pub fn id(mut self, id: &'static str) -> Self { self.id = Some(id); self } pub fn default_endpoint(mut self, default: endpoint::Metadata) -> Self { self.default_endpoint = Some(default); self } pub fn region_regex(mut self, regex: &'static str) -> Self { // We use a stripped down version of the regex crate without unicode support // To support `\d` and `\w`, we need to explicitly opt into the ascii-only version. let ascii_only = regex .replace("\\d", "(?-u:\\d)") .replace("\\w", "(?-u:\\w)"); self.region_regex = Some(Regex::new(&ascii_only).expect("invalid regex")); self } pub fn partition_endpoint(mut self, partition_endpoint: &'static str) -> Self { self.partition_endpoint = Some(Region::new(partition_endpoint)); self } pub fn regionalized(mut self, regionalized: Regionalized) -> Self { self.regionalized = Some(regionalized); self } pub fn endpoint(mut self, region: &'static str, endpoint: endpoint::Metadata) -> Self { self.endpoints.insert(Region::new(region), endpoint); self } /// Construct a Partition from the builder /// /// Returns `None` if: /// - DefaultEndpoint is not set /// - DefaultEndpoint has an empty list of supported signature versions pub fn build(self) -> Option<Partition> { let default_endpoint = self.default_endpoint?; let endpoints = self.endpoints.into_iter().collect(); Some(Partition { id: self.id?, region_regex: self.region_regex?, partition_endpoint: self.partition_endpoint, regionalized: self.regionalized.unwrap_or_default(), default_endpoint, endpoints, }) } } #[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum Regionalized { Regionalized, NotRegionalized, } impl Default for Regionalized { fn default() -> Self { Regionalized::Regionalized } } impl Partition { pub fn can_resolve(&self, region: &Region) -> bool { self.region_regex.is_match(region.as_ref()) } pub fn builder() -> Builder { Builder::default() } } impl ResolveAwsEndpoint for Partition { fn resolve_endpoint(&self, region: &Region) -> Result<AwsEndpoint, BoxError> { if let Some(endpoint) = self.endpoints.get(region) { return endpoint.resolve_endpoint(region); } let resolved_region = match self.regionalized { Regionalized::NotRegionalized => self.partition_endpoint.as_ref(), Regionalized::Regionalized => Some(region), }; let endpoint_for_region = resolved_region .and_then(|region| self.endpoints.get(®ion)) .unwrap_or(&self.default_endpoint); endpoint_for_region.resolve_endpoint(region) } } #[cfg(test)] mod test { use crate::partition::endpoint::Metadata; use crate::partition::endpoint::Protocol::{Http, Https}; use crate::partition::endpoint::SignatureVersion::{self, V4}; use crate::partition::{endpoint, Partition}; use crate::partition::{PartitionResolver, Regionalized}; use crate::{CredentialScope, ResolveAwsEndpoint}; use aws_types::region::{Region, SigningRegion}; use aws_types::SigningService; use http::Uri; fn basic_partition() -> Partition { Partition::builder() .id("part-id-1") .region_regex(r#"^(us)-\w+-\d+$"#) .default_endpoint(endpoint::Metadata { uri_template: "service.{region}.amazonaws.com", protocol: Https, credential_scope: CredentialScope::default(), signature_versions: SignatureVersion::V4, }) .partition_endpoint("") .regionalized(Regionalized::Regionalized) .endpoint( "us-west-1", endpoint::Metadata { uri_template: "service.{region}.amazonaws.com", protocol: Https, credential_scope: CredentialScope::default(), signature_versions: SignatureVersion::V4, }, ) .endpoint( "us-west-1-alt", Metadata { uri_template: "service-alt.us-west-1.amazonaws.com", protocol: Http, credential_scope: CredentialScope { region: Some(SigningRegion::from_static("us-west-1")), service: Some(SigningService::from_static("foo")), }, signature_versions: V4, }, ) .build() .expect("valid partition") } fn global_partition() -> Partition { Partition::builder() .id("part-id-1") .region_regex(r#"^(cn)-\w+-\d+$"#) .default_endpoint(Metadata { uri_template: "service.{region}.amazonaws.com", protocol: Https, credential_scope: CredentialScope { service: Some(SigningService::from_static("foo")), ..Default::default() }, signature_versions: SignatureVersion::V4, }) .partition_endpoint("partition") .regionalized(Regionalized::NotRegionalized) .endpoint( "partition", Metadata { uri_template: "some-global-thing.amazonaws.cn", protocol: Https, credential_scope: CredentialScope { region: Some(SigningRegion::from_static("cn-east-1")), service: Some(SigningService::from_static("foo")), }, signature_versions: SignatureVersion::V4, }, ) .endpoint( "cn-fips-1", Metadata { uri_template: "fips.amazonaws.cn", protocol: Https, credential_scope: CredentialScope { region: Some(SigningRegion::from_static("cn-fips")), service: None, }, signature_versions: SignatureVersion::V4, }, ) .build() .expect("valid partition") } fn partition_resolver() -> PartitionResolver { PartitionResolver::new( basic_partition(), vec![global_partition(), default_partition()], ) } fn default_partition() -> Partition { Partition::builder() .id("part-id-3") .region_regex(r#"^(eu)-\w+-\d+$"#) .default_endpoint(Metadata { uri_template: "service.{region}.amazonaws.com", protocol: Https, signature_versions: V4, credential_scope: CredentialScope { service: Some(SigningService::from_static("foo")), ..Default::default() }, }) .build() .expect("valid partition") } struct TestCase { region: &'static str, uri: &'static str, signing_region: &'static str, signing_service: Option<&'static str>, } /// Modeled region with no endpoint overrides const MODELED_REGION: TestCase = TestCase { region: "us-west-1", uri: "https://service.us-west-1.amazonaws.com", signing_region: "us-west-1", signing_service: None, }; /// Modeled region with endpoint overrides const MODELED_REGION_OVERRIDE: TestCase = TestCase { region: "us-west-1-alt", uri: "http://service-alt.us-west-1.amazonaws.com", signing_region: "us-west-1", signing_service: Some("foo"), }; /// Validates falling back onto the default endpoint const FALLBACK_REGION: TestCase = TestCase { region: "us-east-1", uri: "https://service.us-east-1.amazonaws.com", signing_region: "us-east-1", signing_service: None, }; /// Validates "PartitionName" const PARTITION_NAME: TestCase = TestCase { region: "cn-central-1", uri: "https://some-global-thing.amazonaws.cn", signing_region: "cn-east-1", signing_service: Some("foo"), }; /// Validates non-regionalized endpoints still use endpoints const NON_REGIONALIZED_EXACT_MATCH: TestCase = TestCase { region: "cn-fips-1", uri: "https://fips.amazonaws.cn", signing_region: "cn-fips", signing_service: None, }; const DEFAULT_ENDPOINT: TestCase = TestCase { region: "eu-west-1", uri: "https://service.eu-west-1.amazonaws.com", signing_region: "eu-west-1", signing_service: Some("foo"), }; const TEST_CASES: &[TestCase] = &[ MODELED_REGION, MODELED_REGION_OVERRIDE, FALLBACK_REGION, PARTITION_NAME, DEFAULT_ENDPOINT, NON_REGIONALIZED_EXACT_MATCH, ]; #[test] fn validate_basic_partition() { let p10n = basic_partition(); check_endpoint(&p10n, &MODELED_REGION); check_endpoint(&p10n, &MODELED_REGION_OVERRIDE); check_endpoint(&p10n, &FALLBACK_REGION); } #[test] fn validate_global_partition() { let partition = global_partition(); check_endpoint(&partition, &PARTITION_NAME); check_endpoint(&partition, &NON_REGIONALIZED_EXACT_MATCH) } #[test] fn validate_default_endpoint() { check_endpoint(&default_partition(), &DEFAULT_ENDPOINT); } #[test] fn validate_partition_resolver() { let resolver = partition_resolver(); for test_case in TEST_CASES { check_endpoint(&resolver, test_case); } } #[track_caller] fn check_endpoint(resolver: &impl ResolveAwsEndpoint, test_case: &TestCase) { let endpoint = resolver .resolve_endpoint(&Region::new(test_case.region)) .expect("valid region"); let mut test_uri = Uri::from_static("/"); endpoint.set_endpoint(&mut test_uri, None); assert_eq!(test_uri, Uri::from_static(test_case.uri)); assert_eq!( endpoint.credential_scope.region, Some(SigningRegion::from_static(test_case.signing_region)) ); assert_eq!( endpoint.credential_scope.service, test_case.signing_service.map(SigningService::from_static) ) } }
aws/rust-runtime/aws-hyper/tests/e2e_test.rs +8 −2 Original line number Diff line number Diff line Loading @@ -4,7 +4,8 @@ */ use aws_auth::Credentials; use aws_endpoint::{set_endpoint_resolver, DefaultAwsEndpointResolver}; use aws_endpoint::partition::endpoint::{Protocol, SignatureVersion}; use aws_endpoint::set_endpoint_resolver; use aws_http::user_agent::AwsUserAgent; use aws_http::AwsErrorRetryPolicy; use aws_hyper::test_connection::TestConnection; Loading Loading @@ -77,7 +78,12 @@ fn test_operation() -> Operation<TestOperationParser, AwsErrorRetryPolicy> { .augment(|req, mut conf| { set_endpoint_resolver( &mut conf, Arc::new(DefaultAwsEndpointResolver::for_service("test-service")), Arc::new(aws_endpoint::partition::endpoint::Metadata { uri_template: "test-service.{region}.amazonaws.com", protocol: Protocol::Https, credential_scope: Default::default(), signature_versions: SignatureVersion::V4, }), ); aws_auth::set_provider( &mut conf, Loading