-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: address testcontainers vulnerability by replacing with docker-…
…java
- Loading branch information
Showing
10 changed files
with
310 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
...p-client-engines/test-suite/jvm/test/aws/smithy/kotlin/runtime/http/test/MitmContainer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/* | ||
* 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) | ||
|
||
class MitmContainer(vararg options: String) : Closeable { | ||
private val delegate: Docker.Container | ||
|
||
init { | ||
val cmd = listOf( | ||
"mitmdump", | ||
"--flow-detail", | ||
"2", | ||
"-s", | ||
"$CONTAINER_MOUNT_POINT/fakeupstream.py", | ||
*options, | ||
).also { println("Initializing container with command: $it") } | ||
|
||
// Make proxy scripts from host available to container | ||
val binding = Bind(PROXY_SCRIPT_ROOT, Volume(CONTAINER_MOUNT_POINT), AccessMode.ro) | ||
|
||
try { | ||
delegate = Docker.Instance.createContainer(IMAGE_NAME, cmd, binding, exposedPort).apply { | ||
attachLogger(::println) | ||
start() | ||
waitUntilReady() | ||
} | ||
} catch (e: Throwable) { | ||
close() | ||
throw e | ||
} | ||
} | ||
|
||
val hostPort: Int | ||
get() = delegate.hostPort | ||
|
||
override fun close() = delegate.close() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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") | ||
} | ||
} | ||
|
||
|
@@ -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:[email protected]:$proxyPort") | ||
ProxyConfig.Http("http://testuser:[email protected]:$hostPort") | ||
} | ||
} | ||
|
||
|
153 changes: 153 additions & 0 deletions
153
...ttp-client-engines/test-suite/jvm/test/aws/smithy/kotlin/runtime/http/test/util/Docker.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
package aws.smithy.kotlin.runtime.http.test.util | ||
|
||
import aws.smithy.kotlin.runtime.util.OsFamily | ||
import aws.smithy.kotlin.runtime.util.PlatformProvider | ||
import com.github.dockerjava.api.async.ResultCallback | ||
import com.github.dockerjava.api.command.AsyncDockerCmd | ||
import com.github.dockerjava.api.command.SyncDockerCmd | ||
import com.github.dockerjava.api.model.* | ||
import com.github.dockerjava.core.DefaultDockerClientConfig | ||
import com.github.dockerjava.core.DockerClientImpl | ||
import com.github.dockerjava.zerodep.ZerodepDockerHttpClient | ||
import java.io.Closeable | ||
import java.io.IOException | ||
import java.net.InetAddress | ||
import java.net.InetSocketAddress | ||
import java.net.Socket | ||
import java.net.URI | ||
import kotlin.time.Duration.Companion.milliseconds | ||
import kotlin.time.Duration.Companion.seconds | ||
import kotlin.time.measureTimedValue | ||
|
||
private val MAX_POLL_TIME = 10.seconds | ||
private const val POLL_CONNECT_TIMEOUT_MS = 100 | ||
private val POLL_INTERVAL = 250.milliseconds | ||
|
||
/** | ||
* Wrapper class for the Docker client | ||
*/ | ||
class Docker { | ||
companion object { | ||
val Instance by lazy { Docker() } | ||
} | ||
|
||
private val client = run { | ||
val config = DefaultDockerClientConfig.createDefaultConfigBuilder().build() | ||
|
||
// Default Docker host locations according to Docker reference guide: | ||
// https://docs.docker.com/reference/cli/dockerd/#bind-docker-to-another-hostport-or-a-unix-socket | ||
val dockerHostString = when (PlatformProvider.System.osInfo().family) { | ||
OsFamily.Windows -> "tcp://localhost:2376" | ||
else -> "unix:///var/run/docker.sock" | ||
} | ||
|
||
val httpClient = ZerodepDockerHttpClient.Builder() | ||
.dockerHost(URI.create(dockerHostString)) | ||
.build() | ||
|
||
DockerClientImpl.getInstance(config, httpClient) | ||
} | ||
|
||
fun createContainer(imageName: String, cmd: List<String>, bind: Bind, exposedPort: ExposedPort): Container { | ||
val portBinding = PortBinding(Ports.Binding.empty(), exposedPort) | ||
|
||
val hostConfig = HostConfig | ||
.newHostConfig() | ||
.withBinds(bind) | ||
.withPortBindings(portBinding) | ||
|
||
val id = client | ||
.createContainerCmd(imageName) | ||
.withHostConfig(hostConfig) | ||
.withExposedPorts(exposedPort) | ||
.withCmd(cmd) | ||
.execAndMeasure { "Created container ${it.id}" } | ||
.id | ||
.substring(0..<12) // Short container IDs are 12 chars vs full container IDs at 64 chars | ||
|
||
return Container(id, exposedPort) | ||
} | ||
|
||
inner class Container(val id: String, val exposedPort: ExposedPort) : Closeable { | ||
private val poller = Poller(MAX_POLL_TIME, POLL_INTERVAL) | ||
|
||
fun attachLogger(handler: (String) -> Unit) { | ||
val logger = object : ResultCallback.Adapter<Frame>() { | ||
override fun onNext(frame: Frame?) { | ||
frame?.payload?.decodeToString()?.let(handler) | ||
} | ||
} | ||
|
||
client | ||
.attachContainerCmd(id) | ||
.withFollowStream(true) | ||
.withStdOut(true) | ||
.withStdErr(true) | ||
.withLogs(true) | ||
.execAndMeasure(logger) { "Attached logger to container $id" } | ||
.awaitStarted() | ||
} | ||
|
||
override fun close() { | ||
client | ||
.removeContainerCmd(id) | ||
.withForce(true) | ||
.exec() | ||
.also { println("Container $id removed") } | ||
} | ||
|
||
val hostPort: Int by lazy { | ||
poller.pollNotNull("Port $exposedPort in container $id") { | ||
client | ||
.inspectContainerCmd(id) | ||
.exec() | ||
.networkSettings | ||
.ports | ||
.bindings[exposedPort] | ||
?.first() | ||
?.hostPortSpec | ||
?.toInt() | ||
} | ||
} | ||
|
||
private fun isReady() = | ||
Socket().use { socket -> | ||
val endpoint = InetSocketAddress(InetAddress.getLocalHost(), hostPort) | ||
try { | ||
socket.connect(endpoint, POLL_CONNECT_TIMEOUT_MS) | ||
true | ||
} catch (e: IOException) { | ||
false | ||
} | ||
} | ||
|
||
fun start() { | ||
client.startContainerCmd(id).execAndMeasure { "Container $id running" } | ||
} | ||
|
||
fun waitUntilReady() = poller.pollTrue("Socket localHost:$hostPort → $exposedPort on container $id", ::isReady) | ||
} | ||
} | ||
|
||
private fun <T> SyncDockerCmd<T>.execAndMeasure(msg: (T) -> String): T { | ||
val (value, duration) = measureTimedValue { | ||
exec() | ||
} | ||
println("${msg(value)} in $duration") | ||
return value | ||
} | ||
|
||
private fun <C : AsyncDockerCmd<C, T>?, T, I : ResultCallback<T>> AsyncDockerCmd<C, T>.execAndMeasure( | ||
input: I, | ||
msg: (I) -> String, | ||
): I { | ||
val (value, duration) = measureTimedValue { | ||
exec(input) | ||
} | ||
println("${msg(value)} in $duration") | ||
return value | ||
} |
Oops, something went wrong.