Unverified Commit 3ce52f89 authored by Zelda Hessler's avatar Zelda Hessler Committed by GitHub
Browse files

expose connector in user agent (#3667)

## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here -->
part of the `hyper` 1.0 upgrade

## Description
<!--- Describe your changes in detail -->
This PR adds information about the connector used to send a request to
the metadata included in the user agent.

## Testing
<!--- Please describe in detail how you tested your changes -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->
I added an integration test.

## Checklist
<!--- If a checkbox below is not applicable, then please DELETE it
rather than leaving it unchecked -->
- [ ] I have updated `CHANGELOG.next.toml` if I made changes to the
smithy-rs codegen or runtime crates
- [ ] I have updated `CHANGELOG.next.toml` if I made changes to the AWS
SDK, generated SDK code, or SDK runtime crates

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
parent 41b938df
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
[package]
name = "aws-runtime"
version = "1.2.2"
version = "1.2.3"
authors = ["AWS Rust SDK Team <aws-sdk-rust@amazon.com>"]
description = "Runtime support code for the AWS SDK. This crate isn't intended to be used directly."
edition = "2021"
+19 −0
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@ use std::error::Error;
use std::fmt;

mod interceptor;

pub use interceptor::UserAgentInterceptor;

/// AWS User Agent
@@ -31,6 +32,7 @@ pub struct AwsUserAgent {
    framework_metadata: Vec<FrameworkMetadata>,
    app_name: Option<AppName>,
    build_env_additional_metadata: Option<AdditionalMetadata>,
    additional_metadata: Vec<AdditionalMetadata>,
}

impl AwsUserAgent {
@@ -73,6 +75,7 @@ impl AwsUserAgent {
            framework_metadata: Default::default(),
            app_name: Default::default(),
            build_env_additional_metadata,
            additional_metadata: Default::default(),
        }
    }

@@ -104,6 +107,7 @@ impl AwsUserAgent {
            framework_metadata: Vec::new(),
            app_name: None,
            build_env_additional_metadata: None,
            additional_metadata: Vec::new(),
        }
    }

@@ -149,6 +153,18 @@ impl AwsUserAgent {
        self
    }

    /// Adds additional metadata to the user agent.
    pub fn with_additional_metadata(mut self, metadata: AdditionalMetadata) -> Self {
        self.additional_metadata.push(metadata);
        self
    }

    /// Adds additional metadata to the user agent.
    pub fn add_additional_metadata(&mut self, metadata: AdditionalMetadata) -> &mut Self {
        self.additional_metadata.push(metadata);
        self
    }

    /// Sets the app name for the user agent.
    pub fn with_app_name(mut self, app_name: AppName) -> Self {
        self.app_name = Some(app_name);
@@ -196,6 +212,9 @@ impl AwsUserAgent {
        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();
        }
+28 −6
Original line number Diff line number Diff line
@@ -3,18 +3,22 @@
 * SPDX-License-Identifier: Apache-2.0
 */

use crate::user_agent::{ApiMetadata, AwsUserAgent};
use std::borrow::Cow;
use std::fmt;

use http::header::{InvalidHeaderValue, USER_AGENT};
use http::{HeaderName, HeaderValue};

use aws_smithy_runtime_api::box_error::BoxError;
use aws_smithy_runtime_api::client::http::HttpClient;
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;

use crate::user_agent::{AdditionalMetadata, ApiMetadata, AwsUserAgent, InvalidMetadataValue};

#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this
const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent");
@@ -23,12 +27,14 @@ const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent")
enum UserAgentInterceptorError {
    MissingApiMetadata,
    InvalidHeaderValue(InvalidHeaderValue),
    InvalidMetadataValue(InvalidMetadataValue),
}

impl std::error::Error for UserAgentInterceptorError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::InvalidHeaderValue(source) => Some(source),
            Self::InvalidMetadataValue(source) => Some(source),
            Self::MissingApiMetadata => None,
        }
    }
@@ -38,6 +44,7 @@ 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::InvalidMetadataValue(_) => "AwsUserAgent generated an invalid metadata 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.",
        })
    }
@@ -49,6 +56,12 @@ impl From<InvalidHeaderValue> for UserAgentInterceptorError {
    }
}

impl From<InvalidMetadataValue> for UserAgentInterceptorError {
    fn from(err: InvalidMetadataValue) -> Self {
        UserAgentInterceptorError::InvalidMetadataValue(err)
    }
}

/// Generates and attaches the AWS SDK's user agent to a HTTP request
#[non_exhaustive]
#[derive(Debug, Default)]
@@ -79,7 +92,7 @@ impl Intercept for UserAgentInterceptor {
    fn modify_before_signing(
        &self,
        context: &mut BeforeTransmitInterceptorContextMut<'_>,
        _runtime_components: &RuntimeComponents,
        runtime_components: &RuntimeComponents,
        cfg: &mut ConfigBag,
    ) -> Result<(), BoxError> {
        // Allow for overriding the user agent by an earlier interceptor (so, for example,
@@ -99,6 +112,15 @@ impl Intercept for UserAgentInterceptor {
                if let Some(app_name) = maybe_app_name {
                    ua.set_app_name(app_name.clone());
                }

                let maybe_connector_metadata = runtime_components
                    .http_client()
                    .and_then(|c| c.connector_metadata());
                if let Some(connector_metadata) = maybe_connector_metadata {
                    let am = AdditionalMetadata::new(Cow::Owned(connector_metadata.to_string()))?;
                    ua.add_additional_metadata(am);
                }

                Ok(Cow::Owned(ua))
            })?;

+145 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package software.amazon.smithy.rustsdk

import org.junit.jupiter.api.Test
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
import software.amazon.smithy.rust.codegen.core.testutil.integrationTest

class UserAgentDecoratorTest {
    companion object {
        // Can't use the dollar sign in a multiline string with doing it like this.
        private const val PREFIX = "\$version: \"2\""
        val model =
            """
            $PREFIX
            namespace test

            use aws.api#service
            use aws.auth#sigv4
            use aws.protocols#restJson1
            use smithy.rules#endpointRuleSet

            @service(sdkId: "dontcare")
            @restJson1
            @sigv4(name: "dontcare")
            @auth([sigv4])
            @endpointRuleSet({
                "version": "1.0",
                "rules": [{ "type": "endpoint", "conditions": [], "endpoint": { "url": "https://example.com" } }],
                "parameters": {
                    "Region": { "required": false, "type": "String", "builtIn": "AWS::Region" },
                }
            })
            service TestService {
                version: "2023-01-01",
                operations: [SomeOperation]
            }

            @http(uri: "/SomeOperation", method: "GET")
            @optionalAuth
            operation SomeOperation {
                input: SomeInput,
                output: SomeOutput
            }

            @input
            structure SomeInput {}

            @output
            structure SomeOutput {}
            """.asSmithyModel()
    }

    @Test
    fun smokeTestSdkCodegen() {
        awsSdkIntegrationTest(model) { _, _ ->
            // it should compile
        }
    }

    @Test
    fun userAgentWorks() {
        awsSdkIntegrationTest(model) { context, rustCrate ->
            val rc = context.runtimeConfig
            val moduleName = context.moduleUseName()
            rustCrate.integrationTest("user-agent") {
                rustTemplate(
                    """
                    use $moduleName::config::{AppName, Credentials, Region, SharedCredentialsProvider};
                    use $moduleName::{Config, Client};
                    use #{capture_request};

                    ##[#{tokio}::test]
                    async fn user_agent_app_name() {
                        let (http_client, rcvr) = capture_request(None);
                        let config = Config::builder()
                            .credentials_provider(SharedCredentialsProvider::new(Credentials::for_tests()))
                            .region(Region::new("us-east-1"))
                            .http_client(http_client.clone())
                            .app_name(AppName::new("test-app-name").expect("valid app name")) // set app name in config
                            .build();
                        let client = Client::from_conf(config);
                        let _ = client.some_operation().send().await;

                        // verify app name made it to the user agent
                        let request = rcvr.expect_request();
                        let formatted = std::str::from_utf8(
                            request
                                .headers()
                                .get("x-amz-user-agent")
                                .unwrap()
                                .as_bytes(),
                        )
                        .unwrap();
                        assert!(
                            formatted.ends_with(" app/test-app-name"),
                            "'{}' didn't end with the app name",
                            formatted
                        );
                    }

                    ##[#{tokio}::test]
                    async fn user_agent_http_client() {
                        let (http_client, rcvr) = capture_request(None);
                        let config = Config::builder()
                            .credentials_provider(SharedCredentialsProvider::new(Credentials::for_tests()))
                            .region(Region::new("us-east-1"))
                            .http_client(http_client.clone())
                            .app_name(AppName::new("test-app-name").expect("valid app name")) // set app name in config
                            .build();
                        let client = Client::from_conf(config);
                        let _ = client.some_operation().send().await;

                        // verify app name made it to the user agent
                        let request = rcvr.expect_request();
                        let formatted = std::str::from_utf8(
                            request
                                .headers()
                                .get("x-amz-user-agent")
                                .unwrap()
                                .as_bytes(),
                        )
                        .unwrap();
                        assert!(
                            formatted.contains("md/http##capture-request-handler"),
                            "'{}' didn't include connector metadata",
                            formatted
                        );
                    }
                    """,
                    *preludeScope,
                    "tokio" to CargoDependency.Tokio.toDevDependency().withFeature("rt").withFeature("macros").toType(),
                    "capture_request" to RuntimeType.captureRequest(rc),
                )
            }
        }
    }
}
+0 −39
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

use aws_config::SdkConfig;
use aws_credential_types::provider::SharedCredentialsProvider;
use aws_sdk_s3::config::{AppName, Credentials, Region};
use aws_sdk_s3::Client;
use aws_smithy_runtime::client::http::test_util::capture_request;

#[tokio::test]
async fn user_agent_app_name() {
    let (http_client, rcvr) = capture_request(None);
    let sdk_config = SdkConfig::builder()
        .credentials_provider(SharedCredentialsProvider::new(Credentials::for_tests()))
        .region(Region::new("us-east-1"))
        .http_client(http_client.clone())
        .app_name(AppName::new("test-app-name").expect("valid app name")) // set app name in config
        .build();
    let client = Client::new(&sdk_config);
    let _ = client.list_objects_v2().bucket("test-bucket").send().await;

    // verify app name made it to the user agent
    let request = rcvr.expect_request();
    let formatted = std::str::from_utf8(
        request
            .headers()
            .get("x-amz-user-agent")
            .unwrap()
            .as_bytes(),
    )
    .unwrap();
    assert!(
        formatted.ends_with(" app/test-app-name"),
        "'{}' didn't end with the app name",
        formatted
    );
}
Loading