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