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>();
137 for smithy_sdk_feature in smithy_sdk_features {
138 smithy_sdk_feature
139 .provide_business_metric()
140 .map(|m| ua.add_business_metric(m));
141 }
142
143 let aws_sdk_features = cfg.load::<AwsSdkFeature>();
144 for aws_sdk_feature in aws_sdk_features {
145 aws_sdk_feature
146 .provide_business_metric()
147 .map(|m| ua.add_business_metric(m));
148 }
149
150 let aws_credential_features = cfg.load::<AwsCredentialFeature>();
151 for aws_credential_feature in aws_credential_features {
152 aws_credential_feature
153 .provide_business_metric()
154 .map(|m| ua.add_business_metric(m));
155 }
156
157 let maybe_connector_metadata = runtime_components
158 .http_client()
159 .and_then(|c| c.connector_metadata());
160 if let Some(connector_metadata) = maybe_connector_metadata {
161 let am = AdditionalMetadata::new(Cow::Owned(connector_metadata.to_string()))?;
162 ua.add_additional_metadata(am);
163 }
164
165 let headers = context.request_mut().headers_mut();
166 let (user_agent, x_amz_user_agent) = header_values(&ua)?;
167 headers.append(USER_AGENT, user_agent);
168 headers.append(X_AMZ_USER_AGENT, x_amz_user_agent);
169 Ok(())
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
177 use aws_smithy_runtime_api::client::interceptors::Intercept;
178 use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
179 use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
180 use aws_smithy_types::config_bag::{ConfigBag, Layer};
181 use aws_smithy_types::error::display::DisplayErrorContext;
182
183 fn expect_header<'a>(context: &'a InterceptorContext, header_name: &str) -> &'a str {
184 context
185 .request()
186 .expect("request is set")
187 .headers()
188 .get(header_name)
189 .unwrap()
190 }
191
192 fn context() -> InterceptorContext {
193 let mut context = InterceptorContext::new(Input::doesnt_matter());
194 context.enter_serialization_phase();
195 context.set_request(HttpRequest::empty());
196 let _ = context.take_input();
197 context.enter_before_transmit_phase();
198 context
199 }
200
201 #[test]
202 fn test_overridden_ua() {
203 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
204 let mut context = context();
205
206 let mut layer = Layer::new("test");
207 layer.store_put(AwsUserAgent::for_tests());
208 layer.store_put(ApiMetadata::new("unused", "unused"));
209 let mut cfg = ConfigBag::of_layers(vec![layer]);
210
211 let interceptor = UserAgentInterceptor::new();
212 let mut ctx = Into::into(&mut context);
213 interceptor
214 .modify_before_signing(&mut ctx, &rc, &mut cfg)
215 .unwrap();
216
217 let header = expect_header(&context, "user-agent");
218 assert_eq!(AwsUserAgent::for_tests().ua_header(), header);
219 assert!(!header.contains("unused"));
220
221 assert_eq!(
222 AwsUserAgent::for_tests().aws_ua_header(),
223 expect_header(&context, "x-amz-user-agent")
224 );
225 }
226
227 #[test]
228 fn test_default_ua() {
229 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
230 let mut context = context();
231
232 let api_metadata = ApiMetadata::new("some-service", "some-version");
233 let mut layer = Layer::new("test");
234 layer.store_put(api_metadata.clone());
235 let mut config = ConfigBag::of_layers(vec![layer]);
236
237 let interceptor = UserAgentInterceptor::new();
238 let ctx = Into::into(&context);
239 interceptor
240 .read_after_serialization(&ctx, &rc, &mut config)
241 .unwrap();
242 let mut ctx = Into::into(&mut context);
243 interceptor
244 .modify_before_signing(&mut ctx, &rc, &mut config)
245 .unwrap();
246
247 let expected_ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata);
248 assert!(
249 expected_ua.aws_ua_header().contains("some-service"),
250 "precondition"
251 );
252 assert_eq!(
253 expected_ua.ua_header(),
254 expect_header(&context, "user-agent")
255 );
256 assert_eq!(
257 expected_ua.aws_ua_header(),
258 expect_header(&context, "x-amz-user-agent")
259 );
260 }
261
262 #[test]
263 fn test_app_name() {
264 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
265 let mut context = context();
266
267 let api_metadata = ApiMetadata::new("some-service", "some-version");
268 let mut layer = Layer::new("test");
269 layer.store_put(api_metadata);
270 layer.store_put(AppName::new("my_awesome_app").unwrap());
271 let mut config = ConfigBag::of_layers(vec![layer]);
272
273 let interceptor = UserAgentInterceptor::new();
274 let ctx = Into::into(&context);
275 interceptor
276 .read_after_serialization(&ctx, &rc, &mut config)
277 .unwrap();
278 let mut ctx = Into::into(&mut context);
279 interceptor
280 .modify_before_signing(&mut ctx, &rc, &mut config)
281 .unwrap();
282
283 let app_value = "app/my_awesome_app";
284 let header = expect_header(&context, "user-agent");
285 assert!(
286 !header.contains(app_value),
287 "expected `{header}` to not contain `{app_value}`"
288 );
289
290 let header = expect_header(&context, "x-amz-user-agent");
291 assert!(
292 header.contains(app_value),
293 "expected `{header}` to contain `{app_value}`"
294 );
295 }
296
297 #[test]
298 fn test_api_metadata_missing() {
299 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
300 let context = context();
301 let mut config = ConfigBag::base();
302
303 let interceptor = UserAgentInterceptor::new();
304 let ctx = Into::into(&context);
305
306 let error = format!(
307 "{}",
308 DisplayErrorContext(
309 &*interceptor
310 .read_after_serialization(&ctx, &rc, &mut config)
311 .expect_err("it should error")
312 )
313 );
314 assert!(
315 error.contains("This is a bug"),
316 "`{error}` should contain message `This is a bug`"
317 );
318 }
319
320 #[test]
321 fn test_api_metadata_missing_with_ua_override() {
322 let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
323 let mut context = context();
324
325 let mut layer = Layer::new("test");
326 layer.store_put(AwsUserAgent::for_tests());
327 let mut config = ConfigBag::of_layers(vec![layer]);
328
329 let interceptor = UserAgentInterceptor::new();
330 let mut ctx = Into::into(&mut context);
331
332 interceptor
333 .modify_before_signing(&mut ctx, &rc, &mut config)
334 .expect("it should succeed");
335
336 let header = expect_header(&context, "user-agent");
337 assert_eq!(AwsUserAgent::for_tests().ua_header(), header);
338 assert!(!header.contains("unused"));
339
340 assert_eq!(
341 AwsUserAgent::for_tests().aws_ua_header(),
342 expect_header(&context, "x-amz-user-agent")
343 );
344 }
345}