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

chore: address testcontainers vulnerability by replacing with docker-java #1088

Merged
merged 9 commits into from
May 3, 2024
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ allprojects {
)
}
}

// Enables running `./gradlew allDeps` to get a comprehensive list of dependencies for every subproject
tasks.register<DependencyReportTask>("allDeps") { }
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this different from ./gradlew dependencies?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, ./gradlew dependencies only shows the dependencies of the target project (root if otherwise unspecified). I initially ran ./gradlew dependencies to grep for where we transitively consumed the Apache Commons Compress library but couldn't find it because it only showed me the root project dependencies.

Running ./gradlew :runtime:protocol:http-client-engines:test-suite:dependencies shows our use of Apache Commons Compress but then you have to know that :runtime:protocol:http-client-engines:test-suite is where to look. This new task registers an effective alias of dependencies for every subproject in such a way that it can be invoked at the root project and yield the dependencies for root and every subproject.

}

// configure the root multimodule docs
Expand Down
6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ kotest-version = "5.8.0"
kotlin-compile-testing-version = "1.5.0"
kotlinx-benchmark-version = "0.4.9"
kotlinx-serialization-version = "1.6.0"
testcontainers-version = "1.19.1"
docker-java-version = "3.3.6"
ktor-version = "2.3.6"
kaml-version = "0.55.0"
jsoup-version = "1.16.2"
Expand Down Expand Up @@ -81,8 +81,8 @@ kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.
kotest-assertions-core-jvm = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest-version" }
kotlinx-benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinx-benchmark-version" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-version" }
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers-version" }
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers-version" }
docker-core = { module = "com.github.docker-java:docker-java-core", version.ref = "docker-java-version" }
docker-transport-zerodep = { module = "com.github.docker-java:docker-java-transport-zerodep", version.ref = "docker-java-version" }

ktor-http-cio = { module = "io.ktor:ktor-http-cio", version.ref = "ktor-version" }
ktor-utils = { module = "io.ktor:ktor-utils", version.ref = "ktor-version" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ kotlin {

jvmTest {
dependencies {
implementation(libs.testcontainers)
implementation(libs.testcontainers.junit.jupiter)
implementation(libs.docker.core)
implementation(libs.docker.transport.zerodep)
}
}

Expand Down Expand Up @@ -114,9 +114,13 @@ tasks.jvmTest {
// set test environment for proxy tests
systemProperty("MITM_PROXY_SCRIPTS_ROOT", projectDir.resolve("proxy-scripts").absolutePath)
systemProperty("SSL_CONFIG_PATH", startTestServers.sslConfigPath)

val enableProxyTestsProp = "aws.test.http.enableProxyTests"
val runningInCodeBuild = System.getenv().containsKey("CODEBUILD_BUILD_ID")
systemProperty(enableProxyTestsProp, System.getProperties().getOrDefault(enableProxyTestsProp, !runningInCodeBuild))
val runningInLinux = System.getProperty("os.name").contains("Linux", ignoreCase = true)
val shouldRunProxyTests = !runningInCodeBuild && runningInLinux

systemProperty(enableProxyTestsProp, System.getProperties().getOrDefault(enableProxyTestsProp, shouldRunProxyTests))
}

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

import aws.smithy.kotlin.runtime.http.test.util.Docker
import com.github.dockerjava.api.model.AccessMode
import com.github.dockerjava.api.model.Bind
import com.github.dockerjava.api.model.ExposedPort
import com.github.dockerjava.api.model.Volume
import java.io.Closeable

private const val CONTAINER_MOUNT_POINT = "/home/mitmproxy/scripts"
private const val CONTAINER_PORT = 8080
private const val IMAGE_NAME = "mitmproxy/mitmproxy:8.1.0"
private val PROXY_SCRIPT_ROOT = System.getProperty("MITM_PROXY_SCRIPTS_ROOT") // defined by gradle script

// Port used for communication with container
private val exposedPort = ExposedPort.tcp(CONTAINER_PORT)

/**
* A Docker container which runs the **mitmproxy** image. Upon instantiating this class, a docker container will be
* created and ran with a logger attached echoing logs out to **STDOUT**. The container will be stopped and removed when
* [close] is called.
*/
class MitmContainer(vararg options: String) : Closeable {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: missing KDocs

private val delegate: Docker.Container

init {
val cmd = listOf(
"mitmdump", // https://docs.mitmproxy.org/stable/#mitmdump
"--flow-detail",
"2",
"-s",
"$CONTAINER_MOUNT_POINT/fakeupstream.py",
*options,
).also { println("Initializing container with command: $it") }

// Make proxy scripts from host filesystem available in container's filesystem
val binding = Bind(PROXY_SCRIPT_ROOT, Volume(CONTAINER_MOUNT_POINT), AccessMode.ro)

delegate = Docker.Instance.createContainer(IMAGE_NAME, cmd, binding, exposedPort)

try {
delegate.apply {
start()
waitUntilReady()
}
} catch (e: Throwable) {
close()
throw e
}
}

/**
* Gets the host port that can be used to communicate to the MITM proxy
*/
val hostPort: Int
get() = delegate.hostPort

override fun close() = delegate.close()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package aws.smithy.kotlin.runtime.http.test

import aws.smithy.kotlin.runtime.http.HttpStatusCode
Expand All @@ -18,49 +17,34 @@ import aws.smithy.kotlin.runtime.http.test.util.AbstractEngineTest
import aws.smithy.kotlin.runtime.http.test.util.engineConfig
import aws.smithy.kotlin.runtime.http.test.util.test
import aws.smithy.kotlin.runtime.net.url.Url
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.condition.EnabledIfSystemProperty
import org.testcontainers.containers.BindMode
import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.utility.DockerImageName
import kotlin.test.assertEquals

// defined by gradle script
private val PROXY_SCRIPT_ROOT = System.getProperty("MITM_PROXY_SCRIPTS_ROOT")
private fun mitmProxyContainer(
vararg options: String,
) = GenericContainer(DockerImageName.parse("mitmproxy/mitmproxy:8.1.0"))
.withExposedPorts(8080)
.withFileSystemBind(PROXY_SCRIPT_ROOT, "/home/mitmproxy/scripts", BindMode.READ_ONLY)
.withLogConsumer {
print(it.utf8String)
}.apply {
val command = buildString {
// load the custom addon which by default does nothing without setting additional options
append("mitmdump --flow-detail 2 -s /home/mitmproxy/scripts/fakeupstream.py")
append(options.joinToString(separator = " ", prefix = " "))
}
withCommand(command)
}

@Testcontainers(disabledWithoutDocker = true)
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // enables non-static @BeforeAll/@AfterAll methods
@EnabledIfSystemProperty(named = "aws.test.http.enableProxyTests", matches = "true")
class ProxyTest : AbstractEngineTest() {
private lateinit var mitmProxy: MitmContainer

@BeforeAll
fun setUp() {
mitmProxy = MitmContainer("--set", "fakeupstream=aws.amazon.com")
}

@Container
val mitmProxy = mitmProxyContainer("--set fakeupstream=aws.amazon.com")
@AfterAll
fun cleanUp() {
mitmProxy.close()
}

@Test
fun testHttpProxy() = testEngines(
// we would expect a customer to configure proxy support on the underlying engine
skipEngines = setOf("KtorEngine"),
) {
fun testHttpProxy() = testEngines {
engineConfig {
val proxyPort = mitmProxy.getMappedPort(8080)
val hostPort = mitmProxy.hostPort
proxySelector = ProxySelector {
ProxyConfig.Http("http://127.0.0.1:$proxyPort")
ProxyConfig.Http("http://127.0.0.1:$hostPort")
}
}

Expand All @@ -70,22 +54,27 @@ class ProxyTest : AbstractEngineTest() {
}
}

@Testcontainers(disabledWithoutDocker = true)
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // enables non-static @BeforeAll/@AfterAll methods
@EnabledIfSystemProperty(named = "aws.test.http.enableProxyTests", matches = "true")
class ProxyAuthTest : AbstractEngineTest() {
private lateinit var mitmProxy: MitmContainer

@Container
val mitmProxy = mitmProxyContainer("--proxyauth testuser:testpass --set fakeupstream=aws.amazon.com")
@BeforeAll
fun setUp() {
mitmProxy = MitmContainer("--proxyauth", "testuser:testpass", "--set", "fakeupstream=aws.amazon.com")
}

@AfterAll
fun cleanUp() {
mitmProxy.close()
}

@Test
fun testHttpProxyAuth() = testEngines(
// we would expect a customer to configure proxy support on the underlying engine
skipEngines = setOf("KtorEngine"),
) {
fun testHttpProxyAuth() = testEngines {
engineConfig {
val proxyPort = mitmProxy.getMappedPort(8080)
val hostPort = mitmProxy.hostPort
proxySelector = ProxySelector {
ProxyConfig.Http("http://testuser:testpass@127.0.0.1:$proxyPort")
ProxyConfig.Http("http://testuser:testpass@127.0.0.1:$hostPort")
}
}

Expand Down