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_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
28#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this
29const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent");
30
31#[derive(Debug)]
32enum UserAgentInterceptorError {
33    MissingApiMetadata,
34    InvalidHeaderValue(InvalidHeaderValue),
35    InvalidMetadataValue(InvalidMetadataValue),
36}
37
38impl std::error::Error for UserAgentInterceptorError {
39    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
40        match self {
41            Self::InvalidHeaderValue(source) => Some(source),
42            Self::InvalidMetadataValue(source) => Some(source),
43            Self::MissingApiMetadata => None,
44        }
45    }
46}
47
48impl fmt::Display for UserAgentInterceptorError {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.write_str(match self {
51            Self::InvalidHeaderValue(_) => "AwsUserAgent generated an invalid HTTP header value. This is a bug. Please file an issue.",
52            Self::InvalidMetadataValue(_) => "AwsUserAgent generated an invalid metadata value. This is a bug. Please file an issue.",
53            Self::MissingApiMetadata => "The UserAgentInterceptor requires ApiMetadata to be set before the request is made. This is a bug. Please file an issue.",
54        })
55    }
56}
57
58impl From<InvalidHeaderValue> for UserAgentInterceptorError {
59    fn from(err: InvalidHeaderValue) -> Self {
60        UserAgentInterceptorError::InvalidHeaderValue(err)
61    }
62}
63
64impl From<InvalidMetadataValue> for UserAgentInterceptorError {
65    fn from(err: InvalidMetadataValue) -> Self {
66        UserAgentInterceptorError::InvalidMetadataValue(err)
67    }
68}
69
70/// Generates and attaches the AWS SDK's user agent to a HTTP request
71#[non_exhaustive]
72#[derive(Debug, Default)]
73pub struct UserAgentInterceptor;
74
75impl UserAgentInterceptor {
76    /// Creates a new `UserAgentInterceptor`
77    pub fn new() -> Self {
78        UserAgentInterceptor
79    }
80}
81
82fn header_values(
83    ua: &AwsUserAgent,
84) -> Result<(HeaderValue, HeaderValue), UserAgentInterceptorError> {
85    // Pay attention to the extremely subtle difference between ua_header and aws_ua_header below...
86    Ok((
87        HeaderValue::try_from(ua.ua_header())?,
88        HeaderValue::try_from(ua.aws_ua_header())?,
89    ))
90}
91
92impl Intercept for UserAgentInterceptor {
93    fn name(&self) -> &'static str {
94        "UserAgentInterceptor"
95    }
96
97    fn read_after_serialization(
98        &self,
99        _context: &BeforeTransmitInterceptorContextRef<'_>,
100        _runtime_components: &RuntimeComponents,
101        cfg: &mut ConfigBag,
102    ) -> Result<(), BoxError> {
103        // Allow for overriding the user agent by an earlier interceptor (so, for example,
104        // tests can use `AwsUserAgent::for_tests()`) by attempting to grab one out of the
105        // config bag before creating one.
106        if cfg.load::<AwsUserAgent>().is_some() {
107            return Ok(());
108        }
109
110        let api_metadata = cfg
111            .load::<ApiMetadata>()
112            .ok_or(UserAgentInterceptorError::MissingApiMetadata)?;
113        let mut ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata.clone());
114
115        let maybe_app_name = cfg.load::<AppName>();
116        if let Some(app_name) = maybe_app_name {
117            ua.set_app_name(app_name.clone());
118        }
119
120        cfg.interceptor_state().store_put(ua);
121
122        Ok(())
123    }
124
125    fn modify_before_signing(
126        &self,
127        context: &mut BeforeTransmitInterceptorContextMut<'_>,
128        runtime_components: &RuntimeComponents,
129        cfg: &mut ConfigBag,
130    ) -> Result<(), BoxError> {
131        let mut ua = cfg
132            .load::<AwsUserAgent>()
133            .expect("`AwsUserAgent should have been created in `read_before_execution`")
134            .clone();
135
136        // Load features from both the main config bag and interceptor_state
137        let smithy_sdk_features = cfg.load::<SmithySdkFeature>();
138        for smithy_sdk_feature in smithy_sdk_features {
139            smithy_sdk_feature
140                .provide_business_metric()
141                .map(|m| ua.add_business_metric(m));
142        }
143
144        let aws_sdk_features = cfg.load::<AwsSdkFeature>();
145        for aws_sdk_feature in aws_sdk_features {
146            aws_sdk_feature
147                .provide_business_metric()
148                .map(|m| ua.add_business_metric(m));
149        }
150
151        // Also load AWS SDK features from interceptor_state (where interceptors store them)
152        let aws_sdk_features_from_interceptor = cfg.interceptor_state().load::<AwsSdkFeature>();
153        for aws_sdk_feature in aws_sdk_features_from_interceptor {
154            aws_sdk_feature
155                .provide_business_metric()
156                .map(|m| ua.add_business_metric(m));
157        }
158
159        let aws_credential_features = cfg.load::<AwsCredentialFeature>();
160        for aws_credential_feature in aws_credential_features {
161            aws_credential_feature
162                .provide_business_metric()
163                .map(|m| ua.add_business_metric(m));
164        }
165
166        let maybe_connector_metadata = runtime_components
167            .http_client()
168            .and_then(|c| c.connector_metadata());
169        if let Some(connector_metadata) = maybe_connector_metadata {
170            let am = AdditionalMetadata::new(Cow::Owned(connector_metadata.to_string()))?;
171            ua.add_additional_metadata(am);
172        }
173
174        let headers = context.request_mut().headers_mut();
175        let (user_agent, x_amz_user_agent) = header_values(&ua)?;
176        headers.append(USER_AGENT, user_agent);
177        headers.append(X_AMZ_USER_AGENT, x_amz_user_agent);
178        Ok(())
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
186    use aws_smithy_runtime_api::client::interceptors::Intercept;
187    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
188    use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
189    use aws_smithy_types::config_bag::{ConfigBag, Layer};
190    use aws_smithy_types::error::display::DisplayErrorContext;
191
192    fn expect_header<'a>(context: &'a InterceptorContext, header_name: &str) -> &'a str {
193        context
194            .request()
195            .expect("request is set")
196            .headers()
197            .get(header_name)
198            .unwrap()
199    }
200
201    fn context() -> InterceptorContext {
202        let mut context = InterceptorContext::new(Input::doesnt_matter());
203        context.enter_serialization_phase();
204        context.set_request(HttpRequest::empty());
205        let _ = context.take_input();
206        context.enter_before_transmit_phase();
207        context
208    }
209
210    #[test]
211    fn test_overridden_ua() {
212        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
213        let mut context = context();
214
215        let mut layer = Layer::new("test");
216        layer.store_put(AwsUserAgent::for_tests());
217        layer.store_put(ApiMetadata::new("unused", "unused"));
218        let mut cfg = ConfigBag::of_layers(vec![layer]);
219
220        let interceptor = UserAgentInterceptor::new();
221        let mut ctx = Into::into(&mut context);
222        interceptor
223            .modify_before_signing(&mut ctx, &rc, &mut cfg)
224            .unwrap();
225
226        let header = expect_header(&context, "user-agent");
227        assert_eq!(AwsUserAgent::for_tests().ua_header(), header);
228        assert!(!header.contains("unused"));
229
230        assert_eq!(
231            AwsUserAgent::for_tests().aws_ua_header(),
232            expect_header(&context, "x-amz-user-agent")
233        );
234    }
235
236    #[test]
237    fn test_default_ua() {
238        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
239        let mut context = context();
240
241        let api_metadata = ApiMetadata::new("some-service", "some-version");
242        let mut layer = Layer::new("test");
243        layer.store_put(api_metadata.clone());
244        let mut config = ConfigBag::of_layers(vec![layer]);
245
246        let interceptor = UserAgentInterceptor::new();
247        let ctx = Into::into(&context);
248        interceptor
249            .read_after_serialization(&ctx, &rc, &mut config)
250            .unwrap();
251        let mut ctx = Into::into(&mut context);
252        interceptor
253            .modify_before_signing(&mut ctx, &rc, &mut config)
254            .unwrap();
255
256        let expected_ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata);
257        assert!(
258            expected_ua.aws_ua_header().contains("some-service"),
259            "precondition"
260        );
261        assert_eq!(
262            expected_ua.ua_header(),
263            expect_header(&context, "user-agent")
264        );
265        assert_eq!(
266            expected_ua.aws_ua_header(),
267            expect_header(&context, "x-amz-user-agent")
268        );
269    }
270
271    #[test]
272    fn test_app_name() {
273        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
274        let mut context = context();
275
276        let api_metadata = ApiMetadata::new("some-service", "some-version");
277        let mut layer = Layer::new("test");
278        layer.store_put(api_metadata);
279        layer.store_put(AppName::new("my_awesome_app").unwrap());
280        let mut config = ConfigBag::of_layers(vec![layer]);
281
282        let interceptor = UserAgentInterceptor::new();
283        let ctx = Into::into(&context);
284        interceptor
285            .read_after_serialization(&ctx, &rc, &mut config)
286            .unwrap();
287        let mut ctx = Into::into(&mut context);
288        interceptor
289            .modify_before_signing(&mut ctx, &rc, &mut config)
290            .unwrap();
291
292        let app_value = "app/my_awesome_app";
293        let header = expect_header(&context, "user-agent");
294        assert!(
295            !header.contains(app_value),
296            "expected `{header}` to not contain `{app_value}`"
297        );
298
299        let header = expect_header(&context, "x-amz-user-agent");
300        assert!(
301            header.contains(app_value),
302            "expected `{header}` to contain `{app_value}`"
303        );
304    }
305
306    #[test]
307    fn test_api_metadata_missing() {
308        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
309        let context = context();
310        let mut config = ConfigBag::base();
311
312        let interceptor = UserAgentInterceptor::new();
313        let ctx = Into::into(&context);
314
315        let error = format!(
316            "{}",
317            DisplayErrorContext(
318                &*interceptor
319                    .read_after_serialization(&ctx, &rc, &mut config)
320                    .expect_err("it should error")
321            )
322        );
323        assert!(
324            error.contains("This is a bug"),
325            "`{error}` should contain message `This is a bug`"
326        );
327    }
328
329    #[test]
330    fn test_api_metadata_missing_with_ua_override() {
331        let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
332        let mut context = context();
333
334        let mut layer = Layer::new("test");
335        layer.store_put(AwsUserAgent::for_tests());
336        let mut config = ConfigBag::of_layers(vec![layer]);
337
338        let interceptor = UserAgentInterceptor::new();
339        let mut ctx = Into::into(&mut context);
340
341        interceptor
342            .modify_before_signing(&mut ctx, &rc, &mut config)
343            .expect("it should succeed");
344
345        let header = expect_header(&context, "user-agent");
346        assert_eq!(AwsUserAgent::for_tests().ua_header(), header);
347        assert!(!header.contains("unused"));
348
349        assert_eq!(
350            AwsUserAgent::for_tests().aws_ua_header(),
351            expect_header(&context, "x-amz-user-agent")
352        );
353    }
354}