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