aws_runtime/user_agent/
interceptor.rs1use 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)] const 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#[non_exhaustive]
72#[derive(Debug, Default)]
73pub struct UserAgentInterceptor;
74
75impl UserAgentInterceptor {
76 pub fn new() -> Self {
78 UserAgentInterceptor
79 }
80}
81
82fn header_values(
83 ua: &AwsUserAgent,
84) -> Result<(HeaderValue, HeaderValue), UserAgentInterceptorError> {
85 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 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 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 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}