Skip to main content

aws_runtime/user_agent/
interceptor.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use 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)] // we will never mutate this
59const 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/// Generates and attaches the AWS SDK's user agent to a HTTP request
101#[non_exhaustive]
102#[derive(Debug, Default)]
103pub struct UserAgentInterceptor;
104
105impl UserAgentInterceptor {
106    /// Creates a new `UserAgentInterceptor`
107    pub fn new() -> Self {
108        UserAgentInterceptor
109    }
110}
111
112fn header_values(
113    ua: &AwsUserAgent,
114) -> Result<(HeaderValue, HeaderValue), UserAgentInterceptorError> {
115    // Pay attention to the extremely subtle difference between ua_header and aws_ua_header below...
116    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        // Allow for overriding the user agent by an earlier interceptor (so, for example,
135        // tests can use `AwsUserAgent::for_tests()`) by attempting to grab one out of the
136        // config bag before creating one.
137        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        // The order we emit credential features matters.
172        // Reverse to preserve emission order since StoreAppend pops backwards.
173        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 ua_header = expect_header(&context, "user-agent");
240        assert_eq!(AwsUserAgent::for_tests().ua_header(), ua_header);
241        assert!(!ua_header.contains("unused"));
242
243        let aws_ua_header = expect_header(&context, "x-amz-user-agent");
244        assert_eq!(AwsUserAgent::for_tests().ua_header(), aws_ua_header);
245        assert!(!aws_ua_header.contains("unused"));
246    }
247
248    #[test]
249    fn test_default_ua() {
250        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
251        let mut context = context();
252
253        let api_metadata = ApiMetadata::new("some-service", "some-version");
254        let mut layer = Layer::new("test");
255        layer.store_put(api_metadata.clone());
256        let mut config = ConfigBag::of_layers(vec![layer]);
257
258        let interceptor = UserAgentInterceptor::new();
259        let ctx = Into::into(&context);
260        interceptor
261            .read_after_serialization(&ctx, &rc, &mut config)
262            .unwrap();
263        let mut ctx = Into::into(&mut context);
264        interceptor
265            .modify_before_signing(&mut ctx, &rc, &mut config)
266            .unwrap();
267
268        let expected_ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata);
269        assert!(
270            expected_ua.aws_ua_header().contains("some-service"),
271            "precondition"
272        );
273        assert_eq!(
274            expected_ua.ua_header(),
275            expect_header(&context, "user-agent")
276        );
277        assert_eq!(
278            expected_ua.aws_ua_header(),
279            expect_header(&context, "x-amz-user-agent")
280        );
281    }
282
283    #[test]
284    fn test_modify_before_signing_no_duplicate_metrics() {
285        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
286        let mut context = context();
287
288        let api_metadata = ApiMetadata::new("test-service", "1.0");
289        let mut layer = Layer::new("test");
290        layer.store_put(api_metadata);
291        // Duplicate features
292        layer.store_append(SmithySdkFeature::Waiter);
293        layer.store_append(SmithySdkFeature::Waiter);
294        layer.store_append(AwsSdkFeature::S3Transfer);
295        layer.store_append(AwsSdkFeature::S3Transfer);
296        layer.store_append(AwsCredentialFeature::CredentialsCode);
297        layer.store_append(AwsCredentialFeature::CredentialsCode);
298        let mut config = ConfigBag::of_layers(vec![layer]);
299
300        let interceptor = UserAgentInterceptor::new();
301        let ctx = Into::into(&context);
302        interceptor
303            .read_after_serialization(&ctx, &rc, &mut config)
304            .unwrap();
305        let mut ctx = Into::into(&mut context);
306        interceptor
307            .modify_before_signing(&mut ctx, &rc, &mut config)
308            .unwrap();
309
310        let ua_header = expect_header(&context, "x-amz-user-agent");
311        let aws_ua_header = expect_header(&context, "x-amz-user-agent");
312        let ua_metrics_section = ua_header.split(" m/").nth(1).unwrap();
313        let aws_ua_metrics_section = aws_ua_header.split(" m/").nth(1).unwrap();
314        assert_eq!(ua_metrics_section, aws_ua_metrics_section);
315
316        let waiter_count = ua_metrics_section.matches("B").count();
317        let s3_transfer_count = ua_metrics_section.matches("G").count();
318        let credentials_code_count = ua_metrics_section.matches("e").count();
319        assert_eq!(
320            1, waiter_count,
321            "Waiter metric should appear only once, but found {waiter_count} occurrences in: {aws_ua_header}",
322        );
323        assert_eq!(1, s3_transfer_count, "S3Transfer metric should appear only once, but found {s3_transfer_count} occurrences in metrics section: {aws_ua_header}");
324        assert_eq!(1, credentials_code_count, "CredentialsCode metric should appear only once, but found {credentials_code_count} occurrences in metrics section: {aws_ua_header}");
325    }
326
327    #[test]
328    fn test_metrics_order_preserved() {
329        use aws_credential_types::credential_feature::AwsCredentialFeature;
330
331        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
332        let mut context = context();
333
334        let api_metadata = ApiMetadata::new("test-service", "1.0");
335        let mut layer = Layer::new("test");
336        layer.store_put(api_metadata);
337        layer.store_append(AwsCredentialFeature::CredentialsCode);
338        layer.store_append(AwsCredentialFeature::CredentialsEnvVars);
339        layer.store_append(AwsCredentialFeature::CredentialsProfile);
340        let mut config = ConfigBag::of_layers(vec![layer]);
341
342        let interceptor = UserAgentInterceptor::new();
343        let ctx = Into::into(&context);
344        interceptor
345            .read_after_serialization(&ctx, &rc, &mut config)
346            .unwrap();
347        let mut ctx = Into::into(&mut context);
348        interceptor
349            .modify_before_signing(&mut ctx, &rc, &mut config)
350            .unwrap();
351
352        let ua_header = expect_header(&context, "user-agent");
353        let ua_metrics_section = ua_header.split(" m/").nth(1).unwrap();
354        assert_eq!(
355            ua_metrics_section, "e,g,n",
356            "AwsCredentialFeature metrics should preserve order"
357        );
358
359        let aws_ua_header = expect_header(&context, "x-amz-user-agent");
360        let aws_ua_metrics_section = aws_ua_header.split(" m/").nth(1).unwrap();
361        assert_eq!(
362            aws_ua_metrics_section, "e,g,n",
363            "AwsCredentialFeature metrics should preserve order"
364        );
365    }
366
367    #[test]
368    fn test_app_name() {
369        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
370        let mut context = context();
371
372        let api_metadata = ApiMetadata::new("some-service", "some-version");
373        let mut layer = Layer::new("test");
374        layer.store_put(api_metadata);
375        layer.store_put(AppName::new("my_awesome_app").unwrap());
376        let mut config = ConfigBag::of_layers(vec![layer]);
377
378        let interceptor = UserAgentInterceptor::new();
379        let ctx = Into::into(&context);
380        interceptor
381            .read_after_serialization(&ctx, &rc, &mut config)
382            .unwrap();
383        let mut ctx = Into::into(&mut context);
384        interceptor
385            .modify_before_signing(&mut ctx, &rc, &mut config)
386            .unwrap();
387
388        let app_value = "app/my_awesome_app";
389        let ua_header = expect_header(&context, "user-agent");
390        assert!(
391            ua_header.contains(app_value),
392            "expected `{ua_header}` to contain `{app_value}`"
393        );
394
395        let aws_ua_header = expect_header(&context, "x-amz-user-agent");
396        assert!(
397            aws_ua_header.contains(app_value),
398            "expected `{aws_ua_header}` to contain `{app_value}`"
399        );
400    }
401
402    #[test]
403    fn test_api_metadata_missing() {
404        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
405        let context = context();
406        let mut config = ConfigBag::base();
407
408        let interceptor = UserAgentInterceptor::new();
409        let ctx = Into::into(&context);
410
411        let error = format!(
412            "{}",
413            DisplayErrorContext(
414                &*interceptor
415                    .read_after_serialization(&ctx, &rc, &mut config)
416                    .expect_err("it should error")
417            )
418        );
419        assert!(
420            error.contains("This is a bug"),
421            "`{error}` should contain message `This is a bug`"
422        );
423    }
424
425    #[test]
426    fn test_api_metadata_missing_with_ua_override() {
427        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
428        let mut context = context();
429
430        let mut layer = Layer::new("test");
431        layer.store_put(AwsUserAgent::for_tests());
432        let mut config = ConfigBag::of_layers(vec![layer]);
433
434        let interceptor = UserAgentInterceptor::new();
435        let mut ctx = Into::into(&mut context);
436
437        interceptor
438            .modify_before_signing(&mut ctx, &rc, &mut config)
439            .expect("it should succeed");
440
441        let ua_header = expect_header(&context, "user-agent");
442        assert_eq!(AwsUserAgent::for_tests().ua_header(), ua_header);
443        assert!(!ua_header.contains("unused"));
444
445        let aws_ua_header = expect_header(&context, "x-amz-user-agent");
446        assert_eq!(AwsUserAgent::for_tests().ua_header(), aws_ua_header);
447        assert!(!aws_ua_header.contains("unused"));
448    }
449}