Skip to content

Commit 6f81725

Browse files
committed
Support Reactor-Netty v0.9 too (and thereby more Spring webclients)
Unfortunately we can't test this without doing something *much* more complicated, since the versions conflict, but it works nicely when tested manually against Spring PetClinic Microservices when using either v1 or v0.9.
1 parent 2df9ba4 commit 6f81725

File tree

9 files changed

+241
-10
lines changed

9 files changed

+241
-10
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ Traffic can be captured from at least:
1616
[x] Apache HttpAsyncClient v4 & v5
1717
- [x] OkHttp v2, v3 & v4
1818
- [x] Retrofit
19-
- [x] Jetty-Client v9, 10 & 11
19+
- [x] Jetty-Client v9, v10 & v11
2020
- [x] Async-Http-Client
21-
- [x] Reactor-Netty
21+
- [x] Reactor-Netty v0.9 & v1+
2222
- [x] Spring WebClient
2323

2424
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.

build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ jar {
2020
attributes 'Can-Redefine-Classes': 'true'
2121
attributes 'Can-Retransform-Classes': 'true'
2222
}
23+
24+
// We include classes in our package. We *don't* include stub classes used to support multiple
25+
// dependency versions, which are under their corresponding real package names.
26+
include('tech/httptoolkit/javaagent/**/*')
2327
}
2428

2529
dependencies {
@@ -63,6 +67,10 @@ shadowJar {
6367
exclude '**/*.kotlin_builtins'
6468
exclude '**/module_info.class'
6569
exclude 'META-INF/maven/**'
70+
71+
// We have to specifically exclude packages here, because we *do* want to include lots of non-local code, just not
72+
// these specific client stubs:
73+
exclude 'reactor/'
6674
}
6775

6876
import com.github.jengelman.gradle.plugins.shadow.tasks.ConfigureShadowRelocation
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package reactor.netty.tcp;
2+
3+
import java.net.InetSocketAddress;
4+
5+
/**
6+
* A stub with the parts of the interface we need to support v0.9 of Reactor-Netty. We compile against this, but we
7+
* *don't* include this in the resulting JAR, so references instead resolve to the real implementation, when that
8+
* is present. This is required because we can't depend on both v0.9 and v1 in the same module, and this class has
9+
* moved packages between the two.
10+
*/
11+
public final class ProxyProvider {
12+
13+
public static ProxyProvider.TypeSpec builder() {
14+
return new ProxyProvider.Build();
15+
}
16+
17+
ProxyProvider(ProxyProvider.Build builder) {}
18+
19+
public enum Proxy {
20+
HTTP
21+
}
22+
23+
static final class Build implements TypeSpec, AddressSpec, Builder {
24+
25+
Build() {}
26+
27+
public final Builder address(InetSocketAddress address) {
28+
return this;
29+
}
30+
31+
public final AddressSpec type(Proxy type) {
32+
return this;
33+
}
34+
35+
public ProxyProvider build() {
36+
return new ProxyProvider(this);
37+
}
38+
}
39+
40+
public interface TypeSpec {
41+
AddressSpec type(Proxy type);
42+
}
43+
44+
public interface AddressSpec {
45+
Builder address(InetSocketAddress address);
46+
}
47+
48+
public interface Builder {
49+
ProxyProvider build();
50+
}
51+
}
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 java.net.InetSocketAddress;
6+
import java.net.SocketAddress;
7+
8+
public class ReturnProxyAddressAdvice {
9+
@Advice.OnMethodExit
10+
public static void proxy(@Advice.Return(readOnly = false) SocketAddress returnValue) {
11+
returnValue = new InetSocketAddress(
12+
HttpProxyAgent.getAgentProxyHost(),
13+
HttpProxyAgent.getAgentProxyPort()
14+
);
15+
}
16+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package tech.httptoolkit.javaagent.reactornetty;
2+
3+
import io.netty.handler.ssl.SslContextBuilder;
4+
import net.bytebuddy.asm.Advice;
5+
import reactor.netty.tcp.SslProvider;
6+
import tech.httptoolkit.javaagent.HttpProxyAgent;
7+
8+
public class ReactorNettyResetHttpClientSecureSslAdvice {
9+
10+
public static final SslProvider agentSslProvider;
11+
12+
static {
13+
try {
14+
// Initialize our intercepted SSL provider:
15+
agentSslProvider = SslProvider.builder()
16+
.sslContext(
17+
SslContextBuilder
18+
.forClient()
19+
.trustManager(HttpProxyAgent.getInterceptedTrustManagerFactory())
20+
.build()
21+
).build();
22+
} catch (Exception e) {
23+
throw new RuntimeException(e);
24+
}
25+
}
26+
27+
// In v0.9 versions of Reactor Netty, the sslProvider is stored on HttpClientSecure. Here we hook that class's
28+
// constructor and replace the SSL provider as soon as it's set.
29+
30+
@Advice.OnMethodExit
31+
public static void afterConstructor(
32+
@Advice.FieldValue(value = "sslProvider", readOnly = false) SslProvider sslProviderField
33+
) {
34+
sslProviderField = agentSslProvider;
35+
}
36+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package tech.httptoolkit.javaagent.reactornetty;
2+
3+
import net.bytebuddy.asm.Advice;
4+
import reactor.netty.tcp.ProxyProvider;
5+
import tech.httptoolkit.javaagent.HttpProxyAgent;
6+
7+
import java.net.InetSocketAddress;
8+
9+
10+
// Reset the proxyProvider field to use our own intercepted proxy after certain constructors
11+
// complete. Note that this uses the v0.9 proxyProvider class, so it would fail if applied to
12+
// when v1 is loaded in the target app.
13+
public class ReactorNettyV09ResetProxyProviderFieldAdvice {
14+
15+
public static final ProxyProvider agentProxyProvider = ProxyProvider.builder()
16+
.type(ProxyProvider.Proxy.HTTP)
17+
.address(new InetSocketAddress(
18+
HttpProxyAgent.getAgentProxyHost(),
19+
HttpProxyAgent.getAgentProxyPort()
20+
))
21+
.build();
22+
23+
@Advice.OnMethodExit
24+
public static void afterConstructor(
25+
@Advice.FieldValue(value = "proxyProvider", readOnly = false) ProxyProvider proxyProviderField
26+
) {
27+
proxyProviderField = agentProxyProvider;
28+
}
29+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ fun interceptAllHttps(config: Config, instrumentation: Instrumentation) {
8989
JettyClientTransformer(logger),
9090
AsyncHttpClientConfigTransformer(logger),
9191
AsyncHttpChannelManagerTransformer(logger),
92-
ReactorNettyClientConfigTransformer(logger)
92+
ReactorNettyClientConfigTransformer(logger),
93+
ReactorNettyProxyProviderTransformer(logger),
94+
ReactorNettyOverrideRequestAddressTransformer(logger),
95+
ReactorNettyHttpClientSecureTransformer(logger)
9396
).forEach { matchingAgentTransformer ->
9497
agentBuilder = matchingAgentTransformer.register(agentBuilder)
9598
}

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

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,121 @@ package tech.httptoolkit.javaagent
22

33
import net.bytebuddy.agent.builder.AgentBuilder
44
import net.bytebuddy.asm.Advice
5+
import net.bytebuddy.description.field.FieldDescription
56
import net.bytebuddy.description.method.MethodDescription
67
import net.bytebuddy.description.type.TypeDescription
78
import net.bytebuddy.dynamic.DynamicType
89
import net.bytebuddy.matcher.ElementMatchers.*
9-
import net.bytebuddy.utility.JavaModule
10-
import reactor.netty.http.client.HttpClientConfig
1110
import tech.httptoolkit.javaagent.reactornetty.ReactorNettyResetAllConfigAdvice
11+
import tech.httptoolkit.javaagent.reactornetty.ReactorNettyResetHttpClientSecureSslAdvice
12+
import tech.httptoolkit.javaagent.reactornetty.ReactorNettyV09ResetProxyProviderFieldAdvice
1213

13-
// To patch Reactor-Netty's HTTP client, we hook the constructor of the client itself. It has a constructor
14+
// To patch Reactor-Netty's v1 HTTP client, we hook the constructor of the client itself. It has a constructor
1415
// that receives the config as part of every single HTTP request - we hook that to reset the relevant
1516
// config props every time they're used.
1617

18+
private val matchConfigConstructor = isConstructor<MethodDescription>()
19+
.and(takesArguments(1))
20+
.and(takesArgument(0,
21+
named("reactor.netty.http.client.HttpClientConfig")
22+
))
23+
1724
class ReactorNettyClientConfigTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) {
25+
1826
override fun register(builder: AgentBuilder): AgentBuilder {
1927
return builder
2028
.type(
2129
hasSuperType(named("reactor.netty.http.client.HttpClient"))
2230
).and(
2331
not(isInterface())
32+
).and(
33+
// This matches v1+ only, where the config is passed into the constructor repeatedly, and can
34+
// be mutated there. v0.9 is handled separately below.
35+
declaresMethod(matchConfigConstructor)
2436
).transform(this)
2537
}
2638

2739
override fun transform(builder: DynamicType.Builder<*>): DynamicType.Builder<*> {
2840
return builder
2941
.visit(Advice.to(ReactorNettyResetAllConfigAdvice::class.java)
30-
.on(isConstructor<MethodDescription>().and(
31-
takesArguments(HttpClientConfig::class.java)
32-
)))
42+
.on(matchConfigConstructor)
43+
)
44+
}
45+
}
46+
47+
// In v0.9, that wasn't the case. Instead, the SSL provider and proxy provider are passed as arguments to
48+
// and stored within various client classes. Here, we patch all their constructors to reset those fields
49+
// immediately after instantiation, ensuring our values replace the given arguments.
50+
51+
// First, the sslProvider field:
52+
class ReactorNettyHttpClientSecureTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) {
53+
override fun register(builder: AgentBuilder): AgentBuilder {
54+
return builder
55+
.type(
56+
named("reactor.netty.http.client.HttpClientSecure")
57+
).and(
58+
declaresField(named("sslProvider"))
59+
).transform(this)
60+
}
61+
62+
override fun transform(builder: DynamicType.Builder<*>): DynamicType.Builder<*> {
63+
return builder
64+
.visit(Advice.to(ReactorNettyResetHttpClientSecureSslAdvice::class.java)
65+
.on(isConstructor()))
3366
}
3467
}
68+
69+
// Then each of the important cases where a proxy provider is stored:
70+
class ReactorNettyProxyProviderTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) {
71+
override fun register(builder: AgentBuilder): AgentBuilder {
72+
return builder
73+
.type(
74+
declaresField(named<FieldDescription>("proxyProvider").and(
75+
// This only applies to v0.9+ which uses this package name, not v1+ where ProxyProvider
76+
// lives in reactor.netty.transport (handled by the other transformer above)
77+
fieldType(named("reactor.netty.tcp.ProxyProvider")))
78+
)
79+
).and(
80+
named<TypeDescription>(
81+
"reactor.netty.http.client.HttpClientConnect\$MonoHttpConnect"
82+
).or(
83+
named(
84+
"reactor.netty.http.client.HttpClientConnect\$HttpClientHandler"
85+
)
86+
)
87+
).transform(this)
88+
}
89+
90+
override fun transform(builder: DynamicType.Builder<*>): DynamicType.Builder<*> {
91+
return builder
92+
.visit(Advice.to(ReactorNettyV09ResetProxyProviderFieldAdvice::class.java)
93+
.on(isConstructor()))
94+
}
95+
}
96+
97+
// Then, on top of all that, we also forcibly set the socket address for all outgoing HTTP connections, because that's
98+
// that's the goal, and the above proxyProvider logic doesn't properly cover everything as proxy logic is spread across
99+
// a few places including the generic TCP clients (which we shouldn't touch). This is a bit messy/risky, but only
100+
// applies to v0.9, since v1+ stores the config in a properly structured way.
101+
class ReactorNettyOverrideRequestAddressTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) {
102+
override fun register(builder: AgentBuilder): AgentBuilder {
103+
return builder
104+
.type(
105+
declaresField(named<FieldDescription>("proxyProvider").and(
106+
// This ensures this only applies to v0.9+ which uses this package name, not v1+ where
107+
// ProxyProvider lives in reactor.netty.transport (handled separately above).
108+
fieldType(named("reactor.netty.tcp.ProxyProvider")))
109+
)
110+
).and(
111+
named(
112+
"reactor.netty.http.client.HttpClientConnect\$HttpClientHandler"
113+
)
114+
).transform(this)
115+
}
116+
117+
override fun transform(builder: DynamicType.Builder<*>): DynamicType.Builder<*> {
118+
return builder
119+
.visit(Advice.to(ReturnProxyAddressAdvice::class.java)
120+
.on(hasMethodName("get")))
121+
}
122+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import net.bytebuddy.description.type.TypeDescription
55
import net.bytebuddy.dynamic.DynamicType
66
import net.bytebuddy.utility.JavaModule
77

8-
class TransformationLogger(val debugMode: Boolean) : AgentBuilder.Listener.Adapter() {
8+
class TransformationLogger(private val debugMode: Boolean) : AgentBuilder.Listener.Adapter() {
99

1010
private val transformingTypes: ArrayList<String> = ArrayList()
1111

0 commit comments

Comments
 (0)