Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package aws.sdk.kotlin.runtime.http

import aws.sdk.kotlin.runtime.AwsServiceException
import aws.sdk.kotlin.runtime.InternalSdkApi
import software.aws.clientrt.http.HttpStatusCode
import software.aws.clientrt.http.operation.HttpDeserialize

Expand All @@ -15,11 +16,17 @@ import software.aws.clientrt.http.operation.HttpDeserialize
* @property deserializer The deserializer responsible for providing a [Throwable] instance of the actual exception
* @property httpStatusCode The HTTP status code the error is returned with
*/
public data class ExceptionMetadata(val errorCode: String, val deserializer: HttpDeserialize<*>, val httpStatusCode: HttpStatusCode? = null)
@InternalSdkApi
public data class ExceptionMetadata(
val errorCode: String,
val deserializer: HttpDeserialize<*>,
val httpStatusCode: HttpStatusCode? = null
)

/**
* Container for modeled exceptions
*/
@InternalSdkApi
public class ExceptionRegistry {
// ErrorCode -> Meta
private val errorsByCodeName = mutableMapOf<String, ExceptionMetadata>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@
*/
package aws.sdk.kotlin.runtime.http

import aws.sdk.kotlin.runtime.InternalSdkApi
import software.aws.clientrt.http.HttpBody
import software.aws.clientrt.http.content.ByteArrayContent
import software.aws.clientrt.http.response.HttpResponse

/**
* Default header name identifying the unique requestId
*/
const val X_AMZN_REQUEST_ID_HEADER = "X-Amzn-RequestId"
public const val X_AMZN_REQUEST_ID_HEADER: String = "X-Amzn-RequestId"

/**
* Return a copy of the response with a new payload set
*/
fun HttpResponse.withPayload(payload: ByteArray?): HttpResponse {
@InternalSdkApi
public fun HttpResponse.withPayload(payload: ByteArray?): HttpResponse {
val newBody = if (payload != null) {
ByteArrayContent(payload)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* SPDX-License-Identifier: Apache-2.0.
*/

package aws.sdk.kotlin.runtime.http
package aws.sdk.kotlin.runtime.http.middleware

import aws.sdk.kotlin.runtime.InternalSdkApi
import aws.sdk.kotlin.runtime.client.AwsClientOption
import aws.sdk.kotlin.runtime.endpoint.EndpointResolver
import software.aws.clientrt.http.*
Expand All @@ -15,6 +16,7 @@ import software.aws.clientrt.util.get
/**
* Http feature for resolving the service endpoint.
*/
@InternalSdkApi
public class ServiceEndpointResolver(
config: Config
) : Feature {
Expand Down
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" }
Copy link
Contributor

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 IllegalArgumentException what we want to throw in this case?

Copy link
Contributor Author

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 IllegalArgumentException from 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

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)
}
}
}
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0.
*/

package aws.sdk.kotlin.runtime.http
package aws.sdk.kotlin.runtime.http.middleware

import aws.sdk.kotlin.runtime.client.AwsClientOption
import aws.sdk.kotlin.runtime.endpoint.Endpoint
Expand Down
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))
}
}
Loading