use aws_smithy_types::config_bag::{Storable, StoreReplace};
use aws_types::app_name::AppName;
use aws_types::build_metadata::{OsFamily, BUILD_METADATA};
use aws_types::os_shim_internal::Env;
use std::borrow::Cow;
use std::error::Error;
use std::fmt;
mod interceptor;
mod metrics;
#[cfg(feature = "test-util")]
pub mod test_util;
const USER_AGENT_VERSION: &str = "2.1";
use crate::user_agent::metrics::BusinessMetrics;
pub use interceptor::UserAgentInterceptor;
pub use metrics::BusinessMetric;
#[derive(Clone, Debug)]
pub struct AwsUserAgent {
sdk_metadata: SdkMetadata,
ua_metadata: UaMetadata,
api_metadata: ApiMetadata,
os_metadata: OsMetadata,
language_metadata: LanguageMetadata,
exec_env_metadata: Option<ExecEnvMetadata>,
business_metrics: BusinessMetrics,
framework_metadata: Vec<FrameworkMetadata>,
app_name: Option<AppName>,
build_env_additional_metadata: Option<AdditionalMetadata>,
additional_metadata: Vec<AdditionalMetadata>,
}
impl AwsUserAgent {
pub fn new_from_environment(env: Env, api_metadata: ApiMetadata) -> Self {
let build_metadata = &BUILD_METADATA;
let sdk_metadata = SdkMetadata {
name: "rust",
version: build_metadata.core_pkg_version,
};
let ua_metadata = UaMetadata {
version: USER_AGENT_VERSION,
};
let os_metadata = OsMetadata {
os_family: &build_metadata.os_family,
version: None,
};
let exec_env_metadata = env
.get("AWS_EXECUTION_ENV")
.ok()
.map(|name| ExecEnvMetadata { name });
let build_env_additional_metadata = option_env!("AWS_SDK_RUST_BUILD_UA_METADATA")
.and_then(|value| AdditionalMetadata::new(value).ok());
AwsUserAgent {
sdk_metadata,
ua_metadata,
api_metadata,
os_metadata,
language_metadata: LanguageMetadata {
lang: "rust",
version: BUILD_METADATA.rust_version,
extras: Default::default(),
},
exec_env_metadata,
framework_metadata: Default::default(),
business_metrics: Default::default(),
app_name: Default::default(),
build_env_additional_metadata,
additional_metadata: Default::default(),
}
}
pub fn for_tests() -> Self {
Self {
sdk_metadata: SdkMetadata {
name: "rust",
version: "0.123.test",
},
ua_metadata: UaMetadata { version: "0.1" },
api_metadata: ApiMetadata {
service_id: "test-service".into(),
version: "0.123",
},
os_metadata: OsMetadata {
os_family: &OsFamily::Windows,
version: Some("XPSP3".to_string()),
},
language_metadata: LanguageMetadata {
lang: "rust",
version: "1.50.0",
extras: Default::default(),
},
exec_env_metadata: None,
business_metrics: Default::default(),
framework_metadata: Vec::new(),
app_name: None,
build_env_additional_metadata: None,
additional_metadata: Vec::new(),
}
}
#[deprecated(
since = "1.4.0",
note = "This is a no-op; use `with_business_metric` instead."
)]
#[allow(unused_mut)]
#[allow(deprecated)]
#[doc(hidden)]
pub fn with_feature_metadata(mut self, _metadata: FeatureMetadata) -> Self {
self
}
#[deprecated(
since = "1.4.0",
note = "This is a no-op; use `add_business_metric` instead."
)]
#[allow(deprecated)]
#[allow(unused_mut)]
#[doc(hidden)]
pub fn add_feature_metadata(&mut self, _metadata: FeatureMetadata) -> &mut Self {
self
}
#[deprecated(
since = "1.4.0",
note = "This is a no-op; use `with_business_metric` instead."
)]
#[allow(deprecated)]
#[allow(unused_mut)]
#[doc(hidden)]
pub fn with_config_metadata(mut self, _metadata: ConfigMetadata) -> Self {
self
}
#[deprecated(
since = "1.4.0",
note = "This is a no-op; use `add_business_metric` instead."
)]
#[allow(deprecated)]
#[allow(unused_mut)]
#[doc(hidden)]
pub fn add_config_metadata(&mut self, _metadata: ConfigMetadata) -> &mut Self {
self
}
#[doc(hidden)]
pub fn with_business_metric(mut self, metric: BusinessMetric) -> Self {
self.business_metrics.push(metric);
self
}
#[doc(hidden)]
pub fn add_business_metric(&mut self, metric: BusinessMetric) -> &mut Self {
self.business_metrics.push(metric);
self
}
#[doc(hidden)]
pub fn with_framework_metadata(mut self, metadata: FrameworkMetadata) -> Self {
self.framework_metadata.push(metadata);
self
}
#[doc(hidden)]
pub fn add_framework_metadata(&mut self, metadata: FrameworkMetadata) -> &mut Self {
self.framework_metadata.push(metadata);
self
}
pub fn with_additional_metadata(mut self, metadata: AdditionalMetadata) -> Self {
self.additional_metadata.push(metadata);
self
}
pub fn add_additional_metadata(&mut self, metadata: AdditionalMetadata) -> &mut Self {
self.additional_metadata.push(metadata);
self
}
pub fn with_app_name(mut self, app_name: AppName) -> Self {
self.app_name = Some(app_name);
self
}
pub fn set_app_name(&mut self, app_name: AppName) -> &mut Self {
self.app_name = Some(app_name);
self
}
pub fn aws_ua_header(&self) -> String {
let mut ua_value = String::new();
use std::fmt::Write;
write!(ua_value, "{} ", &self.sdk_metadata).unwrap();
write!(ua_value, "{} ", &self.ua_metadata).unwrap();
write!(ua_value, "{} ", &self.api_metadata).unwrap();
write!(ua_value, "{} ", &self.os_metadata).unwrap();
write!(ua_value, "{} ", &self.language_metadata).unwrap();
if let Some(ref env_meta) = self.exec_env_metadata {
write!(ua_value, "{} ", env_meta).unwrap();
}
if !self.business_metrics.is_empty() {
write!(ua_value, "{} ", &self.business_metrics).unwrap()
}
for framework in &self.framework_metadata {
write!(ua_value, "{} ", framework).unwrap();
}
for additional_metadata in &self.additional_metadata {
write!(ua_value, "{} ", additional_metadata).unwrap();
}
if let Some(app_name) = &self.app_name {
write!(ua_value, "app/{}", app_name).unwrap();
}
if let Some(additional_metadata) = &self.build_env_additional_metadata {
write!(ua_value, "{}", additional_metadata).unwrap();
}
if ua_value.ends_with(' ') {
ua_value.truncate(ua_value.len() - 1);
}
ua_value
}
pub fn ua_header(&self) -> String {
let mut ua_value = String::new();
use std::fmt::Write;
write!(ua_value, "{} ", &self.sdk_metadata).unwrap();
write!(ua_value, "{} ", &self.os_metadata).unwrap();
write!(ua_value, "{}", &self.language_metadata).unwrap();
ua_value
}
}
impl Storable for AwsUserAgent {
type Storer = StoreReplace<Self>;
}
#[derive(Clone, Copy, Debug)]
struct SdkMetadata {
name: &'static str,
version: &'static str,
}
impl fmt::Display for SdkMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "aws-sdk-{}/{}", self.name, self.version)
}
}
#[derive(Clone, Copy, Debug)]
struct UaMetadata {
version: &'static str,
}
impl fmt::Display for UaMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ua/{}", self.version)
}
}
#[derive(Clone, Debug)]
pub struct ApiMetadata {
service_id: Cow<'static, str>,
version: &'static str,
}
impl ApiMetadata {
pub const fn new(service_id: &'static str, version: &'static str) -> Self {
Self {
service_id: Cow::Borrowed(service_id),
version,
}
}
}
impl fmt::Display for ApiMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "api/{}/{}", self.service_id, self.version)
}
}
impl Storable for ApiMetadata {
type Storer = StoreReplace<Self>;
}
#[derive(Debug)]
#[non_exhaustive]
pub struct InvalidMetadataValue;
impl Error for InvalidMetadataValue {}
impl fmt::Display for InvalidMetadataValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"User agent metadata can only have alphanumeric characters, or any of \
'!' | '#' | '$' | '%' | '&' | '\\'' | '*' | '+' | '-' | \
'.' | '^' | '_' | '`' | '|' | '~'"
)
}
}
fn validate_metadata(value: Cow<'static, str>) -> Result<Cow<'static, str>, InvalidMetadataValue> {
fn valid_character(c: char) -> bool {
match c {
_ if c.is_ascii_alphanumeric() => true,
'!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|'
| '~' => true,
_ => false,
}
}
if !value.chars().all(valid_character) {
return Err(InvalidMetadataValue);
}
Ok(value)
}
#[doc(hidden)]
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct AdditionalMetadata {
value: Cow<'static, str>,
}
impl AdditionalMetadata {
pub fn new(value: impl Into<Cow<'static, str>>) -> Result<Self, InvalidMetadataValue> {
Ok(Self {
value: validate_metadata(value.into())?,
})
}
}
impl fmt::Display for AdditionalMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "md/{}", self.value)
}
}
#[derive(Clone, Debug, Default)]
struct AdditionalMetadataList(Vec<AdditionalMetadata>);
impl AdditionalMetadataList {
fn push(&mut self, metadata: AdditionalMetadata) {
self.0.push(metadata);
}
}
impl fmt::Display for AdditionalMetadataList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for metadata in &self.0 {
write!(f, " {}", metadata)?;
}
Ok(())
}
}
#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
#[doc(hidden)]
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct FeatureMetadata {
name: Cow<'static, str>,
version: Option<Cow<'static, str>>,
additional: AdditionalMetadataList,
}
#[allow(deprecated)]
impl FeatureMetadata {
pub fn new(
name: impl Into<Cow<'static, str>>,
version: Option<Cow<'static, str>>,
) -> Result<Self, InvalidMetadataValue> {
Ok(Self {
name: validate_metadata(name.into())?,
version: version.map(validate_metadata).transpose()?,
additional: Default::default(),
})
}
pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self {
self.additional.push(metadata);
self
}
}
#[allow(deprecated)]
impl fmt::Display for FeatureMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(version) = &self.version {
write!(f, "ft/{}/{}{}", self.name, version, self.additional)
} else {
write!(f, "ft/{}{}", self.name, self.additional)
}
}
}
#[deprecated(since = "1.4.0", note = "Replaced by `BusinessMetric`.")]
#[doc(hidden)]
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ConfigMetadata {
config: Cow<'static, str>,
value: Option<Cow<'static, str>>,
}
#[allow(deprecated)]
impl ConfigMetadata {
pub fn new(
config: impl Into<Cow<'static, str>>,
value: Option<Cow<'static, str>>,
) -> Result<Self, InvalidMetadataValue> {
Ok(Self {
config: validate_metadata(config.into())?,
value: value.map(validate_metadata).transpose()?,
})
}
}
#[allow(deprecated)]
impl fmt::Display for ConfigMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(value) = &self.value {
write!(f, "cfg/{}/{}", self.config, value)
} else {
write!(f, "cfg/{}", self.config)
}
}
}
#[doc(hidden)]
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct FrameworkMetadata {
name: Cow<'static, str>,
version: Option<Cow<'static, str>>,
additional: AdditionalMetadataList,
}
impl FrameworkMetadata {
pub fn new(
name: impl Into<Cow<'static, str>>,
version: Option<Cow<'static, str>>,
) -> Result<Self, InvalidMetadataValue> {
Ok(Self {
name: validate_metadata(name.into())?,
version: version.map(validate_metadata).transpose()?,
additional: Default::default(),
})
}
pub fn with_additional(mut self, metadata: AdditionalMetadata) -> Self {
self.additional.push(metadata);
self
}
}
impl fmt::Display for FrameworkMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(version) = &self.version {
write!(f, "lib/{}/{}{}", self.name, version, self.additional)
} else {
write!(f, "lib/{}{}", self.name, self.additional)
}
}
}
#[derive(Clone, Debug)]
struct OsMetadata {
os_family: &'static OsFamily,
version: Option<String>,
}
impl fmt::Display for OsMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let os_family = match self.os_family {
OsFamily::Windows => "windows",
OsFamily::Linux => "linux",
OsFamily::Macos => "macos",
OsFamily::Android => "android",
OsFamily::Ios => "ios",
OsFamily::Other => "other",
};
write!(f, "os/{}", os_family)?;
if let Some(ref version) = self.version {
write!(f, "/{}", version)?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
struct LanguageMetadata {
lang: &'static str,
version: &'static str,
extras: AdditionalMetadataList,
}
impl fmt::Display for LanguageMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "lang/{}/{}{}", self.lang, self.version, self.extras)
}
}
#[derive(Clone, Debug)]
struct ExecEnvMetadata {
name: String,
}
impl fmt::Display for ExecEnvMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "exec-env/{}", &self.name)
}
}
#[cfg(test)]
mod test {
use super::*;
use aws_types::app_name::AppName;
use aws_types::build_metadata::OsFamily;
use aws_types::os_shim_internal::Env;
use std::borrow::Cow;
fn make_deterministic(ua: &mut AwsUserAgent) {
ua.sdk_metadata.version = "0.1";
ua.ua_metadata.version = "0.1";
ua.language_metadata.version = "1.50.0";
ua.os_metadata.os_family = &OsFamily::Macos;
ua.os_metadata.version = Some("1.15".to_string());
}
#[test]
fn generate_a_valid_ua() {
let api_metadata = ApiMetadata {
service_id: "dynamodb".into(),
version: "123",
};
let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata);
make_deterministic(&mut ua);
assert_eq!(
ua.aws_ua_header(),
"aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0"
);
assert_eq!(
ua.ua_header(),
"aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
);
}
#[test]
fn generate_a_valid_ua_with_execution_env() {
let api_metadata = ApiMetadata {
service_id: "dynamodb".into(),
version: "123",
};
let mut ua = AwsUserAgent::new_from_environment(
Env::from_slice(&[("AWS_EXECUTION_ENV", "lambda")]),
api_metadata,
);
make_deterministic(&mut ua);
assert_eq!(
ua.aws_ua_header(),
"aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 exec-env/lambda"
);
assert_eq!(
ua.ua_header(),
"aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
);
}
#[test]
fn generate_a_valid_ua_with_frameworks() {
let api_metadata = ApiMetadata {
service_id: "dynamodb".into(),
version: "123",
};
let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
.with_framework_metadata(
FrameworkMetadata::new("some-framework", Some(Cow::Borrowed("1.3")))
.unwrap()
.with_additional(AdditionalMetadata::new("something").unwrap()),
)
.with_framework_metadata(FrameworkMetadata::new("other", None).unwrap());
make_deterministic(&mut ua);
assert_eq!(
ua.aws_ua_header(),
"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"
);
assert_eq!(
ua.ua_header(),
"aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
);
}
#[test]
fn generate_a_valid_ua_with_app_name() {
let api_metadata = ApiMetadata {
service_id: "dynamodb".into(),
version: "123",
};
let mut ua = AwsUserAgent::new_from_environment(Env::from_slice(&[]), api_metadata)
.with_app_name(AppName::new("my_app").unwrap());
make_deterministic(&mut ua);
assert_eq!(
ua.aws_ua_header(),
"aws-sdk-rust/0.1 ua/0.1 api/dynamodb/123 os/macos/1.15 lang/rust/1.50.0 app/my_app"
);
assert_eq!(
ua.ua_header(),
"aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0"
);
}
#[test]
fn generate_a_valid_ua_with_build_env_additional_metadata() {
let mut ua = AwsUserAgent::for_tests();
ua.build_env_additional_metadata = Some(AdditionalMetadata::new("asdf").unwrap());
assert_eq!(
ua.aws_ua_header(),
"aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 md/asdf"
);
assert_eq!(
ua.ua_header(),
"aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
);
}
#[test]
fn generate_a_valid_ua_with_business_metrics() {
{
let ua = AwsUserAgent::for_tests().with_business_metric(BusinessMetric::ResourceModel);
assert_eq!(
ua.aws_ua_header(),
"aws-sdk-rust/0.123.test ua/0.1 api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0 m/A"
);
assert_eq!(
ua.ua_header(),
"aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
);
}
{
let ua = AwsUserAgent::for_tests()
.with_business_metric(BusinessMetric::RetryModeAdaptive)
.with_business_metric(BusinessMetric::S3Transfer)
.with_business_metric(BusinessMetric::S3ExpressBucket);
assert_eq!(
ua.aws_ua_header(),
"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"
);
assert_eq!(
ua.ua_header(),
"aws-sdk-rust/0.123.test os/windows/XPSP3 lang/rust/1.50.0"
);
}
}
}