Skip to content

Commit 466831b

Browse files
committed
Add support for Apache HttpAsyncClient
1 parent 4a531c5 commit 466831b

File tree

8 files changed

+153
-11
lines changed

8 files changed

+153
-11
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Traffic can be captured from at least:
1313
- [x] Java's built-in HttpURLConnection
1414
- [x] Java 11's built-in HttpClient
1515
- [x] Apache HttpClient v4 & v5
16+
[x] Apache HttpAsyncClient v4 & v5
1617
- [x] OkHttp v2, v3 & v4
1718
- [x] Retrofit
1819
- [x] Jetty-Client v9, 10 & 11
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package tech.httptoolkit.javaagent;
2+
3+
import net.bytebuddy.asm.Advice;
4+
5+
import javax.net.ssl.SSLContext;
6+
7+
public class OverrideSslContextFieldAdvice {
8+
9+
@Advice.OnMethodEnter
10+
public static void beforeMethod(
11+
@Advice.FieldValue(value = "sslContext", readOnly = false) SSLContext sslContextField
12+
) {
13+
sslContextField = HttpProxyAgent.getInterceptedSslContext();
14+
}
15+
16+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ fun interceptAllHttps(config: Config, instrumentation: Instrumentation) {
7878
ApacheClientRoutingV4Transformer(),
7979
ApacheClientRoutingV5Transformer(),
8080
ApacheSslSocketFactoryTransformer(),
81+
ApacheClientTlsStrategyTransformer(),
8182
JavaClientTransformer(),
8283
JettyClientTransformer(),
8384
AsyncHttpClientConfigTransformer(),
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package tech.httptoolkit.javaagent
2+
3+
import net.bytebuddy.agent.builder.AgentBuilder
4+
import net.bytebuddy.asm.Advice
5+
import net.bytebuddy.description.method.MethodDescription
6+
import net.bytebuddy.description.type.TypeDescription
7+
import net.bytebuddy.dynamic.DynamicType
8+
import net.bytebuddy.matcher.ElementMatchers.*
9+
import net.bytebuddy.utility.JavaModule
10+
import tech.httptoolkit.javaagent.apacheclient.ApacheSetSslSocketFactoryAdvice
11+
import tech.httptoolkit.javaagent.apacheclient.ApacheV4ReturnProxyRouteAdvice
12+
import tech.httptoolkit.javaagent.apacheclient.ApacheV5ReturnProxyRouteAdvice
13+
14+
// Apache async client hooks depend on the non-async Apache client transformers, which successfully transform proxy
15+
// configuration, but we need to separate re-transform TLS configuration too.
16+
17+
// To do so, we get all instances of TlsStrategy/SSLIOSessionStrategy, all of which seem to have an sslContext private
18+
// field which they wrap around the connection their implementation upgrade(). Most of the real ones inherit from
19+
// AbstractClientTlsStrategy, which does this, but there's other examples too. We hook upgrade(), so that that
20+
// field is reset to use our context before any client TLS upgrade happens.
21+
22+
class ApacheClientTlsStrategyTransformer : MatchingAgentTransformer {
23+
override fun register(builder: AgentBuilder): AgentBuilder {
24+
return builder
25+
// For v5 we need to hook into every client TlsStrategy (of which there are many).
26+
.type(
27+
hasSuperType(named("org.apache.hc.core5.http.nio.ssl.TlsStrategy"))
28+
).and(
29+
// There are both Server & Client strategies with the same interface, and checking the name is the only
30+
// way to tell the difference:
31+
nameContains("Client")
32+
).and(
33+
// All strategies either do this, or extend a class that does this (which will be intercepted
34+
// first by itself anyway)
35+
declaresField(named("sslContext"))
36+
).and(
37+
not(isInterface())
38+
).transform(this)
39+
// For v4, we do exactly the same, but there's only a single implementation:
40+
.type(
41+
named("org.apache.http.nio.conn.ssl.SSLIOSessionStrategy")
42+
).transform(this)
43+
}
44+
45+
override fun transform(
46+
builder: DynamicType.Builder<*>,
47+
typeDescription: TypeDescription,
48+
classLoader: ClassLoader?,
49+
module: JavaModule?
50+
): DynamicType.Builder<*>? {
51+
return builder
52+
.visit(
53+
Advice.to(OverrideSslContextFieldAdvice::class.java)
54+
.on(hasMethodName("upgrade"))
55+
);
56+
}
57+
}

test-app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ repositories {
1313
dependencies {
1414
implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5'
1515
implementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0.3'
16+
implementation group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.1.4'
1617
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.9.1'
1718
implementation group: 'com.squareup.okhttp', name: 'okhttp', version: '2.7.5'
1819
implementation group: 'com.squareup.retrofit2', name: 'retrofit', version: '2.9.0'

test-app/src/main/java/tech/httptoolkit/testapp/Main.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,23 @@
1010
import java.util.stream.Collectors;
1111

1212
import static java.lang.Thread.sleep;
13+
import static java.util.Map.entry;
1314

1415
public class Main {
1516

16-
private static final Map<String, ClientCase<?>> cases = Map.of(
17-
"apache-v4", new ApacheHttpClientV4Case(),
18-
"apache-v5", new ApacheHttpClientV5Case(),
19-
"http-url-conn", new HttpUrlConnCase(),
20-
"java-http-client", new JavaHttpClientCase(),
21-
"okhttp-v2", new OkHttpV2Case(),
22-
"okhttp-v4", new OkHttpV4Case(),
23-
"retrofit", new RetrofitCase(),
24-
"jetty-client", new JettyClientCase(),
25-
"async-http-client", new AsyncHttpClientCase(),
26-
"spring-web", new SpringWebClientCase()
17+
private static final Map<String, ClientCase<?>> cases = Map.ofEntries(
18+
entry("apache-v4", new ApacheHttpClientV4Case()),
19+
entry("apache-v5", new ApacheHttpClientV5Case()),
20+
entry("apache-async-v4", new ApacheHttpAsyncClientV4Case()),
21+
entry("apache-async-v5", new ApacheHttpAsyncClientV5Case()),
22+
entry("http-url-conn", new HttpUrlConnCase()),
23+
entry("java-http-client", new JavaHttpClientCase()),
24+
entry("okhttp-v2", new OkHttpV2Case()),
25+
entry("okhttp-v4", new OkHttpV4Case()),
26+
entry("retrofit", new RetrofitCase()),
27+
entry("jetty-client", new JettyClientCase()),
28+
entry("async-http-client", new AsyncHttpClientCase()),
29+
entry("spring-web", new SpringWebClientCase())
2730
);
2831

2932
public static void main(String[] args) throws Exception {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package tech.httptoolkit.testapp.cases;
2+
3+
import org.apache.http.HttpResponse;
4+
import org.apache.http.client.methods.HttpGet;
5+
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
6+
import org.apache.http.impl.nio.client.HttpAsyncClients;
7+
8+
import java.util.concurrent.Future;
9+
10+
public class ApacheHttpAsyncClientV4Case extends ClientCase<CloseableHttpAsyncClient> {
11+
12+
@Override
13+
public CloseableHttpAsyncClient newClient(String url) {
14+
CloseableHttpAsyncClient client = HttpAsyncClients.createDefault();
15+
client.start();
16+
return client;
17+
}
18+
19+
@Override
20+
public void stopClient(CloseableHttpAsyncClient client) throws Exception {
21+
client.close();
22+
}
23+
24+
@Override
25+
public int test(String url, CloseableHttpAsyncClient client) throws Exception {
26+
HttpGet request = new HttpGet(url);
27+
Future<HttpResponse> future = client.execute(request, null);
28+
HttpResponse response = future.get();
29+
return response.getStatusLine().getStatusCode();
30+
}
31+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package tech.httptoolkit.testapp.cases;
2+
3+
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
4+
import org.apache.hc.client5.http.async.methods.SimpleHttpRequests;
5+
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
6+
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
7+
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
8+
9+
import java.util.concurrent.Future;
10+
11+
public class ApacheHttpAsyncClientV5Case extends ClientCase<CloseableHttpAsyncClient> {
12+
13+
@Override
14+
public CloseableHttpAsyncClient newClient(String url) {
15+
CloseableHttpAsyncClient client = HttpAsyncClients.createDefault();
16+
client.start();
17+
return client;
18+
}
19+
20+
@Override
21+
public void stopClient(CloseableHttpAsyncClient client) throws Exception {
22+
client.close();
23+
}
24+
25+
@Override
26+
public int test(String url, CloseableHttpAsyncClient client) throws Exception {
27+
SimpleHttpRequest request = SimpleHttpRequests.get(url);
28+
Future<SimpleHttpResponse> future = client.execute(request, null);
29+
SimpleHttpResponse response = future.get();
30+
return response.getCode();
31+
}
32+
}

0 commit comments

Comments
 (0)