-
Notifications
You must be signed in to change notification settings - Fork 55
feat: add user-agent header middleware #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
8ece70e
feat: add user agent middleware
aajtodd 2cf6943
generate ua header feature; encode ua values
aajtodd 73475e9
Merge remote-tracking branch 'origin/main' into feat-ua-header
aajtodd 5b1efb3
move aws ua metadata to its own file
aajtodd fba8826
refactor: move middleware features into dedicated package
aajtodd a765496
protect system property access
aajtodd 1b3b6cc
remove redundant type specs
aajtodd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
163 changes: 163 additions & 0 deletions
163
client-runtime/protocols/http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0. | ||
| */ | ||
|
|
||
| package aws.sdk.kotlin.runtime.http | ||
|
|
||
| import software.aws.clientrt.util.OsFamily | ||
| import software.aws.clientrt.util.Platform | ||
|
|
||
| /** | ||
| * Metadata used to populate the `User-Agent` and `x-amz-user-agent` headers | ||
| */ | ||
| public data class AwsUserAgentMetadata( | ||
| val sdkMetadata: SdkMetadata, | ||
| val apiMetadata: ApiMetadata, | ||
| val osMetadata: OsMetadata, | ||
| val languageMetadata: LanguageMetadata, | ||
| val execEnvMetadata: ExecutionEnvMetadata? = null | ||
| ) { | ||
|
|
||
| public companion object { | ||
| /** | ||
| * Load user agent configuration data from the current environment | ||
| */ | ||
| public fun fromEnvironment(apiMeta: ApiMetadata): AwsUserAgentMetadata { | ||
| val sdkMeta = SdkMetadata("kotlin", apiMeta.version) | ||
| val osInfo = Platform.osInfo() | ||
| val osMetadata = OsMetadata(osInfo.family, osInfo.version) | ||
| val langMeta = platformLanguageMetadata() | ||
| return AwsUserAgentMetadata(sdkMeta, apiMeta, osMetadata, langMeta, detectExecEnv()) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * New-style user agent header value for `x-amz-user-agent` | ||
| */ | ||
| val xAmzUserAgent: String = buildString { | ||
| /* | ||
| ABNF for the user agent: | ||
| ua-string = | ||
| sdk-metadata RWS | ||
| [api-metadata RWS] | ||
| os-metadata RWS | ||
| language-metadata RWS | ||
| [env-metadata RWS] | ||
| *(feat-metadata RWS) | ||
| *(config-metadata RWS) | ||
| *(framework-metadata RWS) | ||
| [appId] | ||
| */ | ||
| append("$sdkMetadata ") | ||
| append("$apiMetadata ") | ||
| append("$osMetadata ") | ||
| append("$languageMetadata ") | ||
| execEnvMetadata?.let { append("$it") } | ||
|
|
||
| // TODO - feature metadata | ||
| // TODO - config metadata | ||
| // TODO - framework metadata (e.g. Amplify would be a good candidate for this data) | ||
| // TODO - appId | ||
| }.trimEnd() | ||
|
|
||
| /** | ||
| * Legacy user agent header value for `UserAgent` | ||
| */ | ||
| val userAgent: String = "$sdkMetadata" | ||
| } | ||
|
|
||
| /** | ||
| * SDK metadata | ||
| * @property name The SDK (language) name | ||
| * @property version The SDK version | ||
| */ | ||
| public data class SdkMetadata(val name: String, val version: String) { | ||
| override fun toString(): String = "aws-sdk-$name/$version" | ||
| } | ||
|
|
||
| /** | ||
| * API metadata | ||
| * @property serviceId The service ID (sdkId) in use (e.g. "Api Gateway") | ||
| * @property version The version of the client (note this may be the same as [SdkMetadata.version] for SDK's | ||
| * that don't independently version clients from one another. | ||
| */ | ||
| public data class ApiMetadata(val serviceId: String, val version: String) { | ||
| override fun toString(): String { | ||
| val formattedServiceId = serviceId.replace(" ", "-").toLowerCase() | ||
| return "api/$formattedServiceId/${version.encodeUaToken()}" | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Operating system metadata | ||
| */ | ||
| public data class OsMetadata(val family: OsFamily, val version: String? = null) { | ||
| override fun toString(): String { | ||
| // os-family = windows / linux / macos / android / ios / other | ||
| val familyStr = when (family) { | ||
| OsFamily.Unknown -> "other" | ||
| else -> family.toString() | ||
| } | ||
| return if (version != null) "os/$familyStr/${version.encodeUaToken()}" else "os/$familyStr" | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Programming language metadata | ||
| * @property version The kotlin version in use | ||
| * @property extras Additional key value pairs appropriate for the language/runtime (e.g.`jvmVm=OpenJdk`, etc) | ||
| */ | ||
| public data class LanguageMetadata( | ||
| val version: String = KotlinVersion.CURRENT.toString(), | ||
| // additional metadata key/value pairs | ||
| val extras: Map<String, String> = emptyMap() | ||
| ) { | ||
| override fun toString(): String = buildString { | ||
| append("lang/kotlin/$version") | ||
| extras.entries.forEach { (key, value) -> | ||
| append(" md/$key/${value.encodeUaToken()}") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // provide platform specific metadata | ||
| internal expect fun platformLanguageMetadata(): LanguageMetadata | ||
|
|
||
| /** | ||
| * Execution environment metadata | ||
| * @property name The execution environment name (e.g. "lambda") | ||
| */ | ||
| public data class ExecutionEnvMetadata(val name: String) { | ||
| override fun toString(): String = "exec-env/${name.encodeUaToken()}" | ||
| } | ||
|
|
||
| private fun detectExecEnv(): ExecutionEnvMetadata? { | ||
| // see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime | ||
| return Platform.getenv("AWS_LAMBDA_FUNCTION_NAME")?.let { | ||
| ExecutionEnvMetadata("lambda") | ||
| } | ||
| } | ||
|
|
||
| // ua-value = token | ||
| // token = 1*tchar | ||
| // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / | ||
| // "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA | ||
| private val VALID_TCHAR = setOf( | ||
| '!', '#', '$', '%', '&', | ||
| '\'', '*', '+', '-', '.', | ||
| '^', '_', '`', '|', '~' | ||
| ) | ||
|
|
||
| private fun String.encodeUaToken(): String { | ||
| val str = this | ||
| return buildString(str.length) { | ||
| for (chr in str) { | ||
| when (chr) { | ||
| ' ' -> append("_") | ||
| in 'a'..'z', in 'A'..'Z', in '0'..'9', in VALID_TCHAR -> append(chr) | ||
| else -> continue | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
client-runtime/protocols/http/common/src/aws/sdk/kotlin/runtime/http/middleware/UserAgent.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0. | ||
| */ | ||
|
|
||
| package aws.sdk.kotlin.runtime.http.middleware | ||
|
|
||
| import aws.sdk.kotlin.runtime.InternalSdkApi | ||
| import aws.sdk.kotlin.runtime.http.AwsUserAgentMetadata | ||
| import software.aws.clientrt.http.Feature | ||
| import software.aws.clientrt.http.FeatureKey | ||
| import software.aws.clientrt.http.HttpClientFeatureFactory | ||
| import software.aws.clientrt.http.operation.SdkHttpOperation | ||
|
|
||
| internal const val X_AMZ_USER_AGENT: String = "x-amz-user-agent" | ||
| internal const val USER_AGENT: String = "User-Agent" | ||
|
|
||
| /** | ||
| * Http middleware that sets the User-Agent and x-amz-user-agent headers | ||
| */ | ||
| @InternalSdkApi | ||
| public class UserAgent(private val awsUserAgentMetadata: AwsUserAgentMetadata) : Feature { | ||
|
|
||
| public class Config { | ||
| public var metadata: AwsUserAgentMetadata? = null | ||
| } | ||
|
|
||
| public companion object Feature : | ||
| HttpClientFeatureFactory<Config, UserAgent> { | ||
| override val key: FeatureKey<UserAgent> = FeatureKey("UserAgent") | ||
|
|
||
| override fun create(block: Config.() -> Unit): UserAgent { | ||
| val config = Config().apply(block) | ||
| val metadata = requireNotNull(config.metadata) { "metadata is required" } | ||
| return UserAgent(metadata) | ||
| } | ||
| } | ||
|
|
||
| override fun <I, O> install(operation: SdkHttpOperation<I, O>) { | ||
| operation.execution.mutate.intercept { req, next -> | ||
| req.builder.headers[USER_AGENT] = awsUserAgentMetadata.userAgent | ||
| req.builder.headers[X_AMZ_USER_AGENT] = awsUserAgentMetadata.xAmzUserAgent | ||
| next.call(req) | ||
| } | ||
| } | ||
| } | ||
30 changes: 30 additions & 0 deletions
30
...untime/protocols/http/common/test/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadataTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0. | ||
| */ | ||
|
|
||
| package aws.sdk.kotlin.runtime.http | ||
|
|
||
| import software.aws.clientrt.util.OsFamily | ||
| import kotlin.test.Test | ||
| import kotlin.test.assertEquals | ||
|
|
||
| class AwsUserAgentMetadataTest { | ||
|
|
||
| @Test | ||
| fun testUserAgent() { | ||
| val ua = AwsUserAgentMetadata.fromEnvironment(ApiMetadata("Test Service", "1.2.3")) | ||
| assertEquals("aws-sdk-kotlin/1.2.3", ua.userAgent) | ||
| } | ||
|
|
||
| @Test | ||
| fun testXAmzUserAgent() { | ||
| val apiMeta = ApiMetadata("Test Service", "1.2.3") | ||
| val sdkMeta = SdkMetadata("kotlin", apiMeta.version) | ||
| val osMetadata = OsMetadata(OsFamily.Linux, "ubuntu-20.04") | ||
| val langMeta = LanguageMetadata("1.4.31", mapOf("jvmVersion" to "1.11")) | ||
| val ua = AwsUserAgentMetadata(sdkMeta, apiMeta, osMetadata, langMeta) | ||
| val expected = "aws-sdk-kotlin/1.2.3 api/test-service/1.2.3 os/linux/ubuntu-20.04 lang/kotlin/1.4.31 md/jvmVersion/1.11" | ||
| assertEquals(expected, ua.xAmzUserAgent) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
...untime/protocols/http/common/test/aws/sdk/kotlin/runtime/http/middleware/UserAgentTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0. | ||
| */ | ||
|
|
||
| package aws.sdk.kotlin.runtime.http.middleware | ||
|
|
||
| import aws.sdk.kotlin.runtime.http.ApiMetadata | ||
| import aws.sdk.kotlin.runtime.http.AwsUserAgentMetadata | ||
| import aws.sdk.kotlin.runtime.testing.runSuspendTest | ||
| import software.aws.clientrt.http.Headers | ||
| import software.aws.clientrt.http.HttpBody | ||
| import software.aws.clientrt.http.HttpStatusCode | ||
| import software.aws.clientrt.http.engine.HttpClientEngine | ||
| import software.aws.clientrt.http.operation.* | ||
| import software.aws.clientrt.http.request.HttpRequestBuilder | ||
| import software.aws.clientrt.http.response.HttpResponse | ||
| import software.aws.clientrt.http.sdkHttpClient | ||
| import kotlin.test.Test | ||
| import kotlin.test.assertTrue | ||
|
|
||
| class UserAgentTest { | ||
|
|
||
| @Test | ||
| fun `it sets ua headers`() = runSuspendTest { | ||
| val mockEngine = object : HttpClientEngine { | ||
| override suspend fun roundTrip(requestBuilder: HttpRequestBuilder): HttpResponse { | ||
| return HttpResponse(HttpStatusCode.fromValue(200), Headers {}, HttpBody.Empty, requestBuilder.build()) | ||
| } | ||
| } | ||
|
|
||
| val client = sdkHttpClient(mockEngine) | ||
|
|
||
| val op = SdkHttpOperation.build<Unit, HttpResponse> { | ||
| serializer = UnitSerializer | ||
| deserializer = IdentityDeserializer | ||
| context { | ||
| service = "Test Service" | ||
| operationName = "testOperation" | ||
| } | ||
| } | ||
|
|
||
| op.install(UserAgent) { | ||
| metadata = AwsUserAgentMetadata.fromEnvironment(ApiMetadata("Test Service", "1.2.3")) | ||
| } | ||
|
|
||
| val response = op.roundTrip(client, Unit) | ||
| assertTrue(response.request.headers.contains(USER_AGENT)) | ||
| assertTrue(response.request.headers.contains(X_AMZ_USER_AGENT)) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question
I'm not sure where to look but in serde I know we require specific exception types. Is
IllegalArgumentExceptionwhat we want to throw in this case?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so far I've been ok with throwing
IllegalArgumentExceptionfrom the standpoint that the only way this should really be happening is if we generated the code wrong. Whatever exception is thrown wouldn't matter because you can't do anything about it anyway, it would need fixed in codegen.It's a good question though, I just don't have an answer that improves the situation in any measurable way