use crate::user_agent::{ApiMetadata, AwsUserAgent};
use aws_smithy_runtime_api::box_error::BoxError;
use aws_smithy_runtime_api::client::interceptors::context::BeforeTransmitInterceptorContextMut;
use aws_smithy_runtime_api::client::interceptors::Intercept;
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
use aws_smithy_types::config_bag::ConfigBag;
use aws_types::app_name::AppName;
use aws_types::os_shim_internal::Env;
use http::header::{InvalidHeaderValue, USER_AGENT};
use http::{HeaderName, HeaderValue};
use std::borrow::Cow;
use std::fmt;
#[allow(clippy::declare_interior_mutable_const)] const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent");
#[derive(Debug)]
enum UserAgentInterceptorError {
MissingApiMetadata,
InvalidHeaderValue(InvalidHeaderValue),
}
impl std::error::Error for UserAgentInterceptorError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidHeaderValue(source) => Some(source),
Self::MissingApiMetadata => None,
}
}
}
impl fmt::Display for UserAgentInterceptorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::InvalidHeaderValue(_) => "AwsUserAgent generated an invalid HTTP header value. This is a bug. Please file an issue.",
Self::MissingApiMetadata => "The UserAgentInterceptor requires ApiMetadata to be set before the request is made. This is a bug. Please file an issue.",
})
}
}
impl From<InvalidHeaderValue> for UserAgentInterceptorError {
fn from(err: InvalidHeaderValue) -> Self {
UserAgentInterceptorError::InvalidHeaderValue(err)
}
}
#[non_exhaustive]
#[derive(Debug, Default)]
pub struct UserAgentInterceptor;
impl UserAgentInterceptor {
pub fn new() -> Self {
UserAgentInterceptor
}
}
fn header_values(
ua: &AwsUserAgent,
) -> Result<(HeaderValue, HeaderValue), UserAgentInterceptorError> {
Ok((
HeaderValue::try_from(ua.ua_header())?,
HeaderValue::try_from(ua.aws_ua_header())?,
))
}
impl Intercept for UserAgentInterceptor {
fn name(&self) -> &'static str {
"UserAgentInterceptor"
}
fn modify_before_signing(
&self,
context: &mut BeforeTransmitInterceptorContextMut<'_>,
_runtime_components: &RuntimeComponents,
cfg: &mut ConfigBag,
) -> Result<(), BoxError> {
let ua: Cow<'_, AwsUserAgent> = cfg
.load::<AwsUserAgent>()
.map(Cow::Borrowed)
.map(Result::<_, UserAgentInterceptorError>::Ok)
.unwrap_or_else(|| {
let api_metadata = cfg
.load::<ApiMetadata>()
.ok_or(UserAgentInterceptorError::MissingApiMetadata)?;
let mut ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata.clone());
let maybe_app_name = cfg.load::<AppName>();
if let Some(app_name) = maybe_app_name {
ua.set_app_name(app_name.clone());
}
Ok(Cow::Owned(ua))
})?;
let headers = context.request_mut().headers_mut();
let (user_agent, x_amz_user_agent) = header_values(&ua)?;
headers.append(USER_AGENT, user_agent);
headers.append(X_AMZ_USER_AGENT, x_amz_user_agent);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
use aws_smithy_runtime_api::client::interceptors::Intercept;
use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
use aws_smithy_types::config_bag::{ConfigBag, Layer};
use aws_smithy_types::error::display::DisplayErrorContext;
fn expect_header<'a>(context: &'a InterceptorContext, header_name: &str) -> &'a str {
context
.request()
.expect("request is set")
.headers()
.get(header_name)
.unwrap()
}
fn context() -> InterceptorContext {
let mut context = InterceptorContext::new(Input::doesnt_matter());
context.enter_serialization_phase();
context.set_request(HttpRequest::empty());
let _ = context.take_input();
context.enter_before_transmit_phase();
context
}
#[test]
fn test_overridden_ua() {
let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
let mut context = context();
let mut layer = Layer::new("test");
layer.store_put(AwsUserAgent::for_tests());
layer.store_put(ApiMetadata::new("unused", "unused"));
let mut cfg = ConfigBag::of_layers(vec![layer]);
let interceptor = UserAgentInterceptor::new();
let mut ctx = Into::into(&mut context);
interceptor
.modify_before_signing(&mut ctx, &rc, &mut cfg)
.unwrap();
let header = expect_header(&context, "user-agent");
assert_eq!(AwsUserAgent::for_tests().ua_header(), header);
assert!(!header.contains("unused"));
assert_eq!(
AwsUserAgent::for_tests().aws_ua_header(),
expect_header(&context, "x-amz-user-agent")
);
}
#[test]
fn test_default_ua() {
let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
let mut context = context();
let api_metadata = ApiMetadata::new("some-service", "some-version");
let mut layer = Layer::new("test");
layer.store_put(api_metadata.clone());
let mut config = ConfigBag::of_layers(vec![layer]);
let interceptor = UserAgentInterceptor::new();
let mut ctx = Into::into(&mut context);
interceptor
.modify_before_signing(&mut ctx, &rc, &mut config)
.unwrap();
let expected_ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata);
assert!(
expected_ua.aws_ua_header().contains("some-service"),
"precondition"
);
assert_eq!(
expected_ua.ua_header(),
expect_header(&context, "user-agent")
);
assert_eq!(
expected_ua.aws_ua_header(),
expect_header(&context, "x-amz-user-agent")
);
}
#[test]
fn test_app_name() {
let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
let mut context = context();
let api_metadata = ApiMetadata::new("some-service", "some-version");
let mut layer = Layer::new("test");
layer.store_put(api_metadata);
layer.store_put(AppName::new("my_awesome_app").unwrap());
let mut config = ConfigBag::of_layers(vec![layer]);
let interceptor = UserAgentInterceptor::new();
let mut ctx = Into::into(&mut context);
interceptor
.modify_before_signing(&mut ctx, &rc, &mut config)
.unwrap();
let app_value = "app/my_awesome_app";
let header = expect_header(&context, "user-agent");
assert!(
!header.contains(app_value),
"expected `{header}` to not contain `{app_value}`"
);
let header = expect_header(&context, "x-amz-user-agent");
assert!(
header.contains(app_value),
"expected `{header}` to contain `{app_value}`"
);
}
#[test]
fn test_api_metadata_missing() {
let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
let mut context = context();
let mut config = ConfigBag::base();
let interceptor = UserAgentInterceptor::new();
let mut ctx = Into::into(&mut context);
let error = format!(
"{}",
DisplayErrorContext(
&*interceptor
.modify_before_signing(&mut ctx, &rc, &mut config)
.expect_err("it should error")
)
);
assert!(
error.contains("This is a bug"),
"`{error}` should contain message `This is a bug`"
);
}
#[test]
fn test_api_metadata_missing_with_ua_override() {
let rc = RuntimeComponentsBuilder::for_tests().build().unwrap();
let mut context = context();
let mut layer = Layer::new("test");
layer.store_put(AwsUserAgent::for_tests());
let mut config = ConfigBag::of_layers(vec![layer]);
let interceptor = UserAgentInterceptor::new();
let mut ctx = Into::into(&mut context);
interceptor
.modify_before_signing(&mut ctx, &rc, &mut config)
.expect("it should succeed");
let header = expect_header(&context, "user-agent");
assert_eq!(AwsUserAgent::for_tests().ua_header(), header);
assert!(!header.contains("unused"));
assert_eq!(
AwsUserAgent::for_tests().aws_ua_header(),
expect_header(&context, "x-amz-user-agent")
);
}
}