Skip to content
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

feat: add HTTP engine config for min TLS version #844

Merged
merged 6 commits into from
May 3, 2023
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
8 changes: 8 additions & 0 deletions .changes/1af71df0-f584-493e-85ab-2ee3e853c827.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "1af71df0-f584-493e-85ab-2ee3e853c827",
"type": "feature",
"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.",
"issues": [
"awslabs/smithy-kotlin#661"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,15 @@ public final class aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig
public synthetic fun <init> (Laws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig$Builder;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getClientBootstrap ()Laws/sdk/kotlin/crt/io/ClientBootstrap;
public final fun getInitialWindowSizeBytes ()I
public final fun getTlsContext ()Laws/sdk/kotlin/crt/io/TlsContext;
public final fun setClientBootstrap (Laws/sdk/kotlin/crt/io/ClientBootstrap;)V
public final fun setTlsContext (Laws/sdk/kotlin/crt/io/TlsContext;)V
}

public final class aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig$Builder : aws/smithy/kotlin/runtime/http/engine/HttpClientEngineConfig$Builder {
public fun <init> ()V
public final fun getClientBootstrap ()Laws/sdk/kotlin/crt/io/ClientBootstrap;
public final fun getInitialWindowSizeBytes ()I
public final fun getTlsContext ()Laws/sdk/kotlin/crt/io/TlsContext;
public final fun setClientBootstrap (Laws/sdk/kotlin/crt/io/ClientBootstrap;)V
public final fun setInitialWindowSizeBytes (I)V
public final fun setTlsContext (Laws/sdk/kotlin/crt/io/TlsContext;)V
}

public final class aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig$Companion {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@

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

import aws.sdk.kotlin.crt.http.*
import aws.sdk.kotlin.crt.io.*
import aws.sdk.kotlin.crt.http.HttpClientConnectionManager
import aws.sdk.kotlin.crt.http.HttpClientConnectionManagerOptionsBuilder
import aws.sdk.kotlin.crt.http.HttpProxyAuthorizationType
import aws.sdk.kotlin.crt.http.HttpProxyOptions
import aws.sdk.kotlin.crt.io.SocketOptions
import aws.sdk.kotlin.crt.io.TlsContextOptionsBuilder
import aws.sdk.kotlin.crt.io.Uri
import aws.smithy.kotlin.runtime.crt.SdkDefaultIO
import aws.smithy.kotlin.runtime.http.HttpErrorCode
import aws.smithy.kotlin.runtime.http.HttpException
Expand All @@ -27,6 +32,9 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import aws.sdk.kotlin.crt.io.TlsContext as CrtTlsContext
import aws.sdk.kotlin.crt.io.TlsVersion as CrtTlsVersion
import aws.smithy.kotlin.runtime.config.TlsVersion as SdkTlsVersion

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

private val customTlsContext: TlsContext? = if (config.alpn.isNotEmpty() && config.tlsContext == null) {
val options = TlsContextOptionsBuilder().apply {
private val crtTlsContext: CrtTlsContext = TlsContextOptionsBuilder()
.apply {
verifyPeer = true
alpn = config.alpn.joinToString(separator = ";") { it.protocolId }
}.build()
TlsContext(options)
} else {
null
}
alpn = config.tlsContext.alpn.joinToString(separator = ";") { it.protocolId }
minTlsVersion = toCrtTlsVersion(config.tlsContext.minVersion)
}
.build()
.let(::CrtTlsContext)

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

private val options = HttpClientConnectionManagerOptionsBuilder().apply {
clientBootstrap = config.clientBootstrap ?: SdkDefaultIO.ClientBootstrap
tlsContext = customTlsContext ?: config.tlsContext ?: SdkDefaultIO.TlsContext
tlsContext = crtTlsContext
manualWindowManagement = true
socketOptions = SocketOptions(
connectTimeoutMs = config.connectTimeout.inWholeMilliseconds.toInt(),
Expand Down Expand Up @@ -129,7 +136,7 @@ public class CrtHttpEngine(public val config: CrtHttpEngineConfig) : HttpClientE
// close all resources
// SAFETY: shutdown is only invoked once AND only after all requests have completed and no more are coming
connManagers.forEach { entry -> entry.value.close() }
customTlsContext?.close()
crtTlsContext.close()
}

private suspend fun getManagerForUri(uri: Uri, proxyConfig: ProxyConfig): HttpClientConnectionManager = mutex.withLock {
Expand All @@ -151,3 +158,11 @@ public class CrtHttpEngine(public val config: CrtHttpEngineConfig) : HttpClientE
}
}
}

private fun toCrtTlsVersion(sdkTlsVersion: SdkTlsVersion?): CrtTlsVersion = when (sdkTlsVersion) {
null -> CrtTlsVersion.SYS_DEFAULT
SdkTlsVersion.TLS_1_0 -> CrtTlsVersion.TLSv1
SdkTlsVersion.TLS_1_1 -> CrtTlsVersion.TLS_V1_1
SdkTlsVersion.TLS_1_2 -> CrtTlsVersion.TLS_V1_2
SdkTlsVersion.TLS_1_3 -> CrtTlsVersion.TLS_V1_3
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package aws.smithy.kotlin.runtime.http.engine.crt

import aws.sdk.kotlin.crt.io.ClientBootstrap
import aws.sdk.kotlin.crt.io.TlsContext
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineConfig

/**
Expand Down Expand Up @@ -35,11 +34,6 @@ public class CrtHttpEngineConfig private constructor(builder: Builder) : HttpCli
*/
public var clientBootstrap: ClientBootstrap? = builder.clientBootstrap

/**
* The TLS context to use. By default it is a shared instance.
*/
public var tlsContext: TlsContext? = builder.tlsContext

public class Builder : HttpClientEngineConfig.Builder() {
/**
* Set the amount of data that can be buffered before reading from the socket will cease. Reading will
Expand All @@ -52,11 +46,6 @@ public class CrtHttpEngineConfig private constructor(builder: Builder) : HttpCli
*/
public var clientBootstrap: ClientBootstrap? = null

/**
* Set the TLS context to use. By default it is a shared instance.
*/
public var tlsContext: TlsContext? = null

internal fun build(): CrtHttpEngineConfig = CrtHttpEngineConfig(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

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

import aws.smithy.kotlin.runtime.config.TlsVersion
import aws.smithy.kotlin.runtime.http.engine.AlpnId
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase
import aws.smithy.kotlin.runtime.http.engine.TlsContext
import aws.smithy.kotlin.runtime.http.engine.callContext
import aws.smithy.kotlin.runtime.http.request.HttpRequest
import aws.smithy.kotlin.runtime.http.response.HttpCall
Expand All @@ -17,6 +19,8 @@ import kotlinx.coroutines.job
import okhttp3.*
import java.util.concurrent.TimeUnit
import kotlin.time.toJavaDuration
import aws.smithy.kotlin.runtime.config.TlsVersion as SdkTlsVersion
import okhttp3.TlsVersion as OkHttpTlsVersion

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

connectionSpecs(listOf(minTlsConnectionSpec(config.tlsContext), ConnectionSpec.CLEARTEXT))

// Transient connection errors are handled by retry strategy (exceptions are wrapped and marked retryable
// appropriately internally). We don't want inner retry logic that inflates the number of retries.
retryOnConnectionFailure(false)
Expand Down Expand Up @@ -97,8 +103,8 @@ private fun OkHttpEngineConfig.buildClient(): OkHttpClient {
eventListenerFactory { call -> HttpEngineEventListener(pool, config.hostResolver, call) }

// map protocols
if (config.alpn.isNotEmpty()) {
val protocols = config.alpn.mapNotNull {
if (config.tlsContext.alpn.isNotEmpty()) {
val protocols = config.tlsContext.alpn.mapNotNull {
when (it) {
AlpnId.HTTP1_1 -> Protocol.HTTP_1_1
AlpnId.HTTP2 -> Protocol.HTTP_2
Expand All @@ -115,3 +121,24 @@ private fun OkHttpEngineConfig.buildClient(): OkHttpClient {
dns(OkHttpDns(config.hostResolver))
}.build()
}

private fun minTlsConnectionSpec(tlsContext: TlsContext): ConnectionSpec {
val minVersion = tlsContext.minVersion ?: TlsVersion.TLS_1_2
val okHttpTlsVersions = SdkTlsVersion
.values()
.filter { it >= minVersion }
.sortedDescending() // Prioritize higher TLS versions first
.map(::toOkHttpTlsVersion)
.toTypedArray()
return ConnectionSpec
.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(*okHttpTlsVersions)
.build()
}

private fun toOkHttpTlsVersion(sdkTlsVersion: SdkTlsVersion): OkHttpTlsVersion = when (sdkTlsVersion) {
SdkTlsVersion.TLS_1_0 -> OkHttpTlsVersion.TLS_1_0
SdkTlsVersion.TLS_1_1 -> OkHttpTlsVersion.TLS_1_1
SdkTlsVersion.TLS_1_2 -> OkHttpTlsVersion.TLS_1_2
SdkTlsVersion.TLS_1_3 -> OkHttpTlsVersion.TLS_1_3
}
24 changes: 13 additions & 11 deletions runtime/protocol/http-client-engines/test-suite/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ kotlin {
implementation(project(":runtime:protocol:http-test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
implementation(project(":runtime:testing"))

implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion")
}
}

jvmMain {
dependencies {
implementation("io.ktor:ktor-server-cio:$ktorVersion")
implementation("io.ktor:ktor-server-jetty:$ktorVersion")

implementation(project(":runtime:protocol:http-client-engines:http-client-engine-default"))
implementation(project(":runtime:protocol:http-client-engines:http-client-engine-crt"))
Expand All @@ -50,7 +52,7 @@ kotlin {
}
}

open class LocalTestServer : DefaultTask() {
open class LocalTestServers : DefaultTask() {
@Internal
var server: Closeable? = null
private set
Expand All @@ -64,42 +66,42 @@ open class LocalTestServer : DefaultTask() {
@TaskAction
fun exec() {
try {
println("[TestServer] start")
println("[TestServers] start")
val urlClassLoaderSource = classpath.map { file -> file.toURI().toURL() }.toTypedArray()
val loader = URLClassLoader(urlClassLoaderSource, ClassLoader.getSystemClassLoader())

val mainClass = loader.loadClass(main)
val main = mainClass.getMethod("startServer")
val main = mainClass.getMethod("startServers")
server = main.invoke(null) as Closeable
println("[TestServer] started")
println("[TestServers] started")
} catch (cause: Throwable) {
println("[TestServer] failed: ${cause.message}")
println("[TestServers] failed: ${cause.message}")
throw cause
}
}

fun stop() {
if (server != null) {
server?.close()
println("[TestServer] stop")
println("[TestServers] stop")
}
}
}

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

val startTestServer = task<LocalTestServer>("startTestServer") {
val startTestServers = task<LocalTestServers>("startTestServers") {
dependsOn(tasks["jvmJar"])

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

val testTasks = listOf("allTests", "jvmTest")
.forEach {
tasks.named(it) {
dependsOn(startTestServer)
dependsOn(startTestServers)
}
}

Expand All @@ -111,5 +113,5 @@ tasks.jvmTest {
}

gradle.buildFinished {
startTestServer.stop()
startTestServers.stop()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.smithy.kotlin.runtime.http.test

// TODO Finish once we have HTTP engine support for client certificates
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a note to the TLS configuration issue to finish this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added note to #820.

/*
private const val TLS1_0_URL = "https://localhost:${TestServer.TlsV1.port}/"
private const val TLS1_1_URL = "https://localhost:${TestServer.TlsV1_1.port}/"
private const val TLS1_2_URL = "https://localhost:${TestServer.TlsV1_2.port}/"
private const val TLS1_3_URL = "https://localhost:${TestServer.TlsV1_3.port}/"

class ConnectionTest : AbstractEngineTest() {
private fun testMinTlsVersion(version: TlsVersion, failUrl: String?, succeedUrl: String) {
testEngines {
engineConfig {
minTlsVersion = version
}

failUrl?.let {
test { env, client ->
val req = HttpRequest {
testSetup(env)
url(Url.parse(failUrl))
}

val call = client.call(req)
call.complete()
assertEquals(HttpStatusCode.UpgradeRequired, call.response.status)
}
}

test { env, client ->
val req = HttpRequest {
testSetup(env)
url(Url.parse(succeedUrl))
}

val call = client.call(req)
call.complete()
assertEquals(HttpStatusCode.OK, call.response.status)
}
}
}

@Test
fun testMinTls1_0() = testMinTlsVersion(TlsVersion.Tls1_0, null, TLS1_0_URL)

@Test
fun testMinTls1_1() = testMinTlsVersion(TlsVersion.Tls1_1, TLS1_0_URL, TLS1_1_URL)

@Test
fun testMinTls1_2() = testMinTlsVersion(TlsVersion.Tls1_2, TLS1_1_URL, TLS1_2_URL)

@Test
fun testMinTls1_3() = testMinTlsVersion(TlsVersion.Tls1_3, TLS1_2_URL, TLS1_3_URL)
}
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.smithy.kotlin.runtime.http.test.suite

import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

internal fun Application.tlsTests() {
routing {
get("/tlsVerification") {
call.respondText("OK")
}
}
}
Loading