1use aws_smithy_types::config_bag::{Storable, StoreReplace};
7use aws_types::app_name::AppName;
8use aws_types::build_metadata::{OsFamily, BUILD_METADATA};
9use aws_types::os_shim_internal::Env;
10use std::borrow::Cow;
11use std::error::Error;
12use std::fmt;
13
14mod interceptor;
15mod metrics;
16#[cfg(feature = "test-util")]
17pub mod test_util;
18
19const USER_AGENT_VERSION: &str = "2.1";
20
21use crate::user_agent::metrics::BusinessMetrics;
22pub use interceptor::UserAgentInterceptor;
23pub use metrics::BusinessMetric;
24
25#[derive(Clone, Debug)]
31pub struct AwsUserAgent {
32 sdk_metadata: SdkMetadata,
33 ua_metadata: UaMetadata,
34 api_metadata: ApiMetadata,
35 os_metadata: OsMetadata,
36 language_metadata: LanguageMetadata,
37 exec_env_metadata: Option<ExecEnvMetadata>,
38 business_metrics: BusinessMetrics,
39 framework_metadata: Vec<FrameworkMetadata>,
40 app_name: Option<AppName>,
41 build_env_additional_metadata: Option<AdditionalMetadata>,
42 additional_metadata: Vec<AdditionalMetadata>,
43}
44
45impl AwsUserAgent {
46 pub fn new_from_environment(env: Env, api_metadata: ApiMetadata) -> Self {
52 let build_metadata = &BUILD_METADATA;
53 let sdk_metadata = SdkMetadata {
54 name: "rust",
55 version: build_metadata.core_pkg_version,
56 };
57 let ua_metadata = UaMetadata {
58 version: USER_AGENT_VERSION,
59 };
60 let os_metadata = OsMetadata {
61 os_family: &build_metadata.os_family,
62 version: None,
63 };
64 let exec_env_metadata = env
65 .get("AWS_EXECUTION_ENV")
66 .ok()
67 .map(|name| ExecEnvMetadata { name });
68
69 let build_env_additional_metadata = option_env!("AWS_SDK_RUST_BUILD_UA_METADATA")
71 .and_then(|value| AdditionalMetadata::new(value).ok());
72
73 AwsUserAgent {
74 sdk_metadata,
75 ua_metadata,
76 api_metadata,
77 os_metadata,
78 language_metadata: LanguageMetadata {
79 lang: "rust",
80 version: BUILD_METADATA.rust_version,
81 extras: Default::default(),
82 },
83 exec_env_metadata,
84 framework_metadata: Default::default(),
85 business_metrics: Default::default(),
86 app_name: Default::default(),
87 build_env_additional_metadata,
88 additional_metadata: Default::default(),
89 }
90 }
91
92 pub fn for_tests() -> Self {
96 Self {
97 sdk_metadata: SdkMetadata {
98 name: "rust",
99 version: "0.123.test",
100 },
101 ua_metadata: UaMetadata { version: "0.1" },
102 api_metadata: ApiMetadata {
103 service_id: "test-service".into(),
104 version: "0.123",
105 },
106 os_metadata: OsMetadata {
107 os_family: &OsFamily::Windows,
108 version: Some("XPSP3".to_string()),
109 },
110 language_metadata: LanguageMetadata {
111 lang: "rust",
112 version: "1.50.0",
113 extras: Default::default(),
114 },
115 exec_env_metadata: None,
116 business_metrics: Default::default(),
117 framework_metadata: Vec::new(),
118 app_name: None,
119 build_env_additional_metadata: None,
120 additional_metadata: Vec::new(),
121 }
122 }
123
124 #[deprecated(
125 since = "1.4.0",
126 note = "This is a no-op; use `with_business_metric` instead."
127 )]
128 #[allow(unused_mut)]
129 #[allow(deprecated)]
130 #[doc(hidden)]
131 pub fn with_feature_metadata(mut self, _metadata: FeatureMetadata) -> Self {
133 self
134 }
135
136 #[deprecated(
137 since = "1.4.0",
138 note = "This is a no-op; use `add_business_metric` instead."
139 )]
140 #[allow(deprecated)]
141 #[allow(unused_mut)]
142 #[doc(hidden)]
143 pub fn add_feature_metadata(&mut self, _metadata: FeatureMetadata) -> &mut Self {
145 self
146 }
147
148 #[deprecated(
149 since = "1.4.0",
150 note = "This is a no-op; use `with_business_metric` instead."
151 )]
152 #[allow(deprecated)]
153 #[allow(unused_mut)]
154 #[doc(hidden)]
155 pub fn with_config_metadata(mut self, _metadata: ConfigMetadata) -> Self {
157 self
158 }
159
160 #[deprecated(
161 since = "1.4.0",
162 note = "This is a no-op; use `add_business_metric` instead."
163 )]
164 #[allow(deprecated)]
165 #[allow(unused_mut)]
166 #[doc(hidden)]
167 pub fn add_config_metadata(&mut self, _metadata: ConfigMetadata) -> &mut Self {
169 self
170 }
171
172 #[doc(hidden)]
173 pub fn with_business_metric(mut self, metric: BusinessMetric) -> Self {
175 self.business_metrics.push(metric);
176 self
177 }
178
179 #[doc(hidden)]
180 pub fn add_business_metric(&mut self, metric: BusinessMetric) -> &mut Self {
182 self.business_metrics.push(metric);
183 self
184 }
185
186 #[doc(hidden)]
187 pub fn with_framework_metadata(mut self, metadata: FrameworkMetadata) -> Self {
189 self.framework_metadata.push(metadata);
190 self
191 }
192
193 #[doc(hidden)]
194 pub fn add_framework_metadata(&mut self, metadata: FrameworkMetadata) -> &mut Self {
196 self.framework_metadata.push(metadata);
197 self
198 }
199
200 pub fn with_additional_metadata(mut self, metadata: AdditionalMetadata) -> Self {
202 self.additional_metadata.push(metadata);
203 self
204 }
205
206 pub fn add_additional_metadata(&mut self, metadata: AdditionalMetadata) -> &mut Self {
208 self.additional_metadata.push(metadata);
209 self
210 }
211
212 pub fn with_app_name(mut self, app_name: AppName) -> Self {
214 self.app_name = Some(app_name);
215 self
216 }
217
218 pub fn set_app_name(&mut self, app_name: AppName) -> &mut Self {
220 self.app_name = Some(app_name);
221 self
222 }
223
224 pub fn aws_ua_header(&self) -> String {
228 let mut ua_value = String::new();
242 use std::fmt::Write;
243 write!(ua_value, "{} ", &self.sdk_metadata).unwrap();
245 write!(ua_value, "{} ", &self.ua_metadata).unwrap();
246 write!(ua_value, "{} ", &self.api_metadata).unwrap();
247 write!(ua_value, "{} ", &self.os_metadata).unwrap();
248 write!(ua_value, "{} ", &self.language_metadata).unwrap();
249 if let Some(ref env_meta) = self.exec_env_metadata {
250 write!(ua_value, "{env_meta} ").unwrap();
251 }
252 if !self.business_metrics.is_empty() {
253 write!(ua_value, "{} ", &self.business_metrics).unwrap()
254 }
255 for framework in &self.framework_metadata {
256 write!(ua_value, "{framework} ").unwrap();
257 }
258 for additional_metadata in &self.additional_metadata {
259 write!(ua_value, "{additional_metadata} ").unwrap();
260 }
261 if let Some(app_name) = &self.app_name {
262 write!(ua_value, "app/{app_name}").unwrap();
263 }
264 if let Some(additional_metadata) = &self.build_env_additional_metadata {
265 write!(ua_value, "{additional_metadata}").unwrap();
266 }
267 if ua_value.ends_with(' ') {
268 ua_value.truncate(ua_value.len() - 1);
269 }
270 ua_value
271 }
272
273 pub fn ua_header(&self) -> String {
277 self.aws_ua_header()
278 }
279}
280
281impl Storable for AwsUserAgent {
282 type Storer = StoreReplace<Self>;
283}
284
285#[derive(Clone, Copy, Debug)]
286struct SdkMetadata {
287 name: &'static str,
288 version: &'static str,
289}
290
291impl fmt::Display for SdkMetadata {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 write!(f, "aws-sdk-{}/{}", self.name, self.version)
294 }
295}
296
297#[derive(Clone, Copy, Debug)]
298struct UaMetadata {
299 version: &'static str,
300}
301
302impl fmt::Display for UaMetadata {
303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304 write!(f, "ua/{}", self.version)
305 }
306}
307
308#[derive(Clone, Debug)]
310pub struct ApiMetadata {
311 service_id: Cow<'static, str>,
312 version: &'static str,
313}
314
315impl ApiMetadata {
316 pub const fn new(service_id: &'static str, version: &'static str) -> Self {
318 Self {
319 service_id: Cow::Borrowed(service_id),
320 version,
321 }
322 }
323}
324
325impl fmt::Display for ApiMetadata {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 write!(f, "api/{}/{}", self.service_id, self.version)
328 }
329}
330
331impl Storable for ApiMetadata {
332 type Storer = StoreReplace<Self>;
333}
334
335#[derive(Debug)]
343#[non_exhaustive]
344pub struct InvalidMetadataValue;
345
346impl Error for InvalidMetadataValue {}
347
348impl fmt::Display for InvalidMetadataValue {
349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350 write!(
351 f,
352 "User agent metadata can only have alphanumeric characters, or any of \
353 '!' | '#' | '$' | '%' | '&' | '\\'' | '*' | '+' | '-' | \
354 '.' | '^' | '_' | '`' | '|' | '~'"
355 )
356 }
357}
358
359fn validate_metadata(value: Cow<'static, str>) -> Result<Cow<'static, str>, InvalidMetadataValue> {
360 fn valid_character(c: char) -> bool {
361 match c {
362 _ if c.is_ascii_alphanumeric() => true,
363 '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|'
364 | '~' => true,
365 _ => false,
366 }
367 }
368 if !value.chars().all(valid_character) {
369 return Err(InvalidMetadataValue);
370 }
371 Ok(value)
372}
373
374#[doc(hidden)]
375#[derive(Clone, Debug)]
377#[non_exhaustive]
378pub struct AdditionalMetadata {
379 value: Cow<'static, str>,
380}
381
382impl AdditionalMetadata {
383 pub fn new(value: impl Into<Cow<'static, str>>) -> Result<Self, InvalidMetadataValue> {
391 Ok(Self {
392 value: validate_metadata(value.into())?,
393 })
394 }
395}
396
397impl fmt::Display for AdditionalMetadata {
398 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399 write!(f, "md/{}", self.value)
401 }
402}
403
404#[derive(Clone, Debug, Default)]
405struct AdditionalMetadataList(Vec<AdditionalMetadata>);
406
407impl AdditionalMetadataList {
408 fn push(&mut self, metadata: AdditionalMetadata) {
409 self.0.push(metadata);
410 }
411}
412
413impl fmt::Display for AdditionalMetadataList {
414 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415 for metadata in &self.0 {
416 write!(f, " {metadata}")?;
417 }
418 Ok(())
419 }
420}
421
422#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
423#[doc(hidden)]
424#[derive(Clone, Debug)]
426#[non_exhaustive]
427pub struct FeatureMetadata {
428 name: Cow<'static, str>,
429 version: Option<Cow<'static, str>>,
430 additional: AdditionalMetadataList,
431}
432
433#[allow(deprecated)]
434impl FeatureMetadata {
435 pub fn new(
443 name: impl Into<Cow<'static, str>>,
444 version: Option<Cow<'static, str>>,
445 ) -> Result<Self, InvalidMetadataValue> {
446 Ok(Self {
447 name: validate_metadata(name.into())?,
448 version: version.map(validate_metadata).transpose()?,
449 additional: Default::default(),
450 })
451 }
452
453 pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self {
455 self.additional.push(metadata);
456 self
457 }
458}
459
460#[allow(deprecated)]
461impl fmt::Display for FeatureMetadata {
462 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463 if let Some(version) = &self.version {
465 write!(f, "ft/{}/{}{}", self.name, version, self.additional)
466 } else {
467 write!(f, "ft/{}{}", self.name, self.additional)
468 }
469 }
470}
471
472#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
473#[doc(hidden)]
474#[derive(Clone, Debug)]
476#[non_exhaustive]
477pub struct ConfigMetadata {
478 config: Cow<'static, str>,
479 value: Option<Cow<'static, str>>,
480}
481
482#[allow(deprecated)]
483impl ConfigMetadata {
484 pub fn new(
492 config: impl Into<Cow<'static, str>>,
493 value: Option<Cow<'static, str>>,
494 ) -> Result<Self, InvalidMetadataValue> {
495 Ok(Self {
496 config: validate_metadata(config.into())?,
497 value: value.map(validate_metadata).transpose()?,
498 })
499 }
500}
501
502#[allow(deprecated)]
503impl fmt::Display for ConfigMetadata {
504 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505 if let Some(value) = &self.value {
507 write!(f, "cfg/{}/{}", self.config, value)
508 } else {
509 write!(f, "cfg/{}", self.config)
510 }
511 }
512}
513
514#[doc(hidden)]
515#[derive(Clone, Debug)]
517#[non_exhaustive]
518pub struct FrameworkMetadata {
519 name: Cow<'static, str>,
520 version: Option<Cow<'static, str>>,
521 additional: AdditionalMetadataList,
522}
523
524impl FrameworkMetadata {
525 pub fn new(
533 name: impl Into<Cow<'static, str>>,
534 version: Option<Cow<'static, str>>,
535 ) -> Result<Self, InvalidMetadataValue> {
536 Ok(Self {
537 name: validate_metadata(name.into())?,
538 version: version.map(validate_metadata).transpose()?,
539 additional: Default::default(),
540 })
541 }
542
543 pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self {
545 self.additional.push(metadata);
546 self
547 }
548}
549
550impl fmt::Display for FrameworkMetadata {
551 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
552 if let Some(version) = &self.version {
554 write!(f, "lib/{}/{}{}", self.name, version, self.additional)
555 } else {
556 write!(f, "lib/{}{}", self.name, self.additional)
557 }
558 }
559}
560
561#[derive(Clone, Debug)]
562struct OsMetadata {
563 os_family: &'static OsFamily,
564 version: Option<String>,
565}
566
567impl fmt::Display for OsMetadata {
568 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569 let os_family = match self.os_family {
570 OsFamily::Windows => "windows",
571 OsFamily::Linux => "linux",
572 OsFamily::Macos => "macos",
573 OsFamily::Android => "android",
574 OsFamily::Ios => "ios",
575 OsFamily::Other => "other",
576 };
577 write!(f, "os/{os_family}")?;
578 if let Some(ref version) = self.version {
579 write!(f, "/{version}")?;
580 }
581 Ok(())
582 }
583}
584
585#[derive(Clone, Debug)]
586struct LanguageMetadata {
587 lang: &'static str,
588 version: &'static str,
589 extras: AdditionalMetadataList,
590}
591impl fmt::Display for LanguageMetadata {
592 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593 write!(f, "lang/{}/{}{}", self.lang, self.version, self.extras)
595 }
596}
597
598#[derive(Clone, Debug)]
599struct ExecEnvMetadata {
600 name: String,
601}
602impl fmt::Display for ExecEnvMetadata {
603 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
604 write!(f, "exec-env/{}", &self.name)
605 }
606}
607
608#[cfg(test)]
609mod test {
610 use super::*;
611 use aws_types::app_name::AppName;
612 use aws_types::build_metadata::OsFamily;
613 use aws_types::os_shim_internal::Env;
614 use std::borrow::Cow;
615
616 fn make_deterministic(ua: &mut AwsUserAgent) {
617 ua.sdk_metadata.version = "0.1";
619 ua.ua_metadata.version = "0.1";
620 ua.language_metadata.version = "1.50.0";
621 ua.os_metadata.os_family = &OsFamily::Macos;
622 ua.os_metadata.version = Some("1.15".to_string());
623 }
624
625 #[test]
626 fn generate_a_valid_ua() {
627 let api_metadata = ApiMetadata {
628 service_id: "dynamodb".into(),
629 version: "123",
630 };
631 let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata);
632 make_deterministic(&mut ua);
633 assert_eq!(
634 ua.aws_ua_header(),
635 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0"
636 );
637 assert_eq!(
638 ua.ua_header(),
639 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0"
640 );
641 }
642
643 #[test]
644 fn generate_a_valid_ua_with_execution_env() {
645 let api_metadata = ApiMetadata {
646 service_id: "dynamodb".into(),
647 version: "123",
648 };
649 let mut ua = AwsUserAgent::new_from_environment(
650 Env::from_slice(&[("AWS_EXECUTION_ENV", "lambda")]),
651 api_metadata,
652 );
653 make_deterministic(&mut ua);
654 assert_eq!(
655 ua.aws_ua_header(),
656 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda"
657 );
658 assert_eq!(
659 ua.ua_header(),
660 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda"
661 );
662 }
663
664 #[test]
665 fn generate_a_valid_ua_with_frameworks() {
666 let api_metadata = ApiMetadata {
667 service_id: "dynamodb".into(),
668 version: "123",
669 };
670 let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
671 .with_framework_metadata(
672 FrameworkMetadata::new("some-framework", Some(Cow::Borrowed("1.3")))
673 .unwrap()
674 .with_additional(AdditionalMetadata::new("something").unwrap()),
675 )
676 .with_framework_metadata(FrameworkMetadata::new("other", None).unwrap());
677 make_deterministic(&mut ua);
678 assert_eq!(
679 ua.aws_ua_header(),
680 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 lib/some-framework/1.3 md/something lib/other"
681 );
682 assert_eq!(
683 ua.ua_header(),
684 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 lib/some-framework/1.3 md/something lib/other"
685 );
686 }
687
688 #[test]
689 fn generate_a_valid_ua_with_app_name() {
690 let api_metadata = ApiMetadata {
691 service_id: "dynamodb".into(),
692 version: "123",
693 };
694 let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
695 .with_app_name(AppName::new("my_app").unwrap());
696 make_deterministic(&mut ua);
697 assert_eq!(
698 ua.aws_ua_header(),
699 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app"
700 );
701 assert_eq!(
702 ua.ua_header(),
703 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app"
704 );
705 }
706
707 #[test]
708 fn generate_a_valid_ua_with_build_env_additional_metadata() {
709 let mut ua = AwsUserAgent::for_tests();
710 ua.build_env_additional_metadata = Some(AdditionalMetadata::new("asdf").unwrap());
711 assert_eq!(
712 ua.aws_ua_header(),
713 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf"
714 );
715 assert_eq!(
716 ua.ua_header(),
717 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf"
718 );
719 }
720
721 #[test]
722 fn generate_a_valid_ua_with_business_metrics() {
723 {
725 let ua = AwsUserAgent::for_tests().with_business_metric(BusinessMetric::ResourceModel);
726 assert_eq!(
727 ua.aws_ua_header(),
728 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
729 );
730 assert_eq!(
731 ua.ua_header(),
732 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
733 );
734 }
735 {
737 let ua = AwsUserAgent::for_tests()
738 .with_business_metric(BusinessMetric::RetryModeAdaptive)
739 .with_business_metric(BusinessMetric::S3Transfer)
740 .with_business_metric(BusinessMetric::S3ExpressBucket);
741 assert_eq!(
742 ua.aws_ua_header(),
743 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/F,G,J"
744 );
745 assert_eq!(
746 ua.ua_header(),
747 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/F,G,J"
748 );
749 }
750 }
751}
752
753