Skip to content

Commit 1a9709f

Browse files
authored
feat: add HTTP engine config for min TLS version (#844)
1 parent 0f3624e commit 1a9709f

File tree

17 files changed

+456
-145
lines changed

17 files changed

+456
-145
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "1af71df0-f584-493e-85ab-2ee3e853c827",
3+
"type": "feature",
4+
"description": "**Breaking**: Add HTTP engine configuration for minimum TLS version. See the [BREAKING: Streamlined TLS configuration](https://github.com/awslabs/aws-sdk-kotlin/discussions/909) discussion post for more details.",
5+
"issues": [
6+
"awslabs/smithy-kotlin#661"
7+
]
8+
}

runtime/protocol/http-client-engines/http-client-engine-crt/api/http-client-engine-crt.api

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,15 @@ public final class aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig
1515
public synthetic fun <init> (Laws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig$Builder;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
1616
public final fun getClientBootstrap ()Laws/sdk/kotlin/crt/io/ClientBootstrap;
1717
public final fun getInitialWindowSizeBytes ()I
18-
public final fun getTlsContext ()Laws/sdk/kotlin/crt/io/TlsContext;
1918
public final fun setClientBootstrap (Laws/sdk/kotlin/crt/io/ClientBootstrap;)V
20-
public final fun setTlsContext (Laws/sdk/kotlin/crt/io/TlsContext;)V
2119
}
2220

2321
public final class aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig$Builder : aws/smithy/kotlin/runtime/http/engine/HttpClientEngineConfig$Builder {
2422
public fun <init> ()V
2523
public final fun getClientBootstrap ()Laws/sdk/kotlin/crt/io/ClientBootstrap;
2624
public final fun getInitialWindowSizeBytes ()I
27-
public final fun getTlsContext ()Laws/sdk/kotlin/crt/io/TlsContext;
2825
public final fun setClientBootstrap (Laws/sdk/kotlin/crt/io/ClientBootstrap;)V
2926
public final fun setInitialWindowSizeBytes (I)V
30-
public final fun setTlsContext (Laws/sdk/kotlin/crt/io/TlsContext;)V
3127
}
3228

3329
public final class aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig$Companion {

runtime/protocol/http-client-engines/http-client-engine-crt/common/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngine.kt

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55

66
package aws.smithy.kotlin.runtime.http.engine.crt
77

8-
import aws.sdk.kotlin.crt.http.*
9-
import aws.sdk.kotlin.crt.io.*
8+
import aws.sdk.kotlin.crt.http.HttpClientConnectionManager
9+
import aws.sdk.kotlin.crt.http.HttpClientConnectionManagerOptionsBuilder
10+
import aws.sdk.kotlin.crt.http.HttpProxyAuthorizationType
11+
import aws.sdk.kotlin.crt.http.HttpProxyOptions
12+
import aws.sdk.kotlin.crt.io.SocketOptions
13+
import aws.sdk.kotlin.crt.io.TlsContextOptionsBuilder
14+
import aws.sdk.kotlin.crt.io.Uri
1015
import aws.smithy.kotlin.runtime.crt.SdkDefaultIO
1116
import aws.smithy.kotlin.runtime.http.HttpErrorCode
1217
import aws.smithy.kotlin.runtime.http.HttpException
@@ -27,6 +32,9 @@ import kotlinx.coroutines.sync.Mutex
2732
import kotlinx.coroutines.sync.withLock
2833
import kotlinx.coroutines.withContext
2934
import kotlinx.coroutines.withTimeoutOrNull
35+
import aws.sdk.kotlin.crt.io.TlsContext as CrtTlsContext
36+
import aws.sdk.kotlin.crt.io.TlsVersion as CrtTlsVersion
37+
import aws.smithy.kotlin.runtime.config.TlsVersion as SdkTlsVersion
3038

3139
internal const val DEFAULT_WINDOW_SIZE_BYTES: Int = 16 * 1024
3240
internal const val CHUNK_BUFFER_SIZE: Long = 64 * 1024
@@ -44,15 +52,14 @@ public class CrtHttpEngine(public val config: CrtHttpEngineConfig) : HttpClientE
4452
}
4553
private val logger = Logger.getLogger<CrtHttpEngine>()
4654

47-
private val customTlsContext: TlsContext? = if (config.alpn.isNotEmpty() && config.tlsContext == null) {
48-
val options = TlsContextOptionsBuilder().apply {
55+
private val crtTlsContext: CrtTlsContext = TlsContextOptionsBuilder()
56+
.apply {
4957
verifyPeer = true
50-
alpn = config.alpn.joinToString(separator = ";") { it.protocolId }
51-
}.build()
52-
TlsContext(options)
53-
} else {
54-
null
55-
}
58+
alpn = config.tlsContext.alpn.joinToString(separator = ";") { it.protocolId }
59+
minTlsVersion = toCrtTlsVersion(config.tlsContext.minVersion)
60+
}
61+
.build()
62+
.let(::CrtTlsContext)
5663

5764
init {
5865
if (config.socketReadTimeout != CrtHttpEngineConfig.Default.socketReadTimeout) {
@@ -70,7 +77,7 @@ public class CrtHttpEngine(public val config: CrtHttpEngineConfig) : HttpClientE
7077

7178
private val options = HttpClientConnectionManagerOptionsBuilder().apply {
7279
clientBootstrap = config.clientBootstrap ?: SdkDefaultIO.ClientBootstrap
73-
tlsContext = customTlsContext ?: config.tlsContext ?: SdkDefaultIO.TlsContext
80+
tlsContext = crtTlsContext
7481
manualWindowManagement = true
7582
socketOptions = SocketOptions(
7683
connectTimeoutMs = config.connectTimeout.inWholeMilliseconds.toInt(),
@@ -129,7 +136,7 @@ public class CrtHttpEngine(public val config: CrtHttpEngineConfig) : HttpClientE
129136
// close all resources
130137
// SAFETY: shutdown is only invoked once AND only after all requests have completed and no more are coming
131138
connManagers.forEach { entry -> entry.value.close() }
132-
customTlsContext?.close()
139+
crtTlsContext.close()
133140
}
134141

135142
private suspend fun getManagerForUri(uri: Uri, proxyConfig: ProxyConfig): HttpClientConnectionManager = mutex.withLock {
@@ -151,3 +158,11 @@ public class CrtHttpEngine(public val config: CrtHttpEngineConfig) : HttpClientE
151158
}
152159
}
153160
}
161+
162+
private fun toCrtTlsVersion(sdkTlsVersion: SdkTlsVersion?): CrtTlsVersion = when (sdkTlsVersion) {
163+
null -> CrtTlsVersion.SYS_DEFAULT
164+
SdkTlsVersion.TLS_1_0 -> CrtTlsVersion.TLSv1
165+
SdkTlsVersion.TLS_1_1 -> CrtTlsVersion.TLS_V1_1
166+
SdkTlsVersion.TLS_1_2 -> CrtTlsVersion.TLS_V1_2
167+
SdkTlsVersion.TLS_1_3 -> CrtTlsVersion.TLS_V1_3
168+
}

runtime/protocol/http-client-engines/http-client-engine-crt/common/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig.kt

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
package aws.smithy.kotlin.runtime.http.engine.crt
77

88
import aws.sdk.kotlin.crt.io.ClientBootstrap
9-
import aws.sdk.kotlin.crt.io.TlsContext
109
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineConfig
1110

1211
/**
@@ -35,11 +34,6 @@ public class CrtHttpEngineConfig private constructor(builder: Builder) : HttpCli
3534
*/
3635
public var clientBootstrap: ClientBootstrap? = builder.clientBootstrap
3736

38-
/**
39-
* The TLS context to use. By default it is a shared instance.
40-
*/
41-
public var tlsContext: TlsContext? = builder.tlsContext
42-
4337
public class Builder : HttpClientEngineConfig.Builder() {
4438
/**
4539
* Set the amount of data that can be buffered before reading from the socket will cease. Reading will
@@ -52,11 +46,6 @@ public class CrtHttpEngineConfig private constructor(builder: Builder) : HttpCli
5246
*/
5347
public var clientBootstrap: ClientBootstrap? = null
5448

55-
/**
56-
* Set the TLS context to use. By default it is a shared instance.
57-
*/
58-
public var tlsContext: TlsContext? = null
59-
6049
internal fun build(): CrtHttpEngineConfig = CrtHttpEngineConfig(this)
6150
}
6251
}

runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55

66
package aws.smithy.kotlin.runtime.http.engine.okhttp
77

8+
import aws.smithy.kotlin.runtime.config.TlsVersion
89
import aws.smithy.kotlin.runtime.http.engine.AlpnId
910
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase
11+
import aws.smithy.kotlin.runtime.http.engine.TlsContext
1012
import aws.smithy.kotlin.runtime.http.engine.callContext
1113
import aws.smithy.kotlin.runtime.http.request.HttpRequest
1214
import aws.smithy.kotlin.runtime.http.response.HttpCall
@@ -17,6 +19,8 @@ import kotlinx.coroutines.job
1719
import okhttp3.*
1820
import java.util.concurrent.TimeUnit
1921
import kotlin.time.toJavaDuration
22+
import aws.smithy.kotlin.runtime.config.TlsVersion as SdkTlsVersion
23+
import okhttp3.TlsVersion as OkHttpTlsVersion
2024

2125
/**
2226
* [aws.smithy.kotlin.runtime.http.engine.HttpClientEngine] based on OkHttp.
@@ -69,6 +73,8 @@ private fun OkHttpEngineConfig.buildClient(): OkHttpClient {
6973
followRedirects(false)
7074
followSslRedirects(false)
7175

76+
connectionSpecs(listOf(minTlsConnectionSpec(config.tlsContext), ConnectionSpec.CLEARTEXT))
77+
7278
// Transient connection errors are handled by retry strategy (exceptions are wrapped and marked retryable
7379
// appropriately internally). We don't want inner retry logic that inflates the number of retries.
7480
retryOnConnectionFailure(false)
@@ -97,8 +103,8 @@ private fun OkHttpEngineConfig.buildClient(): OkHttpClient {
97103
eventListenerFactory { call -> HttpEngineEventListener(pool, config.hostResolver, call) }
98104

99105
// map protocols
100-
if (config.alpn.isNotEmpty()) {
101-
val protocols = config.alpn.mapNotNull {
106+
if (config.tlsContext.alpn.isNotEmpty()) {
107+
val protocols = config.tlsContext.alpn.mapNotNull {
102108
when (it) {
103109
AlpnId.HTTP1_1 -> Protocol.HTTP_1_1
104110
AlpnId.HTTP2 -> Protocol.HTTP_2
@@ -115,3 +121,24 @@ private fun OkHttpEngineConfig.buildClient(): OkHttpClient {
115121
dns(OkHttpDns(config.hostResolver))
116122
}.build()
117123
}
124+
125+
private fun minTlsConnectionSpec(tlsContext: TlsContext): ConnectionSpec {
126+
val minVersion = tlsContext.minVersion ?: TlsVersion.TLS_1_2
127+
val okHttpTlsVersions = SdkTlsVersion
128+
.values()
129+
.filter { it >= minVersion }
130+
.sortedDescending() // Prioritize higher TLS versions first
131+
.map(::toOkHttpTlsVersion)
132+
.toTypedArray()
133+
return ConnectionSpec
134+
.Builder(ConnectionSpec.MODERN_TLS)
135+
.tlsVersions(*okHttpTlsVersions)
136+
.build()
137+
}
138+
139+
private fun toOkHttpTlsVersion(sdkTlsVersion: SdkTlsVersion): OkHttpTlsVersion = when (sdkTlsVersion) {
140+
SdkTlsVersion.TLS_1_0 -> OkHttpTlsVersion.TLS_1_0
141+
SdkTlsVersion.TLS_1_1 -> OkHttpTlsVersion.TLS_1_1
142+
SdkTlsVersion.TLS_1_2 -> OkHttpTlsVersion.TLS_1_2
143+
SdkTlsVersion.TLS_1_3 -> OkHttpTlsVersion.TLS_1_3
144+
}

runtime/protocol/http-client-engines/test-suite/build.gradle.kts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ kotlin {
2323
implementation(project(":runtime:protocol:http-test"))
2424
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
2525
implementation(project(":runtime:testing"))
26+
27+
implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion")
2628
}
2729
}
2830

2931
jvmMain {
3032
dependencies {
31-
implementation("io.ktor:ktor-server-cio:$ktorVersion")
33+
implementation("io.ktor:ktor-server-jetty:$ktorVersion")
3234

3335
implementation(project(":runtime:protocol:http-client-engines:http-client-engine-default"))
3436
implementation(project(":runtime:protocol:http-client-engines:http-client-engine-crt"))
@@ -50,7 +52,7 @@ kotlin {
5052
}
5153
}
5254

53-
open class LocalTestServer : DefaultTask() {
55+
open class LocalTestServers : DefaultTask() {
5456
@Internal
5557
var server: Closeable? = null
5658
private set
@@ -64,42 +66,42 @@ open class LocalTestServer : DefaultTask() {
6466
@TaskAction
6567
fun exec() {
6668
try {
67-
println("[TestServer] start")
69+
println("[TestServers] start")
6870
val urlClassLoaderSource = classpath.map { file -> file.toURI().toURL() }.toTypedArray()
6971
val loader = URLClassLoader(urlClassLoaderSource, ClassLoader.getSystemClassLoader())
7072

7173
val mainClass = loader.loadClass(main)
72-
val main = mainClass.getMethod("startServer")
74+
val main = mainClass.getMethod("startServers")
7375
server = main.invoke(null) as Closeable
74-
println("[TestServer] started")
76+
println("[TestServers] started")
7577
} catch (cause: Throwable) {
76-
println("[TestServer] failed: ${cause.message}")
78+
println("[TestServers] failed: ${cause.message}")
7779
throw cause
7880
}
7981
}
8082

8183
fun stop() {
8284
if (server != null) {
8385
server?.close()
84-
println("[TestServer] stop")
86+
println("[TestServers] stop")
8587
}
8688
}
8789
}
8890

8991
val osName = System.getProperty("os.name")
9092

91-
val startTestServer = task<LocalTestServer>("startTestServer") {
93+
val startTestServers = task<LocalTestServers>("startTestServers") {
9294
dependsOn(tasks["jvmJar"])
9395

94-
main = "aws.smithy.kotlin.runtime.http.test.util.TestServerKt"
96+
main = "aws.smithy.kotlin.runtime.http.test.util.TestServersKt"
9597
val kotlinCompilation = kotlin.targets.getByName("jvm").compilations["test"]
9698
classpath = (kotlinCompilation as org.jetbrains.kotlin.gradle.plugin.KotlinCompilationToRunnableFiles<*>).runtimeDependencyFiles
9799
}
98100

99101
val testTasks = listOf("allTests", "jvmTest")
100102
.forEach {
101103
tasks.named(it) {
102-
dependsOn(startTestServer)
104+
dependsOn(startTestServers)
103105
}
104106
}
105107

@@ -111,5 +113,5 @@ tasks.jvmTest {
111113
}
112114

113115
gradle.buildFinished {
114-
startTestServer.stop()
116+
startTestServers.stop()
115117
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.http.test
6+
7+
// TODO Finish once we have HTTP engine support for client certificates
8+
/*
9+
private const val TLS1_0_URL = "https://localhost:${TestServer.TlsV1.port}/"
10+
private const val TLS1_1_URL = "https://localhost:${TestServer.TlsV1_1.port}/"
11+
private const val TLS1_2_URL = "https://localhost:${TestServer.TlsV1_2.port}/"
12+
private const val TLS1_3_URL = "https://localhost:${TestServer.TlsV1_3.port}/"
13+
14+
class ConnectionTest : AbstractEngineTest() {
15+
private fun testMinTlsVersion(version: TlsVersion, failUrl: String?, succeedUrl: String) {
16+
testEngines {
17+
engineConfig {
18+
minTlsVersion = version
19+
}
20+
21+
failUrl?.let {
22+
test { env, client ->
23+
val req = HttpRequest {
24+
testSetup(env)
25+
url(Url.parse(failUrl))
26+
}
27+
28+
val call = client.call(req)
29+
call.complete()
30+
assertEquals(HttpStatusCode.UpgradeRequired, call.response.status)
31+
}
32+
}
33+
34+
test { env, client ->
35+
val req = HttpRequest {
36+
testSetup(env)
37+
url(Url.parse(succeedUrl))
38+
}
39+
40+
val call = client.call(req)
41+
call.complete()
42+
assertEquals(HttpStatusCode.OK, call.response.status)
43+
}
44+
}
45+
}
46+
47+
@Test
48+
fun testMinTls1_0() = testMinTlsVersion(TlsVersion.Tls1_0, null, TLS1_0_URL)
49+
50+
@Test
51+
fun testMinTls1_1() = testMinTlsVersion(TlsVersion.Tls1_1, TLS1_0_URL, TLS1_1_URL)
52+
53+
@Test
54+
fun testMinTls1_2() = testMinTlsVersion(TlsVersion.Tls1_2, TLS1_1_URL, TLS1_2_URL)
55+
56+
@Test
57+
fun testMinTls1_3() = testMinTlsVersion(TlsVersion.Tls1_3, TLS1_2_URL, TLS1_3_URL)
58+
}
59+
*/
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.http.test.suite
6+
7+
import io.ktor.server.application.*
8+
import io.ktor.server.response.*
9+
import io.ktor.server.routing.*
10+
11+
internal fun Application.tlsTests() {
12+
routing {
13+
get("/tlsVerification") {
14+
call.respondText("OK")
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)