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
5 changes: 5 additions & 0 deletions .changes/afeacad1-0f2c-483d-a755-4df1fd6fd440.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"id": "afeacad1-0f2c-483d-a755-4df1fd6fd440",
"type": "feature",
"description": "Validate caller-specified AWS regions in client config (i.e., `region` and `regionProvider`)"
}
5 changes: 5 additions & 0 deletions aws-runtime/aws-config/api/aws-config.api
Original file line number Diff line number Diff line change
Expand Up @@ -707,3 +707,8 @@ public final class aws/sdk/kotlin/runtime/region/ResolveRegionKt {
public static synthetic fun resolveSigV4aSigningRegionSet$default (Laws/smithy/kotlin/runtime/util/PlatformProvider;Laws/smithy/kotlin/runtime/util/LazyAsyncValue;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}

public final class aws/sdk/kotlin/runtime/region/ValidateRegionKt {
public static final fun isRegionValid (Ljava/lang/String;)Z
public static final fun validateRegion (Ljava/lang/String;)Ljava/lang/String;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.runtime.region

import aws.sdk.kotlin.runtime.ConfigurationException
import aws.sdk.kotlin.runtime.InternalSdkApi

internal fun charSet(chars: String) = chars.toCharArray().toSet()
internal fun charSet(range: CharRange) = range.toSet()

private object Rfc3986CharSets {
val alpha = charSet('A'..'Z') + charSet('a'..'z')
val digit = charSet('0'..'9')
val unreserved = alpha + digit + charSet("-._~")
val hexdig = digit + charSet('A'..'F')
val pctEncoded = hexdig + '%'
val subDelims = charSet("!$&'()*+,;=")
val regName = unreserved + pctEncoded + subDelims
}

/**
* Determines if the given region is valid for the purposes of endpoint lookup, specifically that the region is suitable
* to use in a URI hostname according to [RFC 3986 § 3.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2).
*
* Valid characters for regions include:
* * URI unreserved characters:
* * Uppercase letters (`A` through `Z`)
* * Lowercase letters (`a` through `z`)
* * Digits (`0` through `9`)
* * Hyphen (`-`)
* * Period/dot (`.`)
* * Tilde (`~`)
* * Underscore (`_`)
* * Percent (`%`)
* * URI sub-delimiters
* * Ampersand (`&`)
* * Apostrophe (`'`)
* * Asterisk (`*`)
* * Comma (`,`)
* * Dollar sign (`$`)
* * Equals sign (`=`)
* * Exclamation point (`!`)
* * Parentheses (`(` and `)`)
* * Plus (`+`)
* * Semicolon (`;`)
*
* Notable characters which are _invalid_ for regions include:
* * Space (` `)
* * At sign (`@`)
* * Backtick/grave (`` ` ``)
* * Braces (`{` and `}`)
* * Brackets (`[` and `]`)
* * Caret (`^`)
* * Colon (`:`)
* * Double quote (`"`)
* * Hash/number sign (`#`)
* * Inequality signs (`<` and `>`)
* * Pipe (`|`)
* * Question mark (`?`)
* * Slashes (`/` and `\`)
* * All non-ASCII characters (e.g., Unicode characters)
*/
@InternalSdkApi
public fun isRegionValid(region: String): Boolean = region.isNotEmpty() && region.all(Rfc3986CharSets.regName::contains)

/**
* Validates that a region is suitable to use in a URI hostname according to
* [RFC 3986 § 3.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2). See [isRegionValid] for a detailed
* description of the validation criteria.
*/
@InternalSdkApi
public fun validateRegion(region: String): String = region.also {
if (!isRegionValid(region)) {
throw ConfigurationException("""Configured region "$region" is invalid. A region must be a valid URI host component.""")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.runtime.region

import aws.sdk.kotlin.runtime.ConfigurationException
import kotlin.test.*

/**
* Forms the [combinations](https://en.wikipedia.org/wiki/Combination) of a given length for the given set
*/
private fun combinations(ofSet: Set<Char>, length: Int): Set<String> {
if (length <= 0) return emptySet()
if (length == 1) return ofSet.map { it.toString() }.toSet()

val elements = ofSet.toList()

return buildSet {
fun generate(current: String, startIndex: Int) {
if (current.length == length) {
add(current)
} else {
for (i in startIndex until elements.size) {
generate(current + elements[i], i + 1)
}
}
}

generate("", 0)
}
}

private object TestData {
private val validChars = charSet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%!$&'()*+,;=")

/**
* Non-exhaustive set of [actual AWS regions][1].
*
* [1]: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html
*/
private val realRegions = setOf(
"af-south-1",
"ap-east-1",
"ap-east-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-south-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-6",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
"eu-central-1",
"eu-central-2",
"eu-north-1",
"eu-south-1",
"eu-south-2",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"il-central-1",
"me-central-1",
"me-south-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
)

private val kitchenSinkRegion = validChars.joinToString("") // ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.+~%!$&'()*+,;=
private val regionsWithSpecialChars = combinations(validChars, 3).map { "region-$it" }.toSet() // region-XXX
val validRegions = realRegions + regionsWithSpecialChars + kitchenSinkRegion

private val printableAsciiChars = charSet(32.toChar()..126.toChar()) // ASCII codepoints 32-126 (inclusive)
private val invalidChars = printableAsciiChars - validChars
val invalidRegions = combinations(invalidChars, 3).map { "region-$it" }.toSet() // region-XXX
}

class ValidateRegionTest {
@Test
fun testIsRegionValid() {
TestData.validRegions.forEach {
println("Valid region: $it")
assertTrue(isRegionValid(it))
}
TestData.invalidRegions.forEach {
println("Invalid region: $it")
assertFalse(isRegionValid(it))
}
}

@Test
fun testValidateRegion() {
TestData.validRegions.forEach {
assertEquals(it, validateRegion(it))
}

TestData.invalidRegions.forEach {
assertFailsWith<ConfigurationException> {
validateRegion(it)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ object AwsRuntimeTypes {
object Region : RuntimeTypePackage(AwsKotlinDependency.AWS_CONFIG, "region") {
val DefaultRegionProviderChain = symbol("DefaultRegionProviderChain")
val resolveRegion = symbol("resolveRegion")
val validateRegion = symbol("validateRegion")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@
*/
package aws.sdk.kotlin.codegen

import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.core.CodegenContext
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
import software.amazon.smithy.kotlin.codegen.core.getContextValue
import software.amazon.smithy.kotlin.codegen.integration.AppendingSectionWriter
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
import software.amazon.smithy.kotlin.codegen.model.asNullable
import software.amazon.smithy.kotlin.codegen.model.knowledge.AwsSignatureVersion4
import software.amazon.smithy.kotlin.codegen.model.nullable
import software.amazon.smithy.kotlin.codegen.rendering.*
import software.amazon.smithy.kotlin.codegen.rendering.ServiceClientGenerator
import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpProtocolClientGenerator
import software.amazon.smithy.kotlin.codegen.rendering.util.ConfigProperty
import software.amazon.smithy.kotlin.codegen.rendering.util.ConfigPropertyType
Expand Down Expand Up @@ -42,11 +44,12 @@ class AwsServiceConfigIntegration : KotlinIntegration {
propertyType = ConfigPropertyType.Custom(
render = { prop, writer ->
writer.write(
"override val #1L: #2T? = builder.#1L ?: #3T { builder.regionProvider?.getRegion() ?: #4T() }",
"override val #1L: #2T? = (builder.#1L ?: #3T { builder.regionProvider?.getRegion() ?: #4T() })?.let { #5T(it) }",
prop.propertyName,
prop.symbol,
RuntimeTypes.KotlinxCoroutines.runBlocking,
AwsRuntimeTypes.Config.Region.resolveRegion,
AwsRuntimeTypes.Config.Region.validateRegion,
)
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import org.junit.jupiter.api.Test
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
import software.amazon.smithy.kotlin.codegen.model.expectShape
import software.amazon.smithy.kotlin.codegen.rendering.ServiceClientConfigGenerator
import software.amazon.smithy.kotlin.codegen.test.*
import software.amazon.smithy.kotlin.codegen.test.newTestContext
import software.amazon.smithy.kotlin.codegen.test.shouldContainOnlyOnceWithDiff
import software.amazon.smithy.kotlin.codegen.test.toRenderingContext
import software.amazon.smithy.kotlin.codegen.test.toSmithyModel
import software.amazon.smithy.model.shapes.ServiceShape

class AwsServiceConfigIntegrationTest {
Expand Down Expand Up @@ -45,7 +48,7 @@ class AwsServiceConfigIntegrationTest {
val contents = writer.toString()

val expectedProps = """
override val region: String? = builder.region ?: runBlocking { builder.regionProvider?.getRegion() ?: resolveRegion() }
override val region: String? = (builder.region ?: runBlocking { builder.regionProvider?.getRegion() ?: resolveRegion() })?.let { validateRegion(it) }
override val regionProvider: RegionProvider = builder.regionProvider ?: DefaultRegionProviderChain()
override val credentialsProvider: CredentialsProvider = builder.credentialsProvider ?: DefaultChainCredentialsProvider(httpClient = httpClient, region = region).manage()
"""
Expand Down
Loading