@@ -2,33 +2,121 @@ package tech.httptoolkit.javaagent
22
33import net.bytebuddy.agent.builder.AgentBuilder
44import net.bytebuddy.asm.Advice
5+ import net.bytebuddy.description.field.FieldDescription
56import net.bytebuddy.description.method.MethodDescription
67import net.bytebuddy.description.type.TypeDescription
78import net.bytebuddy.dynamic.DynamicType
89import net.bytebuddy.matcher.ElementMatchers.*
9- import net.bytebuddy.utility.JavaModule
10- import reactor.netty.http.client.HttpClientConfig
1110import 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+
1724class 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+ }
0 commit comments