Skip to content

Commit 26d45c9

Browse files
committed
More forcefully override built-in HttpUrlConnections
Attempting to intercept Intellij shows that applications which manage their own proxy config aren't always captured. This helps to lock down some of those gaps, ensuring HTTP(S) traffic is _always_ sent to our proxy.
1 parent bc18adb commit 26d45c9

File tree

10 files changed

+207
-3
lines changed

10 files changed

+207
-3
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package tech.httptoolkit.javaagent.advice;
2+
3+
import net.bytebuddy.asm.Advice;
4+
5+
import java.net.InetSocketAddress;
6+
import java.net.Proxy;
7+
import java.net.URI;
8+
import java.util.Collections;
9+
import java.util.List;
10+
11+
public class OverrideAllProxySelectionAdvice {
12+
13+
@Advice.OnMethodExit
14+
public static void selectProxy(
15+
@Advice.Argument(value = 0) URI uri,
16+
@Advice.Return(readOnly = false) List<Proxy> returnedProxies
17+
) {
18+
String scheme = uri.getScheme();
19+
20+
// For HTTP URIs only, we override all proxy selection globally to select our proxy instead:
21+
if (scheme.equals("http") || scheme.equals("https")) {
22+
returnedProxies = Collections.singletonList(
23+
new Proxy(Proxy.Type.HTTP, new InetSocketAddress(
24+
// We read from our custom variables, since we can't access HttpProxyAgent from a bootstrapped
25+
// class, and we use namespaced properties to make this extra reliable:
26+
System.getProperty("tech.httptoolkit.proxyHost"),
27+
Integer.parseInt(System.getProperty("tech.httptoolkit.proxyPort"))
28+
))
29+
);
30+
}
31+
}
32+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package tech.httptoolkit.javaagent.advice;
2+
3+
import net.bytebuddy.asm.Advice;
4+
5+
import java.net.Proxy;
6+
import java.net.ProxySelector;
7+
import java.net.URI;
8+
9+
public class OverrideUrlConnectionProxyAdvice {
10+
11+
@Advice.OnMethodEnter
12+
public static void openConnection(
13+
@Advice.FieldValue(value = "protocol") String urlProtocol,
14+
@Advice.Argument(value = 0, readOnly = false) Proxy proxyArgument
15+
) {
16+
if (urlProtocol.equals("http") || urlProtocol.equals("https")) {
17+
// We can't access HttpProxyAgent here or even thisd class, since we're in the bootstrap loader, but
18+
// we've already stored a proxy on ProxySelector for all URLs, so we can just use that directly:
19+
proxyArgument = ProxySelector.getDefault().select(
20+
URI.create("http://example.com")
21+
).get(0);
22+
}
23+
}
24+
}
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package tech.httptoolkit.javaagent.advice;
22

33
import net.bytebuddy.asm.Advice;
4-
import tech.httptoolkit.javaagent.HttpProxyAgent;
54

5+
import javax.net.ssl.SSLContext;
66
import javax.net.ssl.SSLSocketFactory;
7+
import java.security.NoSuchAlgorithmException;
78

89
public class ReturnSslSocketFactoryAdvice {
910
@Advice.OnMethodExit
1011
public static void sslSocketFactory(@Advice.Return(readOnly = false) SSLSocketFactory returnValue) {
11-
returnValue = HttpProxyAgent.getInterceptedSslContext().getSocketFactory();
12+
try {
13+
returnValue = SSLContext.getDefault().getSocketFactory();
14+
} catch (NoSuchAlgorithmException e) {
15+
throw new RuntimeException(e);
16+
}
1217
}
1318
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package tech.httptoolkit.javaagent.advice;
2+
3+
import net.bytebuddy.asm.Advice;
4+
5+
// General purpose advice which skips a given method, returning the default value for its type
6+
// (so usually null) if there is a return value, and silently doing nothing.
7+
public class SkipMethodAdvice {
8+
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) // => skip if we return true (or similar)
9+
public static boolean skipMethod() {
10+
return true; // Skip the method body entirely
11+
}
12+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ fun interceptAllHttps(config: Config, instrumentation: Instrumentation) {
8686
ApacheSslSocketFactoryTransformer(logger),
8787
ApacheClientTlsStrategyTransformer(logger),
8888
JavaClientTransformer(logger),
89+
UrlConnectionTransformer(logger),
90+
HttpsUrlConnectionTransformer(logger),
91+
ProxySelectorTransformer(logger),
92+
SslContextTransformer(logger),
8993
JettyClientTransformer(logger),
9094
AsyncHttpClientConfigTransformer(logger),
9195
AsyncHttpChannelManagerTransformer(logger),
@@ -123,6 +127,10 @@ private fun setDefaultProxy(proxyHost: String, proxyPort: Int) {
123127
System.setProperty("https.proxyHost", proxyHost)
124128
System.setProperty("https.proxyPort", proxyPort.toString())
125129

130+
// We back up the properties in our namespace too, in case anybody manually overrides the above:
131+
System.setProperty("tech.httptoolkit.proxyHost", proxyHost)
132+
System.setProperty("tech.httptoolkit.proxyPort", proxyPort.toString())
133+
126134
val proxySelector = ConstantProxySelector(proxyHost, proxyPort)
127135
AgentProxySelector = proxySelector
128136
ProxySelector.setDefault(proxySelector)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package tech.httptoolkit.javaagent
2+
3+
import net.bytebuddy.agent.builder.AgentBuilder
4+
import net.bytebuddy.asm.Advice
5+
import net.bytebuddy.dynamic.DynamicType
6+
import net.bytebuddy.matcher.ElementMatchers.*
7+
import tech.httptoolkit.javaagent.advice.ReturnSslSocketFactoryAdvice
8+
9+
// We override the SSLSocketFactory field for HttpsURLConnections. This is the only way to access the
10+
// configured field, so this effectively reconfigured every such connection to trust our certificate.
11+
// Without this, connections still work as our SSLContext is the default, but this ensures they work
12+
// even for connections that are explicitly configured with their own settings.
13+
class HttpsUrlConnectionTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) {
14+
override fun register(builder: AgentBuilder): AgentBuilder {
15+
return builder
16+
.type(
17+
named("javax.net.ssl.HttpsURLConnection")
18+
).transform(this)
19+
}
20+
21+
override fun transform(builder: DynamicType.Builder<*>): DynamicType.Builder<*> {
22+
return builder
23+
.visit(Advice.to(ReturnSslSocketFactoryAdvice::class.java)
24+
.on(hasMethodName("getSSLSocketFactory")))
25+
}
26+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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.dynamic.DynamicType
7+
import net.bytebuddy.matcher.ElementMatchers.*
8+
import tech.httptoolkit.javaagent.advice.OverrideAllProxySelectionAdvice
9+
import tech.httptoolkit.javaagent.advice.OverrideUrlConnectionProxyAdvice
10+
import tech.httptoolkit.javaagent.advice.SkipMethodAdvice
11+
12+
// To ensure that target applications don't override our ProxySelector (which we configure as the default), we
13+
// also patch the ProxySelector class itself, to guarantee that our proxy is always always selected, and
14+
// to stop anybody else changing the default.
15+
class ProxySelectorTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) {
16+
override fun register(builder: AgentBuilder): AgentBuilder {
17+
return builder
18+
.type(
19+
hasSuperType(named("java.net.ProxySelector"))
20+
).and(
21+
not(isInterface())
22+
).transform(this)
23+
}
24+
25+
override fun transform(builder: DynamicType.Builder<*>): DynamicType.Builder<*> {
26+
return builder
27+
// We patch *all* proxy selectors, so that even code which doesn't use the default
28+
// still returns our proxy regardless.
29+
.visit(Advice.to(OverrideAllProxySelectionAdvice::class.java)
30+
.on(
31+
hasMethodName<MethodDescription>("select")
32+
.and(takesArguments(1))
33+
.and(takesArgument(0, named("java.net.URI")))))
34+
// We already set the default ProxySelector on startup, before we intercept anything.
35+
// Here we patch ProxySelector so nobody can overwrite that later.
36+
.visit(Advice.to(SkipMethodAdvice::class.java)
37+
.on(hasMethodName("setDefault")));
38+
}
39+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package tech.httptoolkit.javaagent
2+
3+
import net.bytebuddy.agent.builder.AgentBuilder
4+
import net.bytebuddy.asm.Advice
5+
import net.bytebuddy.dynamic.DynamicType
6+
import net.bytebuddy.matcher.ElementMatchers.*
7+
import tech.httptoolkit.javaagent.advice.SkipMethodAdvice
8+
9+
// We patch SSL context purely to ensure that the default context that we set isn't changed later
10+
// by anybody else. The default context is already set in AgentMain before we begin patching.
11+
class SslContextTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) {
12+
override fun register(builder: AgentBuilder): AgentBuilder {
13+
return builder
14+
.type(
15+
named("javax.net.ssl.SSLContext")
16+
).transform(this)
17+
}
18+
19+
override fun transform(builder: DynamicType.Builder<*>): DynamicType.Builder<*> {
20+
return builder
21+
// We set the default SSLContext on startup, before we intercepted anything.
22+
// Here we patch SSLContext itself so nobody can overwrite that later.
23+
.visit(Advice.to(SkipMethodAdvice::class.java)
24+
.on(hasMethodName("setDefault")));
25+
}
26+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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.dynamic.DynamicType
7+
import net.bytebuddy.matcher.ElementMatchers.*
8+
import tech.httptoolkit.javaagent.advice.OverrideUrlConnectionProxyAdvice
9+
10+
// We override URL.openConnection() so that even if a proxy setting is passed explicitly, it's
11+
// overridden and ignored (for HTTP(S) traffic only) so all such traffic goes to our proxy.
12+
class UrlConnectionTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) {
13+
override fun register(builder: AgentBuilder): AgentBuilder {
14+
return builder
15+
.type(
16+
named("java.net.URL")
17+
).transform(this)
18+
}
19+
20+
override fun transform(builder: DynamicType.Builder<*>): DynamicType.Builder<*> {
21+
return builder
22+
.visit(Advice.to(OverrideUrlConnectionProxyAdvice::class.java)
23+
.on(
24+
hasMethodName<MethodDescription>("openConnection")
25+
.and(takesArguments(1))
26+
.and(takesArgument(0, named("java.net.Proxy")))))
27+
}
28+
}

test-app/src/main/java/tech/httptoolkit/testapp/cases/HttpUrlConnCase.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
import java.io.IOException;
44
import java.net.HttpURLConnection;
55
import java.net.MalformedURLException;
6+
import java.net.Proxy;
67
import java.net.URL;
78

9+
// We take over default HttpUrlConnections with no problem by setting config vars, but that doesn't apply in all
10+
// cases, e.g. if the target code manages its own proxy config, or disables it. We intercept here to forcibly
11+
// ensure that our proxy is _always_ used, regardless of the passed proxy configuration.
812
public class HttpUrlConnCase extends ClientCase<URL> {
913

1014
@Override
@@ -14,7 +18,7 @@ public URL newClient(String url) throws MalformedURLException {
1418

1519
@Override
1620
public int test(String url, URL urlInstance) throws IOException {
17-
HttpURLConnection connection = (HttpURLConnection) urlInstance.openConnection();
21+
HttpURLConnection connection = (HttpURLConnection) urlInstance.openConnection(Proxy.NO_PROXY);
1822
return connection.getResponseCode();
1923
}
2024
}

0 commit comments

Comments
 (0)