diff --git a/build.gradle b/build.gradle index f3f5d2a..60c25f3 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ dependencies { compileOnly group: 'io.ktor', name: 'ktor-client-cio', version: '1.5.2' compileOnly group: 'com.typesafe.akka', name: 'akka-http-core_2.13', version: '10.2.4' compileOnly group: 'com.typesafe.akka', name: 'akka-actor_2.13', version: '2.6.13' + compileOnly group: 'io.vertx', name: 'vertx-core', version: '4.2.2' // Test deps: testImplementation group: 'io.kotest', name: 'kotest-runner-junit5-jvm', version: '4.4.0' diff --git a/src/main/java/tech/httptoolkit/javaagent/advice/vertxclient/VertxHttpClientReturnProxyConfigurationAdvice.java b/src/main/java/tech/httptoolkit/javaagent/advice/vertxclient/VertxHttpClientReturnProxyConfigurationAdvice.java new file mode 100644 index 0000000..8a777e4 --- /dev/null +++ b/src/main/java/tech/httptoolkit/javaagent/advice/vertxclient/VertxHttpClientReturnProxyConfigurationAdvice.java @@ -0,0 +1,13 @@ +package tech.httptoolkit.javaagent.advice.vertxclient; + +import io.vertx.core.net.ProxyOptions; +import net.bytebuddy.asm.Advice; +import tech.httptoolkit.javaagent.HttpProxyAgent; + +public class VertxHttpClientReturnProxyConfigurationAdvice { + + @Advice.OnMethodExit + public static void getProxyConfiguration(@Advice.Return(readOnly = false) ProxyOptions returnValue) { + returnValue = new ProxyOptions().setHost(HttpProxyAgent.getAgentProxyHost()).setPort(HttpProxyAgent.getAgentProxyPort()); + } +} diff --git a/src/main/java/tech/httptoolkit/javaagent/advice/vertxclient/VertxNetClientOptionsSetTrustOptionsAdvice.java b/src/main/java/tech/httptoolkit/javaagent/advice/vertxclient/VertxNetClientOptionsSetTrustOptionsAdvice.java new file mode 100644 index 0000000..6635a8f --- /dev/null +++ b/src/main/java/tech/httptoolkit/javaagent/advice/vertxclient/VertxNetClientOptionsSetTrustOptionsAdvice.java @@ -0,0 +1,21 @@ +package tech.httptoolkit.javaagent.advice.vertxclient; + +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.net.ClientOptionsBase; +import io.vertx.core.net.NetClientOptions; +import io.vertx.core.net.TrustOptions; +import net.bytebuddy.asm.Advice; +import tech.httptoolkit.javaagent.HttpProxyAgent; + +public class VertxNetClientOptionsSetTrustOptionsAdvice { + + @Advice.OnMethodExit + public static void afterConstructor( + @Advice.This NetClientOptions thisNetClientOptions, + @Advice.Argument(value = 0) ClientOptionsBase other + ) { + if (other instanceof HttpClientOptions) { + thisNetClientOptions.setTrustOptions(TrustOptions.wrap(HttpProxyAgent.getInterceptedTrustManagerFactory().getTrustManagers()[0])); + } + } +} diff --git a/src/main/kotlin/tech/httptoolkit/javaagent/AgentMain.kt b/src/main/kotlin/tech/httptoolkit/javaagent/AgentMain.kt index 711e333..b49cae8 100644 --- a/src/main/kotlin/tech/httptoolkit/javaagent/AgentMain.kt +++ b/src/main/kotlin/tech/httptoolkit/javaagent/AgentMain.kt @@ -117,7 +117,9 @@ fun interceptAllHttps(config: Config, instrumentation: Instrumentation) { AkkaHttpTransformer(logger), AkkaPoolSettingsTransformer(logger), AkkaPoolTransformer(logger), - AkkaGatewayTransformer(logger) + AkkaGatewayTransformer(logger), + VertxHttpClientTransformer(logger), + VertxNetClientOptionsTransformer(logger), ).forEach { matchingAgentTransformer -> agentBuilder = matchingAgentTransformer.register(agentBuilder) } @@ -171,4 +173,4 @@ private fun setDefaultProxy(proxyHost: String, proxyPort: Int) { private fun setDefaultSslContext(context: SSLContext) { SSLContext.setDefault(context) HttpsURLConnection.setDefaultSSLSocketFactory(context.socketFactory) -} \ No newline at end of file +} diff --git a/src/main/kotlin/tech/httptoolkit/javaagent/VertxHttpClientTransformer.kt b/src/main/kotlin/tech/httptoolkit/javaagent/VertxHttpClientTransformer.kt new file mode 100644 index 0000000..1413ded --- /dev/null +++ b/src/main/kotlin/tech/httptoolkit/javaagent/VertxHttpClientTransformer.kt @@ -0,0 +1,23 @@ +package tech.httptoolkit.javaagent + +import net.bytebuddy.agent.builder.AgentBuilder +import net.bytebuddy.asm.Advice +import net.bytebuddy.dynamic.DynamicType +import net.bytebuddy.matcher.ElementMatchers.* + +// Ensures that the proxy is used by overriding the getProxyOptions method of HttpClientImpl +// to always return our proxy information +class VertxHttpClientTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { + override fun register(builder: AgentBuilder): AgentBuilder { + return builder + .type( + named("io.vertx.core.http.impl.HttpClientImpl") + ).transform(this) + } + + override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { + return builder + .visit(loadAdvice("tech.httptoolkit.javaagent.advice.vertxclient.VertxHttpClientReturnProxyConfigurationAdvice") + .on(hasMethodName("getProxyOptions"))) + } +} diff --git a/src/main/kotlin/tech/httptoolkit/javaagent/VertxNetClientOptionsTransformer.kt b/src/main/kotlin/tech/httptoolkit/javaagent/VertxNetClientOptionsTransformer.kt new file mode 100644 index 0000000..4c67249 --- /dev/null +++ b/src/main/kotlin/tech/httptoolkit/javaagent/VertxNetClientOptionsTransformer.kt @@ -0,0 +1,27 @@ +package tech.httptoolkit.javaagent + +import net.bytebuddy.agent.builder.AgentBuilder +import net.bytebuddy.asm.Advice +import net.bytebuddy.description.method.MethodDescription +import net.bytebuddy.dynamic.DynamicType +import net.bytebuddy.matcher.ElementMatchers.* + +// Ensures that the proxy is trusted by setting the Vert'x TrustOptions based on the TrustManager +// created by the agent +class VertxNetClientOptionsTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { + override fun register(builder: AgentBuilder): AgentBuilder { + return builder + .type( + named("io.vertx.core.net.NetClientOptions") + ).transform(this) + } + + override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { + return builder + .visit(loadAdvice("tech.httptoolkit.javaagent.advice.vertxclient.VertxNetClientOptionsSetTrustOptionsAdvice") + .on( + isConstructor() + .and(takesArguments(1)) + .and(takesArgument(0, named("io.vertx.core.net.ClientOptionsBase"))))) + } +} diff --git a/test-app/build.gradle b/test-app/build.gradle index 3707249..4e1a20d 100644 --- a/test-app/build.gradle +++ b/test-app/build.gradle @@ -32,6 +32,9 @@ dependencies { implementation group: 'com.typesafe.akka', name: 'akka-actor_2.13', version: '2.6.13' implementation group: 'com.typesafe.akka', name: 'akka-http_2.13', version: '10.2.4' implementation group: 'com.typesafe.akka', name: 'akka-stream_2.13', version: '2.6.13' + + implementation group: 'io.vertx', name: 'vertx-core', version: '4.2.2' + implementation group: 'io.vertx', name: 'vertx-web-client', version: '4.2.2' } test { @@ -56,4 +59,4 @@ shadowJar { resource = 'reference.conf' // Required for akka } with jar -} \ No newline at end of file +} diff --git a/test-app/src/main/java/tech/httptoolkit/testapp/Main.java b/test-app/src/main/java/tech/httptoolkit/testapp/Main.java index dcc0071..c3e7784 100644 --- a/test-app/src/main/java/tech/httptoolkit/testapp/Main.java +++ b/test-app/src/main/java/tech/httptoolkit/testapp/Main.java @@ -30,7 +30,9 @@ public class Main { entry("spring-web", new SpringWebClientCase()), entry("ktor-cio", new KtorCioCase()), entry("akka-req-http", new AkkaRequestClientCase()), - entry("akka-host-http", new AkkaHostClientCase()) + entry("akka-host-http", new AkkaHostClientCase()), + entry("vertx-httpclient", new VertxHttpClientCase()), + entry("vertx-webclient", new VertxWebClientCase()) ); public static void main(String[] args) throws Exception { diff --git a/test-app/src/main/java/tech/httptoolkit/testapp/cases/VertxHttpClientCase.java b/test-app/src/main/java/tech/httptoolkit/testapp/cases/VertxHttpClientCase.java new file mode 100644 index 0000000..a8371ba --- /dev/null +++ b/test-app/src/main/java/tech/httptoolkit/testapp/cases/VertxHttpClientCase.java @@ -0,0 +1,26 @@ +package tech.httptoolkit.testapp.cases; + +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpMethod; + +import java.net.URISyntaxException; +import java.util.concurrent.ExecutionException; + +public class VertxHttpClientCase extends ClientCase { + @Override + public HttpClient newClient(String url) throws Exception { + return Vertx.vertx().createHttpClient(); + } + + @Override + public int test(String url, HttpClient client) throws URISyntaxException, InterruptedException, ExecutionException { + HttpClientResponse response = client + .request(HttpMethod.GET, url) + .compose(HttpClientRequest::send) + .toCompletionStage().toCompletableFuture().get(); + return response.statusCode(); + } +} diff --git a/test-app/src/main/java/tech/httptoolkit/testapp/cases/VertxWebClientCase.java b/test-app/src/main/java/tech/httptoolkit/testapp/cases/VertxWebClientCase.java new file mode 100644 index 0000000..0e8648c --- /dev/null +++ b/test-app/src/main/java/tech/httptoolkit/testapp/cases/VertxWebClientCase.java @@ -0,0 +1,25 @@ +package tech.httptoolkit.testapp.cases; + +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.client.WebClient; +import java.net.URISyntaxException; +import java.util.concurrent.ExecutionException; + +public class VertxWebClientCase extends ClientCase { + @Override + public WebClient newClient(String url) throws Exception { + return WebClient.create(Vertx.vertx()); + } + + @Override + public int test(String url, WebClient client) throws URISyntaxException, InterruptedException, ExecutionException { + HttpResponse response = client + .request(HttpMethod.GET, url) + .send() + .toCompletionStage().toCompletableFuture().get(); + return response.statusCode(); + } +}