Skip to content

Commit e06a004

Browse files
committed
Add support for async-http-client
1 parent f5efab3 commit e06a004

File tree

13 files changed

+193
-8
lines changed

13 files changed

+193
-8
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Traffic can be captured from at least:
1616
- [x] OkHttp v2, v3 & v4
1717
- [x] Retrofit
1818
- [x] Jetty-Client v9, 10 & 11
19+
- [x] Async-Http-Client
1920

2021
This will also capture HTTP(S) from any downstream libraries based on each of these clients, and many other untested clients sharing similar implementations, and so should cover a very large percentage of HTTP client usage.
2122

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies {
2929
compileOnly group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5'
3030
compileOnly group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0.3'
3131
compileOnly group: 'org.eclipse.jetty', name: 'jetty-client', version: '11.0.1'
32+
compileOnly group: 'org.asynchttpclient', name: 'async-http-client', version: '2.12.2'
3233

3334
// Test deps:
3435
testImplementation group: 'io.kotest', name: 'kotest-runner-junit5-jvm', version: '4.4.0'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package tech.httptoolkit.javaagent.asynchttpclient;
2+
3+
import net.bytebuddy.asm.Advice;
4+
import org.asynchttpclient.proxy.ProxyServer;
5+
import org.asynchttpclient.proxy.ProxyServerSelector;
6+
import tech.httptoolkit.javaagent.HttpProxyAgent;
7+
8+
import java.net.ProxySelector;
9+
import java.util.Optional;
10+
11+
public class AsyncHttpClientReturnProxySelectorAdvice {
12+
13+
public static ProxyServerSelector proxyServerSelector = uri -> new ProxyServer.Builder(
14+
HttpProxyAgent.getAgentProxyHost(),
15+
HttpProxyAgent.getAgentProxyPort()
16+
).build();
17+
18+
@Advice.OnMethodExit
19+
public static void getProxyServerSelector(@Advice.Return(readOnly = false) ProxyServerSelector returnValue) {
20+
returnValue = proxyServerSelector;
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package tech.httptoolkit.javaagent.asynchttpclient;
2+
3+
import io.netty.handler.ssl.SslContext;
4+
import io.netty.handler.ssl.SslContextBuilder;
5+
import net.bytebuddy.asm.Advice;
6+
import tech.httptoolkit.javaagent.HttpProxyAgent;
7+
8+
import javax.net.ssl.SSLException;
9+
10+
public class AsyncHttpClientReturnSslContextAdvice {
11+
@Advice.OnMethodExit
12+
public static void getSslContext(@Advice.Return(readOnly = false) SslContext returnValue) {
13+
try {
14+
returnValue = SslContextBuilder
15+
.forClient()
16+
.trustManager(HttpProxyAgent.getInterceptedTrustManagerFactory())
17+
.build();
18+
} catch (SSLException e) {
19+
throw new RuntimeException(e);
20+
}
21+
}
22+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package tech.httptoolkit.javaagent.asynchttpclient;
2+
3+
import net.bytebuddy.asm.Advice;
4+
import org.asynchttpclient.AsyncHttpClientConfig;
5+
import org.asynchttpclient.SslEngineFactory;
6+
7+
import java.util.Collections;
8+
import java.util.Set;
9+
import java.util.WeakHashMap;
10+
11+
public class AsyncHttpResetSslEngineFactoryAdvice {
12+
13+
// Track each ChannelManager with a weak ref, to avoid unnecessary reflection overhead by only
14+
// initializing them once, instead of every request
15+
public static Set<Object> patchedChannelManagers = Collections.newSetFromMap(new WeakHashMap());
16+
17+
@Advice.OnMethodEnter
18+
public static void createSslHandler(
19+
@Advice.This Object thisChannelManager
20+
) {
21+
if (patchedChannelManagers.contains(thisChannelManager)) return;
22+
23+
try {
24+
Class<?> ChannelManager = thisChannelManager.getClass();
25+
26+
SslEngineFactory sslEngineFactory = (SslEngineFactory) ChannelManager
27+
.getDeclaredField("sslEngineFactory")
28+
.get(thisChannelManager);
29+
30+
AsyncHttpClientConfig config = (AsyncHttpClientConfig) ChannelManager
31+
.getDeclaredField("config")
32+
.get(thisChannelManager);
33+
34+
// Reinitialize the SSL Engine from the config (which uses our new cert)
35+
// before building the SSL handler.
36+
sslEngineFactory.init(config);
37+
} catch (Exception e) {
38+
throw new RuntimeException(e);
39+
}
40+
41+
patchedChannelManagers.add(thisChannelManager);
42+
}
43+
}

src/main/kotlin/tech/httptoolkit/javaagent/AgentMain.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ import java.lang.instrument.Instrumentation
1010
import javax.net.ssl.SSLContext
1111
import java.net.*
1212
import javax.net.ssl.HttpsURLConnection
13+
import javax.net.ssl.TrustManagerFactory
1314

1415

1516
lateinit var InterceptedSslContext: SSLContext
1617
private set
1718

19+
lateinit var InterceptedTrustManagerFactory: TrustManagerFactory
20+
private set
21+
1822
lateinit var AgentProxyHost: String
1923
private set
2024

@@ -48,7 +52,8 @@ fun agentmain(arguments: String?, instrumentation: Instrumentation) {
4852
fun interceptAllHttps(config: Config, instrumentation: Instrumentation) {
4953
val (certPath, proxyHost, proxyPort) = config
5054

51-
InterceptedSslContext = buildSslContextForCertificate(certPath)
55+
InterceptedTrustManagerFactory = buildTrustManagerFactoryForCertificate(certPath)
56+
InterceptedSslContext = buildSslContextForCertificate(InterceptedTrustManagerFactory)
5257
AgentProxyHost = proxyHost
5358
AgentProxyPort = proxyPort
5459

@@ -74,7 +79,9 @@ fun interceptAllHttps(config: Config, instrumentation: Instrumentation) {
7479
ApacheClientRoutingV5Transformer(),
7580
ApacheSslSocketFactoryTransformer(),
7681
JavaClientTransformer(),
77-
JettyClientTransformer()
82+
JettyClientTransformer(),
83+
AsyncHttpClientConfigTransformer(),
84+
AsyncHttpChannelManagerTransformer()
7885
).forEach { matchingAgentTransformer ->
7986
agentBuilder = matchingAgentTransformer.register(agentBuilder)
8087
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package tech.httptoolkit.javaagent
2+
3+
import net.bytebuddy.agent.builder.AgentBuilder
4+
import net.bytebuddy.asm.Advice
5+
import net.bytebuddy.description.type.TypeDescription
6+
import net.bytebuddy.dynamic.DynamicType
7+
import net.bytebuddy.utility.JavaModule
8+
import net.bytebuddy.matcher.ElementMatchers.*
9+
import tech.httptoolkit.javaagent.asynchttpclient.AsyncHttpClientReturnProxySelectorAdvice
10+
import tech.httptoolkit.javaagent.asynchttpclient.AsyncHttpClientReturnSslContextAdvice
11+
import tech.httptoolkit.javaagent.asynchttpclient.AsyncHttpResetSslEngineFactoryAdvice
12+
13+
// For new clients, we just need to override the properties on the convenient config
14+
// class that contains both proxy & SSL configuration.
15+
class AsyncHttpClientConfigTransformer : MatchingAgentTransformer {
16+
override fun register(builder: AgentBuilder): AgentBuilder {
17+
return builder
18+
.type(
19+
hasSuperType(named("org.asynchttpclient.AsyncHttpClientConfig"))
20+
).and(
21+
not(isInterface())
22+
).transform(this)
23+
}
24+
25+
override fun transform(
26+
builder: DynamicType.Builder<*>,
27+
typeDescription: TypeDescription,
28+
classLoader: ClassLoader?,
29+
module: JavaModule?
30+
): DynamicType.Builder<*>? {
31+
return builder
32+
.visit(Advice.to(AsyncHttpClientReturnSslContextAdvice::class.java)
33+
.on(hasMethodName("getSslContext")))
34+
.visit(Advice.to(AsyncHttpClientReturnProxySelectorAdvice::class.java)
35+
.on(hasMethodName("getProxyServerSelector")))
36+
}
37+
}
38+
39+
// For existing classes, we need to hook SSL Handler creation, called for
40+
// every new connection
41+
class AsyncHttpChannelManagerTransformer : MatchingAgentTransformer {
42+
override fun register(builder: AgentBuilder): AgentBuilder {
43+
return builder
44+
.type(
45+
named("org.asynchttpclient.netty.channel.ChannelManager")
46+
).transform(this)
47+
}
48+
49+
override fun transform(
50+
builder: DynamicType.Builder<*>,
51+
typeDescription: TypeDescription,
52+
classLoader: ClassLoader?,
53+
module: JavaModule?
54+
): DynamicType.Builder<*>? {
55+
return builder
56+
.visit(Advice.to(AsyncHttpResetSslEngineFactoryAdvice::class.java)
57+
.on(hasMethodName("createSslHandler")))
58+
}
59+
}

src/main/kotlin/tech/httptoolkit/javaagent/CustomSslContext.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import java.security.cert.CertificateFactory
88
import javax.net.ssl.SSLContext
99
import javax.net.ssl.TrustManagerFactory
1010

11-
fun buildSslContextForCertificate(certPath: String): SSLContext {
11+
fun buildTrustManagerFactoryForCertificate(certPath: String): TrustManagerFactory {
1212
val certFile = File(certPath)
1313
val certificate: Certificate = CertificateFactory.getInstance("X.509")
1414
.generateCertificate(FileInputStream(certFile))
@@ -19,9 +19,11 @@ fun buildSslContextForCertificate(certPath: String): SSLContext {
1919

2020
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
2121
trustManagerFactory.init(keyStore)
22+
return trustManagerFactory
23+
}
2224

25+
fun buildSslContextForCertificate(trustManagerFactory: TrustManagerFactory): SSLContext {
2326
val sslContext = SSLContext.getInstance("TLS")
2427
sslContext.init(null, trustManagerFactory.trustManagers, null)
25-
2628
return sslContext
2729
}

src/test/kotlin/IntegrationTests.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ class IntegrationTests : StringSpec({
7474
agentAttachProc.isAlive.shouldBe(false)
7575
agentAttachProc.exitValue().shouldBe(0)
7676

77-
// Target should pick up proxy details & quit happily too
78-
targetProc.waitFor(10, TimeUnit.SECONDS)
77+
// Target should pick up proxy details & quit happily, eventually
78+
targetProc.waitFor(15, TimeUnit.SECONDS)
7979
targetProc.isAlive.shouldBe(false)
8080
targetProc.exitValue().shouldBe(0)
8181
}

test-app/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ dependencies {
1616
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.9.1'
1717
implementation group: 'com.squareup.okhttp', name: 'okhttp', version: '2.7.5'
1818
implementation group: 'com.squareup.retrofit2', name: 'retrofit', version: '2.9.0'
19-
implementation group: 'org.eclipse.jetty', name: 'jetty-client', version: '11.0.1'
19+
implementation group: 'org.eclipse.jetty', name: 'jetty-client', version: '10.0.0'
20+
implementation group: 'org.asynchttpclient', name: 'async-http-client', version: '2.12.2'
2021
}
2122

2223
test {

0 commit comments

Comments
 (0)