1use std::borrow::Cow;
7use std::fmt;
8
9use http_1x::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::{dyn_dispatch_hint, 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
122#[dyn_dispatch_hint]
123impl Intercept for UserAgentInterceptor {
124 fn name(&self) -> &'static str {
125 "UserAgentInterceptor"
126 }
127
128 fn read_after_serialization(
129 &self,
130 _context: &BeforeTransmitInterceptorContextRef<'_>,
131 _runtime_components: &RuntimeComponents,
132 cfg: &mut ConfigBag,
133 ) -> Result<(), BoxError> {
134 if cfg.load::<AwsUserAgent>().is_some() {
138 return Ok(());
139 }
140
141 let api_metadata = cfg
142 .load::<ApiMetadata>()
143 .ok_or(UserAgentInterceptorError::MissingApiMetadata)?;
144 let mut ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata.clone());
145
146 let maybe_app_name = cfg.load::<AppName>();
147 if let Some(app_name) = maybe_app_name {
148 ua.set_app_name(app_name.clone());
149 }
150
151 cfg.interceptor_state().store_put(ua);
152
153 Ok(())
154 }
155
156 fn modify_before_signing(
157 &self,
158 context: &mut BeforeTransmitInterceptorContextMut<'_>,
159 runtime_components: &RuntimeComponents,
160 cfg: &mut ConfigBag,
161 ) -> Result<(), BoxError> {
162 let mut ua = cfg
163 .load::<AwsUserAgent>()
164 .expect("`AwsUserAgent should have been created in `read_before_execution`")
165 .clone();
166
167 let mut added_metrics = std::collections::HashSet::new();
168
169 add_metrics_unique!(cfg.load::<SmithySdkFeature>(), &mut ua, &mut added_metrics);
170 add_metrics_unique!(cfg.load::<AwsSdkFeature>(), &mut ua, &mut added_metrics);
171 add_metrics_unique_reverse!(
174 cfg.load::<AwsCredentialFeature>(),
175 &mut ua,
176 &mut added_metrics
177 );
178
179 let maybe_connector_metadata = runtime_components
180 .http_client()
181 .and_then(|c| c.connector_metadata());
182 if let Some(connector_metadata) = maybe_connector_metadata {
183 let am = AdditionalMetadata::new(Cow::Owned(connector_metadata.to_string()))?;
184 ua.add_additional_metadata(am);
185 }
186
187 let headers = context.request_mut().headers_mut();
188 let (user_agent, x_amz_user_agent) = header_values(&ua)?;
189 headers.append(USER_AGENT, user_agent);
190 headers.append(X_AMZ_USER_AGENT, x_amz_user_agent);
191 Ok(())
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
199 use aws_smithy_runtime_api::client::interceptors::Intercept;
200 use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
201 use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
202 use aws_smithy_types::config_bag::{ConfigBag, Layer};
203 use aws_smithy_types::error::display::DisplayErrorContext;
204
205 fn expect_header<'a>(context: &'a InterceptorContext, header_name: &str) -> &'a str {
206 context
207 .request()
208 .expect("request is set")
209 .headers()
210 .get(header_name)
211 .unwrap()
212 }
213
214 fn context() -> InterceptorContext {
215 let mut context = InterceptorContext::new(Input::doesnt_matter());
216 context.enter_serialization_phase();
217 context.set_request(HttpRequest::empty());
218 let _ = context.take_input();
219 context.enter_before_transmit_phase();
220 context
221 }
222
223 #[test]
224 fn test_overridden_ua() {
225 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
226 let mut context = context();
227
228 let mut layer = Layer::new("test");
229 layer.store_put(AwsUserAgent::for_tests());
230 layer.store_put(ApiMetadata::new("unused", "unused"));
231 let mut cfg = ConfigBag::of_layers(vec![layer]);
232
233 let interceptor = UserAgentInterceptor::new();
234 let mut ctx = Into::into(&mut context);
235 interceptor
236 .modify_before_signing(&mut ctx, &rc, &mut cfg)
237 .unwrap();
238
239 let header = expect_header(&context, "user-agent");
240 assert_eq!(AwsUserAgent::for_tests().ua_header(), header);
241 assert!(!header.contains("unused"));
242
243 assert_eq!(
244 AwsUserAgent::for_tests().aws_ua_header(),
245 expect_header(&context, "x-amz-user-agent")
246 );
247 }
248
249 #[test]
250 fn test_default_ua() {
251 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
252 let mut context = context();
253
254 let api_metadata = ApiMetadata::new("some-service", "some-version");
255 let mut layer = Layer::new("test");
256 layer.store_put(api_metadata.clone());
257 let mut config = ConfigBag::of_layers(vec![layer]);
258
259 let interceptor = UserAgentInterceptor::new();
260 let ctx = Into::into(&context);
261 interceptor
262 .read_after_serialization(&ctx, &rc, &mut config)
263 .unwrap();
264 let mut ctx = Into::into(&mut context);
265 interceptor
266 .modify_before_signing(&mut ctx, &rc, &mut config)
267 .unwrap();
268
269 let expected_ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata);
270 assert!(
271 expected_ua.aws_ua_header().contains("some-service"),
272 "precondition"
273 );
274 assert_eq!(
275 expected_ua.ua_header(),
276 expect_header(&context, "user-agent")
277 );
278 assert_eq!(
279 expected_ua.aws_ua_header(),
280 expect_header(&context, "x-amz-user-agent")
281 );
282 }
283
284 #[test]
285 fn test_modify_before_signing_no_duplicate_metrics() {
286 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
287 let mut context = context();
288
289 let api_metadata = ApiMetadata::new("test-service", "1.0");
290 let mut layer = Layer::new("test");
291 layer.store_put(api_metadata);
292 layer.store_append(SmithySdkFeature::Waiter);
294 layer.store_append(SmithySdkFeature::Waiter);
295 layer.store_append(AwsSdkFeature::S3Transfer);
296 layer.store_append(AwsSdkFeature::S3Transfer);
297 layer.store_append(AwsCredentialFeature::CredentialsCode);
298 layer.store_append(AwsCredentialFeature::CredentialsCode);
299 let mut config = ConfigBag::of_layers(vec![layer]);
300
301 let interceptor = UserAgentInterceptor::new();
302 let ctx = Into::into(&context);
303 interceptor
304 .read_after_serialization(&ctx, &rc, &mut config)
305 .unwrap();
306 let mut ctx = Into::into(&mut context);
307 interceptor
308 .modify_before_signing(&mut ctx, &rc, &mut config)
309 .unwrap();
310
311 let aws_ua_header = expect_header(&context, "x-amz-user-agent");
312 let metrics_section = aws_ua_header.split(" m/").nth(1).unwrap();
313 let waiter_count = metrics_section.matches("B").count();
314 let s3_transfer_count = metrics_section.matches("G").count();
315 let credentials_code_count = metrics_section.matches("e").count();
316 assert_eq!(
317 1, waiter_count,
318 "Waiter metric should appear only once, but found {waiter_count} occurrences in: {aws_ua_header}",
319 );
320 assert_eq!(1, s3_transfer_count, "S3Transfer metric should appear only once, but found {s3_transfer_count} occurrences in metrics section: {aws_ua_header}");
321 assert_eq!(1, credentials_code_count, "CredentialsCode metric should appear only once, but found {credentials_code_count} occurrences in metrics section: {aws_ua_header}");
322 }
323
324 #[test]
325 fn test_metrics_order_preserved() {
326 use aws_credential_types::credential_feature::AwsCredentialFeature;
327
328 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
329 let mut context = context();
330
331 let api_metadata = ApiMetadata::new("test-service", "1.0");
332 let mut layer = Layer::new("test");
333 layer.store_put(api_metadata);
334 layer.store_append(AwsCredentialFeature::CredentialsCode);
335 layer.store_append(AwsCredentialFeature::CredentialsEnvVars);
336 layer.store_append(AwsCredentialFeature::CredentialsProfile);
337 let mut config = ConfigBag::of_layers(vec![layer]);
338
339 let interceptor = UserAgentInterceptor::new();
340 let ctx = Into::into(&context);
341 interceptor
342 .read_after_serialization(&ctx, &rc, &mut config)
343 .unwrap();
344 let mut ctx = Into::into(&mut context);
345 interceptor
346 .modify_before_signing(&mut ctx, &rc, &mut config)
347 .unwrap();
348
349 let aws_ua_header = expect_header(&context, "x-amz-user-agent");
350 let metrics_section = aws_ua_header.split(" m/").nth(1).unwrap();
351
352 assert_eq!(
353 metrics_section, "e,g,n",
354 "AwsCredentialFeature metrics should preserve order"
355 );
356 }
357
358 #[test]
359 fn test_app_name() {
360 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
361 let mut context = context();
362
363 let api_metadata = ApiMetadata::new("some-service", "some-version");
364 let mut layer = Layer::new("test");
365 layer.store_put(api_metadata);
366 layer.store_put(AppName::new("my_awesome_app").unwrap());
367 let mut config = ConfigBag::of_layers(vec![layer]);
368
369 let interceptor = UserAgentInterceptor::new();
370 let ctx = Into::into(&context);
371 interceptor
372 .read_after_serialization(&ctx, &rc, &mut config)
373 .unwrap();
374 let mut ctx = Into::into(&mut context);
375 interceptor
376 .modify_before_signing(&mut ctx, &rc, &mut config)
377 .unwrap();
378
379 let app_value = "app/my_awesome_app";
380 let header = expect_header(&context, "user-agent");
381 assert!(
382 !header.contains(app_value),
383 "expected `{header}` to not contain `{app_value}`"
384 );
385
386 let header = expect_header(&context, "x-amz-user-agent");
387 assert!(
388 header.contains(app_value),
389 "expected `{header}` to contain `{app_value}`"
390 );
391 }
392
393 #[test]
394 fn test_api_metadata_missing() {
395 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
396 let context = context();
397 let mut config = ConfigBag::base();
398
399 let interceptor = UserAgentInterceptor::new();
400 let ctx = Into::into(&context);
401
402 let error = format!(
403 "{}",
404 DisplayErrorContext(
405 &*interceptor
406 .read_after_serialization(&ctx, &rc, &mut config)
407 .expect_err("it should error")
408 )
409 );
410 assert!(
411 error.contains("This is a bug"),
412 "`{error}` should contain message `This is a bug`"
413 );
414 }
415
416 #[test]
417 fn test_api_metadata_missing_with_ua_override() {
418 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
419 let mut context = context();
420
421 let mut layer = Layer::new("test");
422 layer.store_put(AwsUserAgent::for_tests());
423 let mut config = ConfigBag::of_layers(vec![layer]);
424
425 let interceptor = UserAgentInterceptor::new();
426 let mut ctx = Into::into(&mut context);
427
428 interceptor
429 .modify_before_signing(&mut ctx, &rc, &mut config)
430 .expect("it should succeed");
431
432 let header = expect_header(&context, "user-agent");
433 assert_eq!(AwsUserAgent::for_tests().ua_header(), header);
434 assert!(!header.contains("unused"));
435
436 assert_eq!(
437 AwsUserAgent::for_tests().aws_ua_header(),
438 expect_header(&context, "x-amz-user-agent")
439 );
440 }
441}