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 let mut ua_value = String::new();
278 use std::fmt::Write;
279 write!(ua_value, "{} ", &self.sdk_metadata).unwrap();
280 write!(ua_value, "{} ", &self.os_metadata).unwrap();
281 write!(ua_value, "{}", &self.language_metadata).unwrap();
282 ua_value
283 }
284}
285
286impl Storable for AwsUserAgent {
287 type Storer = StoreReplace<Self>;
288}
289
290#[derive(Clone, Copy, Debug)]
291struct SdkMetadata {
292 name: &'static str,
293 version: &'static str,
294}
295
296impl fmt::Display for SdkMetadata {
297 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298 write!(f, "aws-sdk-{}/{}", self.name, self.version)
299 }
300}
301
302#[derive(Clone, Copy, Debug)]
303struct UaMetadata {
304 version: &'static str,
305}
306
307impl fmt::Display for UaMetadata {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 write!(f, "ua/{}", self.version)
310 }
311}
312
313#[derive(Clone, Debug)]
315pub struct ApiMetadata {
316 service_id: Cow<'static, str>,
317 version: &'static str,
318}
319
320impl ApiMetadata {
321 pub const fn new(service_id: &'static str, version: &'static str) -> Self {
323 Self {
324 service_id: Cow::Borrowed(service_id),
325 version,
326 }
327 }
328}
329
330impl fmt::Display for ApiMetadata {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332 write!(f, "api/{}/{}", self.service_id, self.version)
333 }
334}
335
336impl Storable for ApiMetadata {
337 type Storer = StoreReplace<Self>;
338}
339
340#[derive(Debug)]
348#[non_exhaustive]
349pub struct InvalidMetadataValue;
350
351impl Error for InvalidMetadataValue {}
352
353impl fmt::Display for InvalidMetadataValue {
354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355 write!(
356 f,
357 "User agent metadata can only have alphanumeric characters, or any of \
358 '!' | '#' | '$' | '%' | '&' | '\\'' | '*' | '+' | '-' | \
359 '.' | '^' | '_' | '`' | '|' | '~'"
360 )
361 }
362}
363
364fn validate_metadata(value: Cow<'static, str>) -> Result<Cow<'static, str>, InvalidMetadataValue> {
365 fn valid_character(c: char) -> bool {
366 match c {
367 _ if c.is_ascii_alphanumeric() => true,
368 '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|'
369 | '~' => true,
370 _ => false,
371 }
372 }
373 if !value.chars().all(valid_character) {
374 return Err(InvalidMetadataValue);
375 }
376 Ok(value)
377}
378
379#[doc(hidden)]
380#[derive(Clone, Debug)]
382#[non_exhaustive]
383pub struct AdditionalMetadata {
384 value: Cow<'static, str>,
385}
386
387impl AdditionalMetadata {
388 pub fn new(value: impl Into<Cow<'static, str>>) -> Result<Self, InvalidMetadataValue> {
396 Ok(Self {
397 value: validate_metadata(value.into())?,
398 })
399 }
400}
401
402impl fmt::Display for AdditionalMetadata {
403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404 write!(f, "md/{}", self.value)
406 }
407}
408
409#[derive(Clone, Debug, Default)]
410struct AdditionalMetadataList(Vec<AdditionalMetadata>);
411
412impl AdditionalMetadataList {
413 fn push(&mut self, metadata: AdditionalMetadata) {
414 self.0.push(metadata);
415 }
416}
417
418impl fmt::Display for AdditionalMetadataList {
419 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420 for metadata in &self.0 {
421 write!(f, " {}", metadata)?;
422 }
423 Ok(())
424 }
425}
426
427#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
428#[doc(hidden)]
429#[derive(Clone, Debug)]
431#[non_exhaustive]
432pub struct FeatureMetadata {
433 name: Cow<'static, str>,
434 version: Option<Cow<'static, str>>,
435 additional: AdditionalMetadataList,
436}
437
438#[allow(deprecated)]
439impl FeatureMetadata {
440 pub fn new(
448 name: impl Into<Cow<'static, str>>,
449 version: Option<Cow<'static, str>>,
450 ) -> Result<Self, InvalidMetadataValue> {
451 Ok(Self {
452 name: validate_metadata(name.into())?,
453 version: version.map(validate_metadata).transpose()?,
454 additional: Default::default(),
455 })
456 }
457
458 pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self {
460 self.additional.push(metadata);
461 self
462 }
463}
464
465#[allow(deprecated)]
466impl fmt::Display for FeatureMetadata {
467 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
468 if let Some(version) = &self.version {
470 write!(f, "ft/{}/{}{}", self.name, version, self.additional)
471 } else {
472 write!(f, "ft/{}{}", self.name, self.additional)
473 }
474 }
475}
476
477#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
478#[doc(hidden)]
479#[derive(Clone, Debug)]
481#[non_exhaustive]
482pub struct ConfigMetadata {
483 config: Cow<'static, str>,
484 value: Option<Cow<'static, str>>,
485}
486
487#[allow(deprecated)]
488impl ConfigMetadata {
489 pub fn new(
497 config: impl Into<Cow<'static, str>>,
498 value: Option<Cow<'static, str>>,
499 ) -> Result<Self, InvalidMetadataValue> {
500 Ok(Self {
501 config: validate_metadata(config.into())?,
502 value: value.map(validate_metadata).transpose()?,
503 })
504 }
505}
506
507#[allow(deprecated)]
508impl fmt::Display for ConfigMetadata {
509 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510 if let Some(value) = &self.value {
512 write!(f, "cfg/{}/{}", self.config, value)
513 } else {
514 write!(f, "cfg/{}", self.config)
515 }
516 }
517}
518
519#[doc(hidden)]
520#[derive(Clone, Debug)]
522#[non_exhaustive]
523pub struct FrameworkMetadata {
524 name: Cow<'static, str>,
525 version: Option<Cow<'static, str>>,
526 additional: AdditionalMetadataList,
527}
528
529impl FrameworkMetadata {
530 pub fn new(
538 name: impl Into<Cow<'static, str>>,
539 version: Option<Cow<'static, str>>,
540 ) -> Result<Self, InvalidMetadataValue> {
541 Ok(Self {
542 name: validate_metadata(name.into())?,
543 version: version.map(validate_metadata).transpose()?,
544 additional: Default::default(),
545 })
546 }
547
548 pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self {
550 self.additional.push(metadata);
551 self
552 }
553}
554
555impl fmt::Display for FrameworkMetadata {
556 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
557 if let Some(version) = &self.version {
559 write!(f, "lib/{}/{}{}", self.name, version, self.additional)
560 } else {
561 write!(f, "lib/{}{}", self.name, self.additional)
562 }
563 }
564}
565
566#[derive(Clone, Debug)]
567struct OsMetadata {
568 os_family: &'static OsFamily,
569 version: Option<String>,
570}
571
572impl fmt::Display for OsMetadata {
573 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
574 let os_family = match self.os_family {
575 OsFamily::Windows => "windows",
576 OsFamily::Linux => "linux",
577 OsFamily::Macos => "macos",
578 OsFamily::Android => "android",
579 OsFamily::Ios => "ios",
580 OsFamily::Other => "other",
581 };
582 write!(f, "os/{}", os_family)?;
583 if let Some(ref version) = self.version {
584 write!(f, "/{}", version)?;
585 }
586 Ok(())
587 }
588}
589
590#[derive(Clone, Debug)]
591struct LanguageMetadata {
592 lang: &'static str,
593 version: &'static str,
594 extras: AdditionalMetadataList,
595}
596impl fmt::Display for LanguageMetadata {
597 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598 write!(f, "lang/{}/{}{}", self.lang, self.version, self.extras)
600 }
601}
602
603#[derive(Clone, Debug)]
604struct ExecEnvMetadata {
605 name: String,
606}
607impl fmt::Display for ExecEnvMetadata {
608 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
609 write!(f, "exec-env/{}", &self.name)
610 }
611}
612
613#[cfg(test)]
614mod test {
615 use super::*;
616 use aws_types::app_name::AppName;
617 use aws_types::build_metadata::OsFamily;
618 use aws_types::os_shim_internal::Env;
619 use std::borrow::Cow;
620
621 fn make_deterministic(ua: &mut AwsUserAgent) {
622 ua.sdk_metadata.version = "0.1";
624 ua.ua_metadata.version = "0.1";
625 ua.language_metadata.version = "1.50.0";
626 ua.os_metadata.os_family = &OsFamily::Macos;
627 ua.os_metadata.version = Some("1.15".to_string());
628 }
629
630 #[test]
631 fn generate_a_valid_ua() {
632 let api_metadata = ApiMetadata {
633 service_id: "dynamodb".into(),
634 version: "123",
635 };
636 let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata);
637 make_deterministic(&mut ua);
638 assert_eq!(
639 ua.aws_ua_header(),
640 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0"
641 );
642 assert_eq!(
643 ua.ua_header(),
644 "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
645 );
646 }
647
648 #[test]
649 fn generate_a_valid_ua_with_execution_env() {
650 let api_metadata = ApiMetadata {
651 service_id: "dynamodb".into(),
652 version: "123",
653 };
654 let mut ua = AwsUserAgent::new_from_environment(
655 Env::from_slice(&[("AWS_EXECUTION_ENV", "lambda")]),
656 api_metadata,
657 );
658 make_deterministic(&mut ua);
659 assert_eq!(
660 ua.aws_ua_header(),
661 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda"
662 );
663 assert_eq!(
664 ua.ua_header(),
665 "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
666 );
667 }
668
669 #[test]
670 fn generate_a_valid_ua_with_frameworks() {
671 let api_metadata = ApiMetadata {
672 service_id: "dynamodb".into(),
673 version: "123",
674 };
675 let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
676 .with_framework_metadata(
677 FrameworkMetadata::new("some-framework", Some(Cow::Borrowed("1.3")))
678 .unwrap()
679 .with_additional(AdditionalMetadata::new("something").unwrap()),
680 )
681 .with_framework_metadata(FrameworkMetadata::new("other", None).unwrap());
682 make_deterministic(&mut ua);
683 assert_eq!(
684 ua.aws_ua_header(),
685 "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"
686 );
687 assert_eq!(
688 ua.ua_header(),
689 "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
690 );
691 }
692
693 #[test]
694 fn generate_a_valid_ua_with_app_name() {
695 let api_metadata = ApiMetadata {
696 service_id: "dynamodb".into(),
697 version: "123",
698 };
699 let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
700 .with_app_name(AppName::new("my_app").unwrap());
701 make_deterministic(&mut ua);
702 assert_eq!(
703 ua.aws_ua_header(),
704 "aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app"
705 );
706 assert_eq!(
707 ua.ua_header(),
708 "aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
709 );
710 }
711
712 #[test]
713 fn generate_a_valid_ua_with_build_env_additional_metadata() {
714 let mut ua = AwsUserAgent::for_tests();
715 ua.build_env_additional_metadata = Some(AdditionalMetadata::new("asdf").unwrap());
716 assert_eq!(
717 ua.aws_ua_header(),
718 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf"
719 );
720 assert_eq!(
721 ua.ua_header(),
722 "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
723 );
724 }
725
726 #[test]
727 fn generate_a_valid_ua_with_business_metrics() {
728 {
730 let ua = AwsUserAgent::for_tests().with_business_metric(BusinessMetric::ResourceModel);
731 assert_eq!(
732 ua.aws_ua_header(),
733 "aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
734 );
735 assert_eq!(
736 ua.ua_header(),
737 "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
738 );
739 }
740 {
742 let ua = AwsUserAgent::for_tests()
743 .with_business_metric(BusinessMetric::RetryModeAdaptive)
744 .with_business_metric(BusinessMetric::S3Transfer)
745 .with_business_metric(BusinessMetric::S3ExpressBucket);
746 assert_eq!(
747 ua.aws_ua_header(),
748 "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"
749 );
750 assert_eq!(
751 ua.ua_header(),
752 "aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
753 );
754 }
755 }
756}
757
758