1- //! AWS hostname parsing: service-code and region extraction .
1+ //! AWS endpoint parsing: extract the SigV4 region and service from an `amazonaws.com` hostname .
22//!
3- //! ## Canonical AWS hostname shapes
3+ //! Supported hostname shapes:
4+ //! - Regional: `{service}.{region}.amazonaws.com`
5+ //! - Global: `{service}.amazonaws.com`
6+ //! - FIPS: `{service}-fips.{region}.amazonaws.com` (`-fips` is stripped)
47//!
5- //! ```text
6- //! Regional: {service}.{region}.amazonaws.com (4 dot-segments)
7- //! Global: {service}.amazonaws.com (3 dot-segments)
8- //! FIPS: {service}-fips.{region}.amazonaws.com (4 dot-segments, service ends in "-fips")
9- //! Dual-stack:{service}.{region}.api.aws (different TLD — not handled here)
10- //! ```
11- //!
12- //! `api.aws` dual-stack endpoints are out of scope for auto-detection; users
13- //! on those endpoints supply explicit `aws_auth.region` and `aws_auth.service`
14- //! overrides in their config.
15- //!
16- //! ## Region code pattern
17- //!
18- //! Standard regions match `^[a-z]{2,3}-[a-z]+-\d+$`. GovCloud and ISO
19- //! partition regions have an extra word between the geo prefix and direction
20- //! segment (`us-gov-east-1`, `us-iso-east-1`, `us-isob-east-1`), so the
21- //! effective pattern is `^[a-z]{2,3}-(?:[a-z]+-)?[a-z]+-\d+$`. This covers
22- //! all current regions including newer geo prefixes (`il-`, `mx-`, `me-`,
23- //! `af-`) and high-index AZ suffixes (`ap-southeast-7`).
24- //!
25- //! ## Service-code table
26- //!
27- //! The service segment is not always the SigV4 signing name. AWS uses
28- //! irregular host prefixes (e.g. `bedrock-runtime` signs as `bedrock`,
29- //! `email` signs as `ses`). The table maps the canonical segment (with any
30- //! `-fips` suffix stripped) to the signing name.
8+ //! `api.aws` dual-stack endpoints are not supported; supply explicit
9+ //! `aws_auth.region` and `aws_auth.service` config values for those.
3110
3211use std:: collections:: HashMap ;
3312use std:: sync:: OnceLock ;
@@ -43,7 +22,8 @@ fn region_re() -> &'static Regex {
4322}
4423
4524fn service_map ( ) -> & ' static HashMap < & ' static str , & ' static str > {
46- // Maps from the URL hostname path (key) to the AWS service name (value)
25+ // Maps hostname service segment to SigV4 signing name. Some segments differ
26+ // from the signing name (e.g. "bedrock-runtime" signs as "bedrock", "email" as "ses").
4727 static MAP : OnceLock < HashMap < & ' static str , & ' static str > > = OnceLock :: new ( ) ;
4828 MAP . get_or_init ( || {
4929 HashMap :: from ( [
@@ -73,32 +53,18 @@ fn service_map() -> &'static HashMap<&'static str, &'static str> {
7353 } )
7454}
7555
76- /// Parsed components of an AWS URL
56+ /// Parsed components of an `amazonaws.com` hostname.
7757#[ derive( Debug , PartialEq , Eq ) ]
7858pub struct AwsUrlParts < ' a > {
79- /// Service-code segment from the hostname, with any `-fips` suffix
80- /// already stripped (e.g. `"bedrock-runtime"`, `"iam"`).
59+ /// Service segment with any `-fips` suffix stripped (e.g. `"bedrock-runtime"`, `"iam"`).
8160 pub normalized_service : & ' a str ,
82- /// Region code if present (e.g. `"us-east-1"`), or `None` for
83- /// global-only endpoints.
61+ /// Region code, or `None` for global endpoints.
8462 pub region : Option < & ' a str > ,
8563}
8664
8765impl < ' a > AwsUrlParts < ' a > {
88- /// Parse a hostname into its service segment and optional region.
89- ///
90- /// Accepts only `amazonaws.com` endpoints in the two canonical shapes:
91- /// - Regional (4 segments): `{service}.{region}.amazonaws.com`
92- /// - Global (3 segments): `{service}.amazonaws.com`
93- ///
94- /// A `-fips` suffix on the service segment is stripped before returning so
95- /// callers always receive the canonical segment name (e.g. `"bedrock-runtime"`
96- /// for both `bedrock-runtime.us-east-1.amazonaws.com` and
97- /// `bedrock-runtime-fips.us-east-1.amazonaws.com`).
98- ///
99- /// Returns `None` for non-`amazonaws.com` hosts (including `api.aws`
100- /// dual-stack endpoints), wrong segment counts, and region strings that do
101- /// not match the regex
66+ /// Parse an `amazonaws.com` hostname into its service and region components.
67+ /// Returns `None` for unrecognised shapes or invalid region codes.
10268 #[ must_use]
10369 pub fn parse ( host : & ' a str ) -> Option < Self > {
10470 match host. split ( '.' ) . collect :: < Vec < _ > > ( ) . as_slice ( ) {
@@ -119,12 +85,8 @@ impl<'a> AwsUrlParts<'a> {
11985 }
12086 }
12187
122- /// Return the SigV4 signing service name for this endpoint.
123- ///
124- /// The `normalized_service` segment (with any `-fips` suffix already
125- /// stripped) is looked up in the service table. Returns `None` for
126- /// unrecognised service segments; callers should fall back to an explicit
127- /// `aws_auth.service` config value or emit a warning.
88+ /// Look up the SigV4 signing service name for this endpoint.
89+ /// Returns `None` for unrecognised service segments.
12890 #[ must_use]
12991 pub fn signing_service ( & self ) -> Option < & ' static str > {
13092 service_map ( ) . get ( self . normalized_service ) . copied ( )
@@ -133,25 +95,33 @@ impl<'a> AwsUrlParts<'a> {
13395
13496/// Resolve the SigV4 `(region, service)` pair for an AWS route.
13597///
136- /// Explicit config values in `aws_auth` take precedence over auto-detection.
137- /// When either is absent the `upstream_host` is parsed as an `amazonaws.com`
138- /// endpoint; if the hostname is not recognised and a value cannot be inferred,
139- /// returns `None` after emitting a `warn!` that names the missing field and
140- /// how to fix it.
141- ///
142- /// Returns `Some((region, service))` on success, `None` when the route should
143- /// be skipped (the warning has already been emitted).
98+ /// Config values in `aws_auth` take precedence. When either is absent, the
99+ /// host of `upstream` is parsed as an `amazonaws.com` endpoint to fill in the
100+ /// missing value. Returns `None` and emits a warning if resolution fails.
144101#[ must_use]
145102pub fn resolve_signing_params (
146103 prefix : & str ,
147104 aws_auth : & crate :: config:: AwsAuthConfig ,
148- upstream_host : & str ,
105+ upstream : & str ,
149106) -> Option < ( String , String ) > {
150- // When either region or service is missing from config, parse the upstream
151- // host once and derive both from it. If both are explicit we skip the parse
152- // entirely.
107+ let upstream_host: String ;
153108 let parsed_host = if aws_auth. region . is_none ( ) || aws_auth. service . is_none ( ) {
154- let h = AwsUrlParts :: parse ( upstream_host) ;
109+ upstream_host = match url:: Url :: parse ( upstream)
110+ . ok ( )
111+ . and_then ( |u| u. host_str ( ) . map ( str:: to_owned) )
112+ {
113+ Some ( h) => h,
114+ None => {
115+ warn ! (
116+ "AWS route '{}': upstream '{}' is not a valid URL with a \
117+ host component; fix the upstream or set aws_auth.region \
118+ and aws_auth.service explicitly — skipping this route.",
119+ prefix, upstream
120+ ) ;
121+ return None ;
122+ }
123+ } ;
124+ let h = AwsUrlParts :: parse ( & upstream_host) ;
155125 if h. is_none ( ) {
156126 warn ! (
157127 "AWS route '{}': upstream host '{}' is not a recognised \
@@ -166,7 +136,7 @@ pub fn resolve_signing_params(
166136 None
167137 } ;
168138
169- // Region: explicit override wins; otherwise take from parsed host.
139+ // Region: explicit config wins; otherwise derive from the parsed host.
170140 let region = match aws_auth. region . as_deref ( ) {
171141 Some ( r) => r. to_owned ( ) ,
172142 None => match parsed_host. as_ref ( ) . and_then ( |h| h. region ) {
@@ -176,15 +146,14 @@ pub fn resolve_signing_params(
176146 "AWS route '{}': could not determine region from upstream \
177147 host '{}' (global endpoint?); set aws_auth.region \
178148 explicitly — skipping this route.",
179- prefix, upstream_host
149+ prefix, upstream
180150 ) ;
181151 return None ;
182152 }
183153 } ,
184154 } ;
185155
186- // Service: explicit override wins; otherwise look up signing name from the
187- // parsed host's service segment.
156+ // Service: explicit config wins; otherwise look up from the parsed host.
188157 let service = match aws_auth. service . as_deref ( ) {
189158 Some ( s) => s. to_owned ( ) ,
190159 None => {
@@ -199,7 +168,7 @@ pub fn resolve_signing_params(
199168 "AWS route '{}': service segment '{}' from upstream \
200169 host '{}' is not in the signing-name table; set \
201170 aws_auth.service explicitly — skipping this route.",
202- prefix, seg, upstream_host
171+ prefix, seg, upstream
203172 ) ;
204173 return None ;
205174 }
0 commit comments