Skip to content

Commit fc5d863

Browse files
committed
ssl tunnelling
1 parent 653d1ab commit fc5d863

6 files changed

Lines changed: 1072 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package io.split.android.client.network;
2+
3+
import androidx.annotation.NonNull;
4+
import androidx.annotation.Nullable;
5+
6+
import java.io.IOException;
7+
import java.net.HttpRetryException;
8+
import java.net.Socket;
9+
import java.net.URL;
10+
import java.security.cert.Certificate;
11+
import java.util.Map;
12+
13+
import javax.net.ssl.SSLContext;
14+
import javax.net.ssl.SSLSocket;
15+
import javax.net.ssl.SSLSocketFactory;
16+
17+
import io.split.android.client.utils.logger.Logger;
18+
19+
/**
20+
* Handles PROXY_CACERT SSL proxy connections.
21+
*
22+
* This handler establishes SSL tunnels through SSL proxies using custom CA certificates
23+
* for proxy authentication, then executes HTTP requests through the SSL tunnel.
24+
*
25+
* CONNECT Specification Compliance:
26+
* - Establishes SSL connection to proxy for authentication
27+
* - Sends CONNECT request through SSL connection
28+
* - Maintains SSL socket connection after successful CONNECT
29+
* - Executes HTTP requests through SSL tunnel socket
30+
*/
31+
class ProxyCacertConnectionHandler implements SslProxyConnectionHandler {
32+
33+
public static final String HTTPS = "https";
34+
public static final String HTTP = "http";
35+
public static final int PORT_HTTPS = 443;
36+
public static final int HTTP_PORT = 80;
37+
private final HttpOverTunnelExecutor mTunnelExecutor;
38+
39+
public ProxyCacertConnectionHandler() {
40+
mTunnelExecutor = new HttpOverTunnelExecutor();
41+
}
42+
43+
// For testing - allow injection of dependencies
44+
ProxyCacertConnectionHandler(HttpOverTunnelExecutor tunnelExecutor) {
45+
mTunnelExecutor = tunnelExecutor;
46+
}
47+
48+
@Override
49+
public boolean canHandle(@NonNull HttpProxy httpProxy) {
50+
return httpProxy.getAuthType() == HttpProxy.ProxyAuthType.PROXY_CACERT || httpProxy.getAuthType() == HttpProxy.ProxyAuthType.MTLS;
51+
}
52+
53+
@Override
54+
@NonNull
55+
public HttpResponse executeRequest(@NonNull HttpProxy httpProxy,
56+
@NonNull URL targetUrl,
57+
@NonNull HttpMethod method,
58+
@NonNull Map<String, String> headers,
59+
@Nullable String body,
60+
@NonNull SSLSocketFactory sslSocketFactory,
61+
@Nullable ProxyCredentialsProvider proxyCredentialsProvider) throws IOException {
62+
63+
Logger.v("PROXY_CACERT: Executing request to: " + targetUrl);
64+
65+
try {
66+
// PROXY_CACERT requires SSL authentication with proxy using CA certificate
67+
// Use the provided sslSocketFactory which contains the proxy CA certificate
68+
69+
// Establish SSL tunnel through proxy with CA certificate authentication
70+
SslProxyTunnelEstablisher tunnelEstablisher = new SslProxyTunnelEstablisher();
71+
Socket tunnelSocket = tunnelEstablisher.establishTunnel(
72+
httpProxy.getHost(),
73+
httpProxy.getPort(),
74+
targetUrl.getHost(),
75+
getTargetPort(targetUrl),
76+
sslSocketFactory, // Use the SSL socket factory with proxy CA certificate,
77+
proxyCredentialsProvider
78+
);
79+
80+
Logger.v("SSL tunnel established successfully");
81+
82+
Socket finalSocket = tunnelSocket;
83+
Certificate[] serverCertificates = null;
84+
85+
// If the origin is HTTPS, wrap the tunnel socket with a new SSLSocket (system CA)
86+
if ("https".equalsIgnoreCase(targetUrl.getProtocol())) {
87+
Logger.v("Wrapping tunnel socket with new SSLSocket for origin server handshake");
88+
try {
89+
// Get system default SSL context
90+
SSLContext systemSslContext = SSLContext.getInstance("TLS");
91+
systemSslContext.init(null, null, null); // null = system default trust managers
92+
SSLSocketFactory systemSslSocketFactory = systemSslContext.getSocketFactory();
93+
94+
// Create SSLSocket layered over tunnel
95+
finalSocket = systemSslSocketFactory.createSocket(
96+
tunnelSocket,
97+
targetUrl.getHost(),
98+
getTargetPort(targetUrl),
99+
true // autoClose
100+
);
101+
if (finalSocket instanceof SSLSocket) {
102+
SSLSocket originSslSocket = (SSLSocket) finalSocket;
103+
originSslSocket.setUseClientMode(true);
104+
originSslSocket.startHandshake();
105+
106+
// Capture server certificates after successful handshake
107+
try {
108+
serverCertificates = originSslSocket.getSession().getPeerCertificates();
109+
Logger.v("Captured " + (serverCertificates != null ? serverCertificates.length : 0) + " certificates from origin server");
110+
} catch (Exception certEx) {
111+
Logger.w("Could not capture origin server certificates: " + certEx.getMessage());
112+
}
113+
} else {
114+
throw new IOException("Failed to create SSLSocket to origin");
115+
}
116+
Logger.v("SSL handshake with origin server completed");
117+
} catch (Exception sslEx) {
118+
Logger.e("Failed to establish SSL connection to origin: " + sslEx.getMessage());
119+
throw new IOException("Failed to establish SSL connection to origin server", sslEx);
120+
}
121+
}
122+
123+
// Execute request through the (possibly wrapped) socket, passing the certificates
124+
HttpResponse response = mTunnelExecutor.executeRequest(
125+
finalSocket,
126+
targetUrl,
127+
method,
128+
headers,
129+
body,
130+
serverCertificates
131+
);
132+
133+
Logger.v("PROXY_CACERT request completed successfully, status: " + response.getHttpStatus());
134+
return response;
135+
136+
} catch (Exception e) {
137+
if (e instanceof HttpRetryException) {
138+
throw (HttpRetryException) e;
139+
}
140+
throw new IOException("Failed to execute request through custom tunnel", e);
141+
}
142+
}
143+
144+
private static int getTargetPort(@NonNull URL targetUrl) {
145+
int port = targetUrl.getPort();
146+
if (port == -1) {
147+
if (HTTPS.equalsIgnoreCase(targetUrl.getProtocol())) {
148+
return PORT_HTTPS;
149+
} else if (HTTP.equalsIgnoreCase(targetUrl.getProtocol())) {
150+
return HTTP_PORT;
151+
}
152+
}
153+
return port;
154+
}
155+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.split.android.client.network;
2+
3+
import androidx.annotation.NonNull;
4+
import androidx.annotation.Nullable;
5+
6+
import java.io.IOException;
7+
import java.net.URL;
8+
import java.util.Map;
9+
10+
import javax.net.ssl.SSLSocketFactory;
11+
12+
/**
13+
* Interface for handling SSL proxy connections that HttpURLConnection cannot support natively.
14+
* Provides custom connection establishment for PROXY_CACERT and MTLS proxy scenarios
15+
* where the proxy itself requires SSL authentication.
16+
*/
17+
interface SslProxyConnectionHandler {
18+
19+
/**
20+
* Determines if this handler can handle the given proxy configuration.
21+
*
22+
* @param proxy The HttpProxy configuration
23+
* @return true if this handler supports the proxy authentication type
24+
*/
25+
boolean canHandle(@NonNull HttpProxy proxy);
26+
27+
/**
28+
* Executes an HTTP request through an SSL proxy using custom connection handling.
29+
* This bypasses HttpURLConnection's built-in proxy mechanism entirely.
30+
*
31+
* @param proxy The SSL proxy configuration
32+
* @param targetUrl The final destination URL
33+
* @param method HTTP method for the request
34+
* @param headers Headers to include in the request
35+
* @param body Request body (null for GET requests)
36+
* @param sslSocketFactory SSL socket factory configured for proxy authentication
37+
* @param proxyCredentialsProvider Credentials provider for proxy authentication
38+
* @return HttpResponse containing the server's response
39+
* @throws IOException if the request execution fails
40+
*/
41+
@NonNull
42+
HttpResponse executeRequest(
43+
@NonNull HttpProxy proxy,
44+
@NonNull URL targetUrl,
45+
@NonNull HttpMethod method,
46+
@NonNull Map<String, String> headers,
47+
@Nullable String body,
48+
@NonNull SSLSocketFactory sslSocketFactory,
49+
@Nullable ProxyCredentialsProvider proxyCredentialsProvider) throws IOException;
50+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package io.split.android.client.network;
2+
3+
import androidx.annotation.NonNull;
4+
import androidx.annotation.Nullable;
5+
6+
import java.io.IOException;
7+
import java.net.URL;
8+
import java.util.Arrays;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
import javax.net.ssl.SSLSocketFactory;
13+
14+
import io.split.android.client.utils.logger.Logger;
15+
16+
/**
17+
* Factory/manager for creating appropriate SSL proxy connection handlers.
18+
* Routes SSL proxy requests to the correct handler based on proxy authentication type.
19+
*/
20+
class SslProxyConnectionManager {
21+
22+
private final List<SslProxyConnectionHandler> mHandlers;
23+
24+
public SslProxyConnectionManager() {
25+
mHandlers = Arrays.asList(
26+
new ProxyCacertConnectionHandler()
27+
// MtlsProxyConnectionHandler will be added later
28+
);
29+
}
30+
31+
// For testing - allow injection of custom handlers
32+
SslProxyConnectionManager(List<SslProxyConnectionHandler> handlers) {
33+
mHandlers = handlers;
34+
}
35+
36+
/**
37+
* Determines if the given proxy requires custom SSL proxy handling.
38+
*
39+
* @param proxy The HttpProxy configuration to check
40+
* @return true if custom SSL proxy handling is needed
41+
*/
42+
public boolean requiresCustomSslHandling(@Nullable HttpProxy proxy) {
43+
if (proxy == null) {
44+
return false;
45+
}
46+
47+
for (SslProxyConnectionHandler handler : mHandlers) {
48+
if (handler.canHandle(proxy)) {
49+
return true;
50+
}
51+
}
52+
53+
return false;
54+
}
55+
56+
/**
57+
* Executes an HTTP request using the appropriate SSL proxy handler.
58+
*
59+
* @param proxy The SSL proxy configuration
60+
* @param targetUrl The final destination URL
61+
* @param method HTTP method for the request
62+
* @param headers Headers to include in the request
63+
* @param body Request body (null for GET requests)
64+
* @param sslSocketFactory SSL socket factory configured for proxy authentication
65+
* @param proxyCredentialsProvider Credentials provider for proxy authentication
66+
* @return HttpResponse containing the server's response
67+
* @throws IOException if no handler can handle the proxy or if request execution fails
68+
*/
69+
@NonNull
70+
public HttpResponse executeRequest(
71+
@NonNull HttpProxy proxy,
72+
@NonNull URL targetUrl,
73+
@NonNull HttpMethod method,
74+
@NonNull Map<String, String> headers,
75+
@Nullable String body,
76+
@NonNull SSLSocketFactory sslSocketFactory,
77+
@Nullable ProxyCredentialsProvider proxyCredentialsProvider) throws IOException {
78+
79+
Logger.v("Looking for SSL proxy handler for auth type: " + proxy.getAuthType());
80+
81+
// Find appropriate handler
82+
for (SslProxyConnectionHandler handler : mHandlers) {
83+
if (handler.canHandle(proxy)) {
84+
return handler.executeRequest(proxy, targetUrl, method, headers, body, sslSocketFactory, proxyCredentialsProvider);
85+
}
86+
}
87+
88+
throw new IOException("No SSL proxy handler available for proxy auth type: " + proxy.getAuthType());
89+
}
90+
}

0 commit comments

Comments
 (0)