Skip to content

Commit 1997f0a

Browse files
committed
Add support for jetty-client
1 parent f60d2f2 commit 1997f0a

File tree

12 files changed

+175
-8
lines changed

12 files changed

+175
-8
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Traffic can be captured from at least:
1818
- [x] OkHttp v3
1919
- [x] OkHttp v4
2020
- [x] Retrofit
21+
- [x] Jetty-Client
2122

2223
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.
2324

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies {
2828
// Dependencies we load only as part of rewriting them, iff the target app includes them:
2929
compileOnly group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5'
3030
compileOnly group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0.3'
31+
compileOnly group: 'org.eclipse.jetty', name: 'jetty-client', version: '11.0.1'
3132

3233
// Test deps:
3334
testImplementation group: 'io.kotest', name: 'kotest-runner-junit5-jvm', version: '4.4.0'
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package tech.httptoolkit.javaagent;
2+
3+
import net.bytebuddy.asm.Advice;
4+
import org.eclipse.jetty.client.*;
5+
6+
import java.util.Map;
7+
8+
public class JettyResetDestinationsAdvice {
9+
@Advice.OnMethodEnter
10+
public static void beforeResolveDestination(
11+
@Advice.This Object thisHttpClient
12+
// ^ Note that we can't use the real HttpClient type here, since this class is redefining it, so it would
13+
// cause a circular reference that breaks patching completely.
14+
) {
15+
boolean alreadyReset = JettyClientTransformer.getPatchedHttpClients().contains(thisHttpClient);
16+
if (!alreadyReset) {
17+
// If this is the first time that we've seen this client, it's possible that it existed before we attached,
18+
// and it might have some existing open connections that don't use our proxy. To fix that, just once per
19+
// client, we use reflection to get the destinations (cached connections) and reset them.
20+
try {
21+
@SuppressWarnings("unchecked")
22+
Map<Origin, HttpDestination> destinations = (Map<Origin, HttpDestination>)
23+
thisHttpClient.getClass().getDeclaredField("destinations").get(thisHttpClient);
24+
25+
// Reset this destinations list:
26+
for (HttpDestination destination : destinations.values()) {
27+
destination.close();
28+
}
29+
destinations.clear();
30+
} catch (Exception e) {
31+
throw new RuntimeException(e);
32+
}
33+
JettyClientTransformer.getPatchedHttpClients().add(thisHttpClient);
34+
}
35+
}
36+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package tech.httptoolkit.javaagent;
2+
3+
import net.bytebuddy.asm.Advice;
4+
import org.eclipse.jetty.client.HttpProxy;
5+
import org.eclipse.jetty.client.Origin;
6+
import org.eclipse.jetty.client.ProxyConfiguration;
7+
8+
public class JettyReturnProxyConfigurationAdvice {
9+
@Advice.OnMethodExit
10+
public static void getProxyConfiguration(@Advice.Return(readOnly = false) ProxyConfiguration returnValue) {
11+
Origin.Address proxyAddress = new Origin.Address(HttpProxyAgent.getAgentProxyHost(), HttpProxyAgent.getAgentProxyPort());
12+
13+
ProxyConfiguration proxyConfig = new ProxyConfiguration();
14+
proxyConfig.getProxies().add(new HttpProxy(proxyAddress, false));
15+
16+
returnValue = proxyConfig;
17+
}
18+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package tech.httptoolkit.javaagent;
2+
3+
import net.bytebuddy.asm.Advice;
4+
import org.eclipse.jetty.util.ssl.SslContextFactory;
5+
6+
public class JettyReturnSslContextFactoryAdvice {
7+
@Advice.OnMethodExit
8+
public static void getSslContextFactory(@Advice.Return(readOnly = false) SslContextFactory.Client returnValue) {
9+
SslContextFactory.Client sslFactory = new SslContextFactory.Client();
10+
sslFactory.setSslContext(HttpProxyAgent.getInterceptedSslContext());
11+
try {
12+
sslFactory.start();
13+
} catch (Exception e) {
14+
throw new RuntimeException(e);
15+
}
16+
17+
returnValue = sslFactory;
18+
}
19+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ fun interceptAllHttps(config: Config, instrumentation: Instrumentation) {
7474
ApacheClientRoutingV5Transformer(),
7575
ApacheSslSocketFactoryTransformer(),
7676
JavaClientTransformer(),
77+
JettyClientTransformer()
7778
).forEach { matchingAgentTransformer ->
7879
agentBuilder = matchingAgentTransformer.register(agentBuilder)
7980
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 java.util.*
10+
11+
/**
12+
* Transforms the JettyClient to use our proxy & trust our certificate.
13+
*
14+
* For new clients, we just need to override the proxyConfiguration and
15+
* sslContextFactory properties on the HTTP client itself.
16+
*
17+
* For existing clients, we do that, and we also reset the destinations
18+
* (internal connection pools) when resolveDestination is first called
19+
* on each client.
20+
*/
21+
class JettyClientTransformer : MatchingAgentTransformer {
22+
23+
companion object {
24+
// This is used to cache the clients that have been fully configured (had any existing destinations
25+
// reset) so that we don't unnecessarily reset them again later.
26+
@JvmStatic
27+
public val patchedHttpClients: MutableSet<Any> = Collections.newSetFromMap(WeakHashMap())
28+
}
29+
30+
override fun register(builder: AgentBuilder): AgentBuilder {
31+
return builder
32+
.type(
33+
named("org.eclipse.jetty.client.HttpClient")
34+
).transform(this)
35+
}
36+
37+
override fun transform(
38+
builder: DynamicType.Builder<*>,
39+
typeDescription: TypeDescription,
40+
classLoader: ClassLoader?,
41+
module: JavaModule?
42+
): DynamicType.Builder<*>? {
43+
return builder
44+
.visit(Advice.to(JettyReturnProxyConfigurationAdvice::class.java)
45+
.on(hasMethodName("getProxyConfiguration")))
46+
.visit(Advice.to(JettyReturnSslContextFactoryAdvice::class.java)
47+
.on(hasMethodName("getSslContextFactory")))
48+
.visit(Advice.to(JettyResetDestinationsAdvice::class.java)
49+
.on(hasMethodName("resolveDestination")))
50+
}
51+
}

src/test/kotlin/IntegrationTests.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@file:Suppress("BlockingMethodInNonBlockingContext")
2+
13
import com.github.tomakehurst.wiremock.WireMockServer
24
import com.github.tomakehurst.wiremock.client.WireMock.*
35
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.options

test-app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ 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'
1920
}
2021

2122
test {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public class Main {
1717
"java-http-client", new JavaHttpClientCase(),
1818
"okhttp-v2", new OkHttpV2Case(),
1919
"okhttp-v4", new OkHttpV4Case(),
20-
"retrofit", new RetrofitCase()
20+
"retrofit", new RetrofitCase(),
21+
"jetty-client", new JettyClientCase()
2122
);
2223

2324
public static void main(String[] args) throws Exception {

0 commit comments

Comments
 (0)