Skip to content

Commit 2fe34f5

Browse files
committed
Add support for Apache Commons HttpClient v3
Super old now, but it's a classic, and still used in some notable places including Intellij itself (but only for plugins AFAICT).
1 parent 7ce374a commit 2fe34f5

File tree

11 files changed

+212
-5
lines changed

11 files changed

+212
-5
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ Traffic can be captured from at least:
1212

1313
- [x] Java's built-in HttpURLConnection
1414
- [x] Java 11's built-in HttpClient
15-
- [x] Apache HttpClient v4 & v5
16-
[x] Apache HttpAsyncClient v4 & v5
15+
- [x] Apache HttpClient v3, v4 & v5
16+
- [x] Apache HttpAsyncClient v4 & v5
1717
- [x] OkHttp v2, v3 & v4
1818
- [x] Retrofit
1919
- [x] Jetty-Client v9, v10 & v11

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ dependencies {
3232
implementation group: 'net.bytebuddy', name: 'byte-buddy-dep', version: '1.10.20'
3333

3434
// Dependencies we load only as part of rewriting them, iff the target app includes them:
35+
compileOnly group: 'commons-httpclient', name: 'commons-httpclient', version: '3.1'
3536
compileOnly group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5'
3637
compileOnly group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0.3'
3738
compileOnly group: 'org.eclipse.jetty', name: 'jetty-client', version: '11.0.1'
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package tech.httptoolkit.javaagent.advice.apacheclient;
2+
3+
import org.apache.commons.httpclient.params.HttpConnectionParams;
4+
import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
5+
import tech.httptoolkit.javaagent.HttpProxyAgent;
6+
7+
import javax.net.SocketFactory;
8+
import javax.net.ssl.SSLSocketFactory;
9+
import java.io.IOException;
10+
import java.net.*;
11+
12+
public class ApacheCustomSslProtocolSocketFactory implements SecureProtocolSocketFactory {
13+
14+
private final SSLSocketFactory interceptedSocketFactory = HttpProxyAgent
15+
.getInterceptedSslContext()
16+
.getSocketFactory();
17+
18+
@Override
19+
public Socket createSocket(String host, int port) throws IOException {
20+
return interceptedSocketFactory.createSocket(host, port);
21+
}
22+
23+
@Override
24+
public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) throws IOException {
25+
return interceptedSocketFactory.createSocket(host, port, localAddress, localPort);
26+
}
27+
28+
@Override
29+
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
30+
return interceptedSocketFactory.createSocket(socket, host, port, autoClose);
31+
}
32+
33+
@Override
34+
public Socket createSocket(String host, int port, InetAddress localAddress, int localPort, HttpConnectionParams params) throws IOException {
35+
// Marginally more complicated logic here unfortunately, since timeout isn't natively
36+
// supported. Minimal implementation taken from the existing lib implementations:
37+
if (params == null) {
38+
throw new IllegalArgumentException("Parameters may not be null");
39+
}
40+
int timeout = params.getConnectionTimeout();
41+
Socket socket;
42+
43+
SocketFactory socketfactory = SSLSocketFactory.getDefault();
44+
if (timeout == 0) {
45+
socket = socketfactory.createSocket(host, port, localAddress, localPort);
46+
} else {
47+
socket = socketfactory.createSocket();
48+
SocketAddress localAddr = new InetSocketAddress(localAddress, localPort);
49+
SocketAddress remoteAddr = new InetSocketAddress(host, port);
50+
socket.bind(localAddr);
51+
socket.connect(remoteAddr, timeout);
52+
}
53+
54+
return socket;
55+
}
56+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package tech.httptoolkit.javaagent.advice.apacheclient;
2+
3+
import net.bytebuddy.asm.Advice;
4+
import org.apache.commons.httpclient.ProxyHost;
5+
import tech.httptoolkit.javaagent.HttpProxyAgent;
6+
7+
public class ApacheOverrideProxyHostFieldAdvice {
8+
9+
@Advice.OnMethodExit
10+
public static void resetProxyHost(
11+
@Advice.FieldValue(value = "proxyHost", readOnly = false) ProxyHost proxyHostField
12+
) {
13+
// After creating/changing HostConfiguration we override the proxy field:
14+
proxyHostField = new ProxyHost(
15+
HttpProxyAgent.getAgentProxyHost(),
16+
HttpProxyAgent.getAgentProxyPort()
17+
);
18+
}
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package tech.httptoolkit.javaagent.advice.apacheclient;
2+
3+
import net.bytebuddy.asm.Advice;
4+
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
5+
6+
public class ApacheReturnCustomSslProtocolSocketFactoryAdvice {
7+
8+
@Advice.OnMethodExit
9+
public static void getSocketFactory(
10+
@Advice.FieldValue(value = "secure") boolean isSecure,
11+
@Advice.Return(readOnly = false) ProtocolSocketFactory returnValue
12+
) {
13+
if (isSecure) {
14+
returnValue = new ApacheCustomSslProtocolSocketFactory();
15+
}
16+
}
17+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package tech.httptoolkit.javaagent.advice.apacheclient;
2+
3+
import net.bytebuddy.asm.Advice;
4+
import org.apache.commons.httpclient.HostConfiguration;
5+
6+
public class ApacheSetConfigProxyHostAdvice {
7+
8+
@Advice.OnMethodEnter
9+
public static void beforeMakingRequests(
10+
@Advice.FieldValue(value = "hostConfiguration") HostConfiguration hostConfiguration
11+
) {
12+
// Elsewhere, we hook setProxyHost to reset the proxy to our configured version whenever it's called.
13+
// Then, here we hook various methods to call it before they use the config:
14+
hostConfiguration.setProxyHost(null); // null here is ignored as this method is already hooked
15+
}
16+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import java.lang.instrument.Instrumentation
1313
import javax.net.ssl.SSLContext
1414
import java.net.*
1515
import javax.net.ssl.HttpsURLConnection
16+
import javax.net.ssl.SSLSocketFactory
1617
import javax.net.ssl.TrustManagerFactory
1718

1819

@@ -85,6 +86,9 @@ fun interceptAllHttps(config: Config, instrumentation: Instrumentation) {
8586
ApacheClientRoutingV5Transformer(logger),
8687
ApacheSslSocketFactoryTransformer(logger),
8788
ApacheClientTlsStrategyTransformer(logger),
89+
ApacheHostConfigurationTransformer(logger),
90+
ApacheHttpMethodDirectorTransformer(logger),
91+
ApacheProtocolTransformer(logger),
8892
JavaClientTransformer(logger),
8993
UrlConnectionTransformer(logger),
9094
HttpsUrlConnectionTransformer(logger),

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

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ package tech.httptoolkit.javaagent
22

33
import net.bytebuddy.agent.builder.AgentBuilder
44
import net.bytebuddy.asm.Advice
5+
import net.bytebuddy.description.method.MethodDescription
56
import net.bytebuddy.dynamic.DynamicType
67
import net.bytebuddy.matcher.ElementMatchers.*
7-
import tech.httptoolkit.javaagent.advice.apacheclient.ApacheSetSslSocketFactoryAdvice
8-
import tech.httptoolkit.javaagent.advice.apacheclient.ApacheV4ReturnProxyRouteAdvice
9-
import tech.httptoolkit.javaagent.advice.apacheclient.ApacheV5ReturnProxyRouteAdvice
8+
import tech.httptoolkit.javaagent.advice.apacheclient.*
109

1110
// For both v4 & v5 we override all implementations of the RoutePlanner interface, and we redefine all routes
1211
// to go via our proxy instead of their existing configuration.
@@ -68,3 +67,71 @@ class ApacheSslSocketFactoryTransformer(logger: TransformationLogger) : Matching
6867
);
6968
}
7069
}
70+
71+
// Meanwhile, for V3 we need to do something totally different: we patch HostConfiguration to apply a proxy to
72+
// all new configurations (and ignore changes), we patch HttpMethodDirector to update existing configurations
73+
// as they're used, and we patch Protocol to change the SslSocketFactory on all secure protocols.
74+
75+
class ApacheHostConfigurationTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) {
76+
override fun register(builder: AgentBuilder): AgentBuilder {
77+
return builder
78+
.type(
79+
named("org.apache.commons.httpclient.HostConfiguration")
80+
).transform(this)
81+
}
82+
83+
override fun transform(builder: DynamicType.Builder<*>): DynamicType.Builder<*> {
84+
return builder
85+
// Override the proxy field value for all new configurations, and for any attempts to call
86+
// setProxy/ProxyHost. We don't no-op these, because we want to call them ourselves later on
87+
// existing configs to reset them - we don't just want to ignore this.
88+
.visit(
89+
Advice.to(ApacheOverrideProxyHostFieldAdvice::class.java)
90+
.on(isConstructor<MethodDescription>()
91+
.or(hasMethodName("setProxy"))
92+
.or(hasMethodName("setProxyHost"))
93+
)
94+
)
95+
}
96+
}
97+
98+
// Whenever an HttpMethodDirector is used, we reset the proxy in the passed configuration. This uses the above
99+
// hooks, which ensure that setProxyHost(anything) automatically loads & sets our intercepted proxy.
100+
// We *don't* want to reset all proxy hosts in all existing configurations, because that's a) quite tricky and
101+
// b) some are used as keys in existing direct connections in pools, and we don't want to match those later.
102+
class ApacheHttpMethodDirectorTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) {
103+
override fun register(builder: AgentBuilder): AgentBuilder {
104+
return builder
105+
.type(
106+
named("org.apache.commons.httpclient.HttpMethodDirector")
107+
).transform(this)
108+
}
109+
110+
override fun transform(builder: DynamicType.Builder<*>): DynamicType.Builder<*> {
111+
return builder
112+
.visit(
113+
Advice.to(ApacheSetConfigProxyHostAdvice::class.java)
114+
.on(hasMethodName("executeMethod"))
115+
)
116+
}
117+
}
118+
119+
// Every v3 configuration has a protocol, and each one can build sockets in its own unique way. Here, we patch
120+
// all of them so that all _secure_ protocols trust only our certificate, and nothing else. This would
121+
// be an issue for a generic TCP client, but for HTTPS we know we should be the only authority present.
122+
class ApacheProtocolTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) {
123+
override fun register(builder: AgentBuilder): AgentBuilder {
124+
return builder
125+
.type(
126+
named("org.apache.commons.httpclient.protocol.Protocol")
127+
).transform(this)
128+
}
129+
130+
override fun transform(builder: DynamicType.Builder<*>): DynamicType.Builder<*> {
131+
return builder
132+
.visit(
133+
Advice.to(ApacheReturnCustomSslProtocolSocketFactoryAdvice::class.java)
134+
.on(hasMethodName("getSocketFactory"))
135+
)
136+
}
137+
}

test-app/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ repositories {
1111
}
1212

1313
dependencies {
14+
implementation group: 'commons-httpclient', name: 'commons-httpclient', version: '3.1'
1415
implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5'
1516
implementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0.3'
1617
implementation group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.1.4'
18+
1719
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.9.1'
1820
implementation group: 'com.squareup.okhttp', name: 'okhttp', version: '2.7.5'
1921
implementation group: 'com.squareup.retrofit2', name: 'retrofit', version: '2.9.0'
22+
2023
implementation group: 'org.eclipse.jetty', name: 'jetty-client', version: '10.0.0'
2124
implementation group: 'org.asynchttpclient', name: 'async-http-client', version: '2.12.2'
2225
implementation group: 'org.springframework', name: 'spring-webflux', version: '5.3.4'

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
public class Main {
1616

1717
private static final Map<String, ClientCase<?>> cases = Map.ofEntries(
18+
entry("apache-v3", new ApacheHttpClientV3Case()),
1819
entry("apache-v4", new ApacheHttpClientV4Case()),
1920
entry("apache-v5", new ApacheHttpClientV5Case()),
2021
entry("apache-async-v4", new ApacheHttpAsyncClientV4Case()),

0 commit comments

Comments
 (0)