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