1use std::borrow::Cow;
7use std::fmt;
8
9use http_02x::header::{HeaderName, HeaderValue, InvalidHeaderValue, USER_AGENT};
10
11use aws_credential_types::credential_feature::AwsCredentialFeature;
12use aws_smithy_runtime::client::sdk_feature::SmithySdkFeature;
13use aws_smithy_runtime_api::box_error::BoxError;
14use aws_smithy_runtime_api::client::http::HttpClient;
15use aws_smithy_runtime_api::client::interceptors::context::{
16 BeforeTransmitInterceptorContextMut, BeforeTransmitInterceptorContextRef,
17};
18use aws_smithy_runtime_api::client::interceptors::Intercept;
19use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
20use aws_smithy_types::config_bag::ConfigBag;
21use aws_types::app_name::AppName;
22use aws_types::os_shim_internal::Env;
23
24use crate::sdk_feature::AwsSdkFeature;
25use crate::user_agent::metrics::ProvideBusinessMetric;
26use crate::user_agent::{AdditionalMetadata, ApiMetadata, AwsUserAgent, InvalidMetadataValue};
27
28macro_rules! add_metrics_unique {
29 ($features:expr, $ua:expr, $added:expr) => {
30 for feature in $features {
31 if let Some(m) = feature.provide_business_metric() {
32 if !$added.contains(&m) {
33 $added.insert(m.clone());
34 $ua.add_business_metric(m);
35 }
36 }
37 }
38 };
39}
40
41macro_rules! add_metrics_unique_reverse {
42 ($features:expr, $ua:expr, $added:expr) => {
43 let mut unique_metrics = Vec::new();
44 for feature in $features {
45 if let Some(m) = feature.provide_business_metric() {
46 if !$added.contains(&m) {
47 $added.insert(m.clone());
48 unique_metrics.push(m);
49 }
50 }
51 }
52 for m in unique_metrics.into_iter().rev() {
53 $ua.add_business_metric(m);
54 }
55 };
56}
57
58#[allow(clippy::declare_interior_mutable_const)] const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent");
60
61#[derive(Debug)]
62enum UserAgentInterceptorError {
63 MissingApiMetadata,
64 InvalidHeaderValue(InvalidHeaderValue),
65 InvalidMetadataValue(InvalidMetadataValue),
66}
67
68impl std::error::Error for UserAgentInterceptorError {
69 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
70 match self {
71 Self::InvalidHeaderValue(source) => Some(source),
72 Self::InvalidMetadataValue(source) => Some(source),
73 Self::MissingApiMetadata => None,
74 }
75 }
76}
77
78impl fmt::Display for UserAgentInterceptorError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.write_str(match self {
81 Self::InvalidHeaderValue(_) => "AwsUserAgent generated an invalid HTTP header value. This is a bug. Please file an issue.",
82 Self::InvalidMetadataValue(_) => "AwsUserAgent generated an invalid metadata value. This is a bug. Please file an issue.",
83 Self::MissingApiMetadata => "The UserAgentInterceptor requires ApiMetadata to be set before the request is made. This is a bug. Please file an issue.",
84 })
85 }
86}
87
88impl From<InvalidHeaderValue> for UserAgentInterceptorError {
89 fn from(err: InvalidHeaderValue) -> Self {
90 UserAgentInterceptorError::InvalidHeaderValue(err)
91 }
92}
93
94impl From<InvalidMetadataValue> for UserAgentInterceptorError {
95 fn from(err: InvalidMetadataValue) -> Self {
96 UserAgentInterceptorError::InvalidMetadataValue(err)
97 }
98}
99
100#[non_exhaustive]
102#[derive(Debug, Default)]
103pub struct UserAgentInterceptor;
104
105impl UserAgentInterceptor {
106 pub fn new() -> Self {
108 UserAgentInterceptor
109 }
110}
111
112fn header_values(
113 ua: &AwsUserAgent,
114) -> Result<(HeaderValue, HeaderValue), UserAgentInterceptorError> {
115 Ok((
117 HeaderValue::try_from(ua.ua_header())?,
118 HeaderValue::try_from(ua.aws_ua_header())?,
119 ))
120}
121
122impl Intercept for UserAgentInterceptor {
123 fn name(&self) -> &'static str {
124 "UserAgentInterceptor"
125 }
126
127 fn read_after_serialization(
128 &self,
129 _context: &BeforeTransmitInterceptorContextRef<'_>,
130 _runtime_components: &RuntimeComponents,
131 cfg: &mut ConfigBag,
132 ) -> Result<(), BoxError> {
133 if cfg.load::<AwsUserAgent>().is_some() {
137 return Ok(());
138 }
139
140 let api_metadata = cfg
141 .load::<ApiMetadata>()
142 .ok_or(UserAgentInterceptorError::MissingApiMetadata)?;
143 let mut ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata.clone());
144
145 let maybe_app_name = cfg.load::<AppName>();
146 if let Some(app_name) = maybe_app_name {
147 ua.set_app_name(app_name.clone());
148 }
149
150 cfg.interceptor_state().store_put(ua);
151
152 Ok(())
153 }
154
155 fn modify_before_signing(
156 &self,
157 context: &mut BeforeTransmitInterceptorContextMut<'_>,
158 runtime_components: &RuntimeComponents,
159 cfg: &mut ConfigBag,
160 ) -> Result<(), BoxError> {
161 let mut ua = cfg
162 .load::<AwsUserAgent>()
163 .expect("`AwsUserAgent should have been created in `read_before_execution`")
164 .clone();
165
166 let smithy_sdk_features = cfg.load::<SmithySdkFeature>();
171 for smithy_sdk_feature in smithy_sdk_features {
172 smithy_sdk_feature
173 .provide_business_metric()
174 .map(|m| ua.add_business_metric(m));
175 }
176
177 let aws_sdk_features = cfg.load::<AwsSdkFeature>();
178 for aws_sdk_feature in aws_sdk_features {
179 aws_sdk_feature
180 .provide_business_metric()
181 .map(|m| ua.add_business_metric(m));
182 }
183
184 let aws_credential_features = cfg.load::<AwsCredentialFeature>();
185 for aws_credential_feature in aws_credential_features {
186 aws_credential_feature
187 .provide_business_metric()
188 .map(|m| ua.add_business_metric(m));
189 }
190
191 let maybe_connector_metadata = runtime_components
192 .http_client()
193 .and_then(|c| c.connector_metadata());
194 if let Some(connector_metadata) = maybe_connector_metadata {
195 let am = AdditionalMetadata::new(Cow::Owned(connector_metadata.to_string()))?;
196 ua.add_additional_metadata(am);
197 }
198
199 let headers = context.request_mut().headers_mut();
200 let (user_agent, x_amz_user_agent) = header_values(&ua)?;
201 headers.append(USER_AGENT, user_agent);
202 headers.append(X_AMZ_USER_AGENT, x_amz_user_agent);
203 Ok(())
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
211 use aws_smithy_runtime_api::client::interceptors::Intercept;
212 use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
213 use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
214 use aws_smithy_types::config_bag::{ConfigBag, Layer};
215 use aws_smithy_types::error::display::DisplayErrorContext;
216
217 fn expect_header<'a>(context: &'a InterceptorContext, header_name: &str) -> &'a str {
218 context
219 .request()
220 .expect("request is set")
221 .headers()
222 .get(header_name)
223 .unwrap()
224 }
225
226 fn context() -> InterceptorContext {
227 let mut context = InterceptorContext::new(Input::doesnt_matter());
228 context.enter_serialization_phase();
229 context.set_request(HttpRequest::empty());
230 let _ = context.take_input();
231 context.enter_before_transmit_phase();
232 context
233 }
234
235 #[test]
236 fn test_overridden_ua() {
237 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
238 let mut context = context();
239
240 let mut layer = Layer::new("test");
241 layer.store_put(AwsUserAgent::for_tests());
242 layer.store_put(ApiMetadata::new("unused", "unused"));
243 let mut cfg = ConfigBag::of_layers(vec![layer]);
244
245 let interceptor = UserAgentInterceptor::new();
246 let mut ctx = Into::into(&mut context);
247 interceptor
248 .modify_before_signing(&mut ctx, &rc, &mut cfg)
249 .unwrap();
250
251 let header = expect_header(&context, "user-agent");
252 assert_eq!(AwsUserAgent::for_tests().ua_header(), header);
253 assert!(!header.contains("unused"));
254
255 assert_eq!(
256 AwsUserAgent::for_tests().aws_ua_header(),
257 expect_header(&context, "x-amz-user-agent")
258 );
259 }
260
261 #[test]
262 fn test_default_ua() {
263 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
264 let mut context = context();
265
266 let api_metadata = ApiMetadata::new("some-service", "some-version");
267 let mut layer = Layer::new("test");
268 layer.store_put(api_metadata.clone());
269 let mut config = ConfigBag::of_layers(vec![layer]);
270
271 let interceptor = UserAgentInterceptor::new();
272 let ctx = Into::into(&context);
273 interceptor
274 .read_after_serialization(&ctx, &rc, &mut config)
275 .unwrap();
276 let mut ctx = Into::into(&mut context);
277 interceptor
278 .modify_before_signing(&mut ctx, &rc, &mut config)
279 .unwrap();
280
281 let expected_ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata);
282 assert!(
283 expected_ua.aws_ua_header().contains("some-service"),
284 "precondition"
285 );
286 assert_eq!(
287 expected_ua.ua_header(),
288 expect_header(&context, "user-agent")
289 );
290 assert_eq!(
291 expected_ua.aws_ua_header(),
292 expect_header(&context, "x-amz-user-agent")
293 );
294 }
295
296 #[test]
297 fn test_modify_before_signing_no_duplicate_metrics() {
298 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
299 let mut context = context();
300
301 let api_metadata = ApiMetadata::new("test-service", "1.0");
302 let mut layer = Layer::new("test");
303 layer.store_put(api_metadata);
304 layer.store_append(SmithySdkFeature::Waiter);
306 layer.store_append(SmithySdkFeature::Waiter);
307 layer.store_append(AwsSdkFeature::S3Transfer);
308 layer.store_append(AwsSdkFeature::S3Transfer);
309 layer.store_append(AwsCredentialFeature::CredentialsCode);
310 layer.store_append(AwsCredentialFeature::CredentialsCode);
311 let mut config = ConfigBag::of_layers(vec![layer]);
312
313 let interceptor = UserAgentInterceptor::new();
314 let ctx = Into::into(&context);
315 interceptor
316 .read_after_serialization(&ctx, &rc, &mut config)
317 .unwrap();
318 let mut ctx = Into::into(&mut context);
319 interceptor
320 .modify_before_signing(&mut ctx, &rc, &mut config)
321 .unwrap();
322
323 let aws_ua_header = expect_header(&context, "x-amz-user-agent");
324 let metrics_section = aws_ua_header.split(" m/").nth(1).unwrap();
325 let waiter_count = metrics_section.matches("B").count();
326 let s3_transfer_count = metrics_section.matches("G").count();
327 let credentials_code_count = metrics_section.matches("e").count();
328 assert_eq!(
329 1, waiter_count,
330 "Waiter metric should appear only once, but found {waiter_count} occurrences in: {aws_ua_header}",
331 );
332 assert_eq!(1, s3_transfer_count, "S3Transfer metric should appear only once, but found {s3_transfer_count} occurrences in metrics section: {aws_ua_header}");
333 assert_eq!(1, credentials_code_count, "CredentialsCode metric should appear only once, but found {credentials_code_count} occurrences in metrics section: {aws_ua_header}");
334 }
335
336 #[test]
337 fn test_metrics_order_preserved() {
338 use aws_credential_types::credential_feature::AwsCredentialFeature;
339
340 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
341 let mut context = context();
342
343 let api_metadata = ApiMetadata::new("test-service", "1.0");
344 let mut layer = Layer::new("test");
345 layer.store_put(api_metadata);
346 layer.store_append(AwsCredentialFeature::CredentialsCode);
347 layer.store_append(AwsCredentialFeature::CredentialsEnvVars);
348 layer.store_append(AwsCredentialFeature::CredentialsProfile);
349 let mut config = ConfigBag::of_layers(vec![layer]);
350
351 let interceptor = UserAgentInterceptor::new();
352 let ctx = Into::into(&context);
353 interceptor
354 .read_after_serialization(&ctx, &rc, &mut config)
355 .unwrap();
356 let mut ctx = Into::into(&mut context);
357 interceptor
358 .modify_before_signing(&mut ctx, &rc, &mut config)
359 .unwrap();
360
361 let aws_ua_header = expect_header(&context, "x-amz-user-agent");
362 let metrics_section = aws_ua_header.split(" m/").nth(1).unwrap();
363
364 assert_eq!(
365 metrics_section, "e,g,n",
366 "AwsCredentialFeature metrics should preserve order"
367 );
368 }
369
370 #[test]
371 fn test_app_name() {
372 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
373 let mut context = context();
374
375 let api_metadata = ApiMetadata::new("some-service", "some-version");
376 let mut layer = Layer::new("test");
377 layer.store_put(api_metadata);
378 layer.store_put(AppName::new("my_awesome_app").unwrap());
379 let mut config = ConfigBag::of_layers(vec![layer]);
380
381 let interceptor = UserAgentInterceptor::new();
382 let ctx = Into::into(&context);
383 interceptor
384 .read_after_serialization(&ctx, &rc, &mut config)
385 .unwrap();
386 let mut ctx = Into::into(&mut context);
387 interceptor
388 .modify_before_signing(&mut ctx, &rc, &mut config)
389 .unwrap();
390
391 let app_value = "app/my_awesome_app";
392 let header = expect_header(&context, "user-agent");
393 assert!(
394 !header.contains(app_value),
395 "expected `{header}` to not contain `{app_value}`"
396 );
397
398 let header = expect_header(&context, "x-amz-user-agent");
399 assert!(
400 header.contains(app_value),
401 "expected `{header}` to contain `{app_value}`"
402 );
403 }
404
405 #[test]
406 fn test_api_metadata_missing() {
407 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
408 let context = context();
409 let mut config = ConfigBag::base();
410
411 let interceptor = UserAgentInterceptor::new();
412 let ctx = Into::into(&context);
413
414 let error = format!(
415 "{}",
416 DisplayErrorContext(
417 &*interceptor
418 .read_after_serialization(&ctx, &rc, &mut config)
419 .expect_err("it should error")
420 )
421 );
422 assert!(
423 error.contains("This is a bug"),
424 "`{error}` should contain message `This is a bug`"
425 );
426 }
427
428 #[test]
429 fn test_api_metadata_missing_with_ua_override() {
430 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
431 let mut context = context();
432
433 let mut layer = Layer::new("test");
434 layer.store_put(AwsUserAgent::for_tests());
435 let mut config = ConfigBag::of_layers(vec![layer]);
436
437 let interceptor = UserAgentInterceptor::new();
438 let mut ctx = Into::into(&mut context);
439
440 interceptor
441 .modify_before_signing(&mut ctx, &rc, &mut config)
442 .expect("it should succeed");
443
444 let header = expect_header(&context, "user-agent");
445 assert_eq!(AwsUserAgent::for_tests().ua_header(), header);
446 assert!(!header.contains("unused"));
447
448 assert_eq!(
449 AwsUserAgent::for_tests().aws_ua_header(),
450 expect_header(&context, "x-amz-user-agent")
451 );
452 }
453
454 #[test]
455 fn test_cfg_load_captures_all_feature_layers() {
456 use crate::sdk_feature::AwsSdkFeature;
457
458 let mut base_layer = Layer::new("base");
460 base_layer.store_append(AwsSdkFeature::EndpointOverride);
461
462 let mut config = ConfigBag::of_layers(vec![base_layer]);
463
464 config
466 .interceptor_state()
467 .store_append(AwsSdkFeature::SsoLoginDevice);
468
469 let all_features: Vec<&AwsSdkFeature> = config.load::<AwsSdkFeature>().collect();
471
472 assert_eq!(
473 all_features.len(),
474 2,
475 "cfg.load() should capture features from all layers"
476 );
477 assert!(
478 all_features.contains(&&AwsSdkFeature::EndpointOverride),
479 "should contain feature from base layer"
480 );
481 assert!(
482 all_features.contains(&&AwsSdkFeature::SsoLoginDevice),
483 "should contain feature from interceptor_state"
484 );
485 }
486}