use aws_smithy_http::middleware::MapRequest;
use aws_smithy_http::operation::Request;
use aws_types::build_metadata::{OsFamily, BUILD_METADATA};
use http::header::{HeaderName, InvalidHeaderValue, USER_AGENT};
use http::HeaderValue;
use std::borrow::Cow;
use std::convert::TryFrom;
use std::fmt;
use std::fmt::{Display, Formatter};
use thiserror::Error;
#[derive(Clone)]
pub struct AwsUserAgent {
sdk_metadata: SdkMetadata,
api_metadata: ApiMetadata,
os_metadata: OsMetadata,
language_metadata: LanguageMetadata,
exec_env_metadata: Option<ExecEnvMetadata>,
additional_metadata: Vec<AdditionalMetadata>,
}
impl AwsUserAgent {
pub const fn new_from_environment(api_metadata: ApiMetadata) -> Self {
let build_metadata = &BUILD_METADATA;
let sdk_metadata = SdkMetadata {
name: "rust",
version: build_metadata.core_pkg_version,
};
let os_metadata = OsMetadata {
os_family: &build_metadata.os_family,
version: None,
};
AwsUserAgent {
sdk_metadata,
api_metadata,
os_metadata,
language_metadata: LanguageMetadata {
lang: "rust",
version: BUILD_METADATA.rust_version,
extras: vec![],
},
exec_env_metadata: None,
additional_metadata: vec![],
}
}
pub fn for_tests() -> Self {
Self {
sdk_metadata: SdkMetadata {
name: "rust",
version: "0.123.test",
},
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: vec![],
},
exec_env_metadata: None,
additional_metadata: vec![],
}
}
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.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 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();
for metadata in &self.additional_metadata {
write!(ua_value, " {}", metadata).unwrap();
}
ua_value
}
pub fn add_metadata(&mut self, additional_metadata: AdditionalMetadata) {
self.additional_metadata.push(additional_metadata);
}
}
#[derive(Clone, Copy)]
pub struct SdkMetadata {
name: &'static str,
version: &'static str,
}
impl Display for SdkMetadata {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "aws-sdk-{}/{}", self.name, self.version)
}
}
#[derive(Clone)]
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 Display for ApiMetadata {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "api/{}/{}", self.service_id, self.version)
}
}
#[derive(Clone)]
pub struct AdditionalMetadata {
key: String,
value: String,
}
impl AdditionalMetadata {
pub const fn new(key: String, value: String) -> Self {
Self { key, value }
}
}
impl Display for AdditionalMetadata {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", &self.key, &self.value)
}
}
#[derive(Clone)]
struct OsMetadata {
os_family: &'static OsFamily,
version: Option<String>,
}
impl Display for OsMetadata {
fn fmt(&self, f: &mut 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)]
struct LanguageMetadata {
lang: &'static str,
version: &'static str,
extras: Vec<AdditionalMetadata>,
}
impl Display for LanguageMetadata {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "lang/{}/{}", self.lang, self.version)?;
for extra in &self.extras {
write!(f, " md/{}/{}", &extra.key, &extra.value)?;
}
Ok(())
}
}
#[derive(Clone)]
struct ExecEnvMetadata {
name: String,
}
impl Display for ExecEnvMetadata {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "exec-env/{}", &self.name)
}
}
#[non_exhaustive]
#[derive(Default, Clone, Debug)]
pub struct UserAgentStage;
impl UserAgentStage {
pub fn new() -> Self {
Self
}
}
#[derive(Debug, Error)]
pub enum UserAgentStageError {
#[error("User agent missing from property bag")]
UserAgentMissing,
#[error("Provided user agent header was invalid")]
InvalidHeader(#[from] InvalidHeaderValue),
}
lazy_static::lazy_static! {
static ref X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent");
}
impl MapRequest for UserAgentStage {
type Error = UserAgentStageError;
fn apply(&self, request: Request) -> Result<Request, Self::Error> {
request.augment(|mut req, conf| {
let ua = conf
.get::<AwsUserAgent>()
.ok_or(UserAgentStageError::UserAgentMissing)?;
req.headers_mut()
.append(USER_AGENT, HeaderValue::try_from(ua.ua_header())?);
req.headers_mut().append(
X_AMZ_USER_AGENT.clone(),
HeaderValue::try_from(ua.aws_ua_header())?,
);
Ok(req)
})
}
}
#[cfg(test)]
mod test {
use crate::user_agent::X_AMZ_USER_AGENT;
use crate::user_agent::{AdditionalMetadata, ApiMetadata, AwsUserAgent, UserAgentStage};
use aws_smithy_http::body::SdkBody;
use aws_smithy_http::middleware::MapRequest;
use aws_smithy_http::operation;
use aws_types::build_metadata::OsFamily;
use http::header::USER_AGENT;
#[test]
fn generate_a_valid_ua() {
let api_metadata = ApiMetadata {
service_id: "dynamodb".into(),
version: "123",
};
let mut ua = AwsUserAgent::new_from_environment(api_metadata);
ua.sdk_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());
assert_eq!(
ua.aws_ua_header(),
"aws-sdk-rust/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_additional_metadata() {
let api_metadata = ApiMetadata {
service_id: "dynamodb".into(),
version: "123",
};
let mut ua = AwsUserAgent::new_from_environment(api_metadata);
ua.sdk_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());
let additional_metadata_0 = AdditionalMetadata {
key: "key_0".to_string(),
value: "val_0".to_string(),
};
ua.add_metadata(additional_metadata_0);
assert_eq!(
ua.aws_ua_header(),
"aws-sdk-rust/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 key_0/val_0"
);
let additional_metadata_1 = AdditionalMetadata {
key: "key_1".to_string(),
value: "val_1".to_string(),
};
ua.add_metadata(additional_metadata_1);
assert_eq!(
ua.ua_header(),
"aws-sdk-rust/0.1 os/macos/1.15 lang/rust/1.50.0 key_0/val_0 key_1/val_1"
);
}
#[test]
fn ua_stage_adds_headers() {
let stage = UserAgentStage::new();
let req = operation::Request::new(http::Request::new(SdkBody::from("some body")));
stage
.apply(req)
.expect_err("adding UA should fail without a UA set");
let mut req = operation::Request::new(http::Request::new(SdkBody::from("some body")));
req.properties_mut()
.insert(AwsUserAgent::new_from_environment(ApiMetadata {
service_id: "dynamodb".into(),
version: "0.123",
}));
let req = stage.apply(req).expect("setting user agent should succeed");
let (req, _) = req.into_parts();
req.headers()
.get(USER_AGENT)
.expect("UA header should be set");
req.headers()
.get(&*X_AMZ_USER_AGENT)
.expect("UA header should be set");
}
}