Skip to content
9 changes: 6 additions & 3 deletions src/androidTest/java/fake/HttpResponseMock.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package fake;

import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.BlockingQueue;
import java.security.cert.Certificate;

import io.split.android.client.network.BaseHttpResponseImpl;
import io.split.android.client.network.HttpResponse;
Expand All @@ -25,4 +23,9 @@ public HttpResponseMock(int status, String data) {
public String getData() {
return data;
}

@Override
public Certificate[] getServerCertificates() {
return new Certificate[0];
}
}
7 changes: 7 additions & 0 deletions src/androidTest/java/fake/HttpResponseStub.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package fake;

import java.security.cert.Certificate;

import io.split.android.client.network.BaseHttpResponseImpl;
import io.split.android.client.network.HttpResponse;

Expand Down Expand Up @@ -29,4 +31,9 @@ public boolean isSuccess() {
public String getData() {
return data;
}

@Override
public Certificate[] getServerCertificates() {
return new Certificate[0];
}
}
10 changes: 9 additions & 1 deletion src/androidTest/java/helper/TestableSplitConfigBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.split.android.client.impressions.ImpressionListener;
import io.split.android.client.network.CertificatePinningConfiguration;
import io.split.android.client.network.DevelopmentSslConfig;
import io.split.android.client.network.ProxyConfiguration;
import io.split.android.client.network.SplitAuthenticator;
import io.split.android.client.service.ServiceConstants;
import io.split.android.client.service.impressions.ImpressionsMode;
Expand Down Expand Up @@ -66,6 +67,7 @@ public class TestableSplitConfigBuilder {
private CertificatePinningConfiguration mCertificatePinningConfiguration;
private long mImpressionsDedupeTimeInterval = ServiceConstants.DEFAULT_IMPRESSIONS_DEDUPE_TIME_INTERVAL;
private RolloutCacheConfiguration mRolloutCacheConfiguration = RolloutCacheConfiguration.builder().build();
private ProxyConfiguration mProxyConfiguration = null;

public TestableSplitConfigBuilder() {
mServiceEndpoints = ServiceEndpoints.builder().build();
Expand Down Expand Up @@ -281,6 +283,11 @@ public TestableSplitConfigBuilder rolloutCacheConfiguration(RolloutCacheConfigur
return this;
}

public TestableSplitConfigBuilder logger(ProxyConfiguration proxyConfiguration) {
this.mProxyConfiguration = proxyConfiguration;
return this;
}

public SplitClientConfig build() {
Constructor constructor = SplitClientConfig.class.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Expand Down Expand Up @@ -337,7 +344,8 @@ public SplitClientConfig build() {
mObserverCacheExpirationPeriod,
mCertificatePinningConfiguration,
mImpressionsDedupeTimeInterval,
mRolloutCacheConfiguration);
mRolloutCacheConfiguration,
mProxyConfiguration);

Logger.instance().setLevel(mLogLevel);
return config;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.X509TrustManager;

import io.split.android.client.utils.Base64Util;
import io.split.android.client.utils.logger.Logger;

class CertificateCheckerImpl implements CertificateChecker {
Expand Down Expand Up @@ -100,17 +99,4 @@ private String certificateChainInfo(List<X509Certificate> cleanCertificates) {

return builder.toString();
}

private static class DefaultBase64Encoder implements Base64Encoder {

@Override
public String encode(String value) {
return Base64Util.encode(value);
}

@Override
public String encode(byte[] bytes) {
return Base64Util.encode(bytes);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.split.android.client.network;

import io.split.android.client.utils.Base64Util;

class DefaultBase64Encoder implements Base64Encoder {

@Override
public String encode(String value) {
return Base64Util.encode(value);
}

@Override
public String encode(byte[] bytes) {
return Base64Util.encode(bytes);
}
}
28 changes: 17 additions & 11 deletions src/main/java/io/split/android/client/network/HttpClientImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public class HttpClientImpl implements HttpClient {
private final UrlSanitizer mUrlSanitizer;
@Nullable
private final CertificateChecker mCertificateChecker;
@Nullable
private final ProxyCacertConnectionHandler mConnectionHandler;

HttpClientImpl(@Nullable HttpProxy proxy,
@Nullable SplitAuthenticator proxyAuthenticator,
Expand All @@ -66,6 +68,9 @@ public class HttpClientImpl implements HttpClient {
mSslSocketFactory = sslSocketFactory;
mUrlSanitizer = urlSanitizer;
mCertificateChecker = certificateChecker;
mConnectionHandler = mHttpProxy != null && mSslSocketFactory != null &&
(mHttpProxy.getCaCertStream() != null || mHttpProxy.getClientCertStream() != null) ?
new ProxyCacertConnectionHandler() : null;
}

@Override
Expand Down Expand Up @@ -113,7 +118,8 @@ public HttpStreamRequest streamRequest(URI uri) {
mUrlSanitizer,
mCertificateChecker,
mHttpProxy,
mProxyCredentialsProvider);
mProxyCredentialsProvider,
mConnectionHandler);
}

@Override
Expand Down Expand Up @@ -274,7 +280,7 @@ public HttpClient build() {
}

if (mProxy != null) {
mSslSocketFactory = createSslSocketFactoryFromProxy();
mSslSocketFactory = createSslSocketFactoryFromProxy(mProxy);
} else {
try {
mSslSocketFactory = new Tls12OnlySocketFactory();
Expand Down Expand Up @@ -311,23 +317,23 @@ public HttpClient build() {
certificateChecker);
}

private SSLSocketFactory createSslSocketFactoryFromProxy() {
private SSLSocketFactory createSslSocketFactoryFromProxy(HttpProxy proxyParams) {
ProxySslSocketFactoryProviderImpl factoryProvider = new ProxySslSocketFactoryProviderImpl(mBase64Decoder);
try {
if (mProxy.getClientCertStream() != null && mProxy.getClientKeyStream() != null) {
try (InputStream caInput = mProxy.getCaCertStream();
InputStream certInput = mProxy.getClientCertStream();
InputStream keyInput = mProxy.getClientKeyStream()) {
Logger.v("Custom proxy CA cert and client cert/key loaded for proxy: " + mProxy.getHost());
if (proxyParams.getClientCertStream() != null && proxyParams.getClientKeyStream() != null) {
try (InputStream caInput = proxyParams.getCaCertStream();
InputStream certInput = proxyParams.getClientCertStream();
InputStream keyInput = proxyParams.getClientKeyStream()) {
Logger.v("Custom proxy CA cert and client cert/key loaded for proxy: " + proxyParams.getHost());
return factoryProvider.create(caInput, certInput, keyInput);
}
} else if (mProxy.getCaCertStream() != null) {
try (InputStream caInput = mProxy.getCaCertStream()) {
} else if (proxyParams.getCaCertStream() != null) {
try (InputStream caInput = proxyParams.getCaCertStream()) {
return factoryProvider.create(caInput);
}
}
} catch (Exception e) {
Logger.e("Failed to create SSLSocketFactory for proxy: " + mProxy.getHost() + ", error: " + e.getMessage());
Logger.e("Failed to create SSLSocketFactory for proxy: " + proxyParams.getHost() + ", error: " + e.getMessage());
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.security.cert.Certificate;
import java.util.Map;
Expand All @@ -31,20 +32,8 @@ public HttpOverTunnelExecutor() {
mResponseParser = new RawHttpResponseParser();
}

/**
* Executes an HTTP request through the established tunnel socket.
*
* @param tunnelSocket The SSL Socket with tunnel established (connection maintained)
* @param targetUrl The final destination URL (HTTP or HTTPS)
* @param method HTTP method for the request
* @param headers Headers to include in the request
* @param body Request body (null for GET requests)
* @param serverCertificates The server certificates from the SSL connection (null if not available)
* @return HttpResponse containing the server's response
* @throws IOException if the request execution fails
*/
@NonNull
public HttpResponse executeRequest(
HttpResponse executeRequest(
@NonNull Socket tunnelSocket,
@NonNull URL targetUrl,
@NonNull HttpMethod method,
Expand All @@ -58,12 +47,41 @@ public HttpResponse executeRequest(
sendHttpRequest(tunnelSocket, targetUrl, method, headers, body);

return readHttpResponse(tunnelSocket, serverCertificates);
} catch (SocketException e) {
// Let socket-related IOExceptions pass through unwrapped
// This ensures consistent behavior with non-proxy flows
throw e;
} catch (Exception e) {
// Wrap other exceptions in IOException
Logger.e("Failed to execute request through tunnel: " + e.getMessage());
throw new IOException("Failed to execute HTTP request through tunnel to " + targetUrl, e);
}
}

@NonNull
HttpStreamResponse executeStreamRequest(@NonNull Socket finalSocket,
@Nullable Socket tunnelSocket,
@Nullable Socket originSocket,
@NonNull URL targetUrl,
@NonNull HttpMethod method,
@NonNull Map<String, String> headers,
@Nullable Certificate[] serverCertificates) throws IOException {
Logger.v("Executing stream request through tunnel to: " + targetUrl);

try {
sendHttpRequest(finalSocket, targetUrl, method, headers, null);
return readHttpStreamResponse(finalSocket, originSocket);
} catch (SocketException e) {
// Let socket-related IOExceptions pass through unwrapped
// This ensures consistent behavior with non-proxy flows
throw e;
} catch (Exception e) {
// Wrap other exceptions in IOException
Logger.e("Failed to execute stream request through tunnel: " + e.getMessage());
throw new IOException("Failed to execute HTTP stream request through tunnel to " + targetUrl, e);
}
}

/**
* Sends the HTTP request through the tunnel socket.
*/
Expand Down Expand Up @@ -97,7 +115,6 @@ private void sendHttpRequest(
host += ":" + port;
}

Logger.v("Sending Host header: 'Host: " + host + "'");
writer.write("Host: " + host + CRLF);

// 3. Send custom headers (excluding Host and Content-Length)
Expand Down Expand Up @@ -151,6 +168,10 @@ private HttpResponse readHttpResponse(@NonNull Socket tunnelSocket, @Nullable Ce
return mResponseParser.parseHttpResponse(tunnelSocket.getInputStream(), serverCertificates);
}

private HttpStreamResponse readHttpStreamResponse(@NonNull Socket tunnelSocket, @Nullable Socket originSocket) throws IOException {
return mResponseParser.parseHttpStreamResponse(tunnelSocket.getInputStream(), tunnelSocket, originSocket);
}

/**
* Gets the target port from URL, defaulting based on protocol.
*/
Expand Down
30 changes: 17 additions & 13 deletions src/main/java/io/split/android/client/network/HttpRequestImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -190,23 +190,12 @@ private HttpURLConnection setUpConnection(boolean authenticate) throws IOExcepti

HttpURLConnection connection;
try {
connection = createConnection(
url,
mProxy,
mHttpProxy,
mProxyAuthenticator,
mHttpMethod,
mHeaders,
authenticate,
mSslSocketFactory,
mProxyCredentialsProvider,
mBody
);
connection = getConnection(authenticate, url);
} catch (HttpRetryException e) {
if (mProxyAuthenticator == null) {
throw e;
}
connection = createConnection(url, mProxy, mHttpProxy, mProxyAuthenticator, mHttpMethod, mHeaders, authenticate, null, null, null);
connection = getConnection(authenticate, url);
}
applyTimeouts(mReadTimeout, mConnectionTimeout, connection);
applySslConfig(mSslSocketFactory, mDevelopmentSslConfig, connection);
Expand All @@ -226,6 +215,21 @@ private HttpURLConnection setUpConnection(boolean authenticate) throws IOExcepti
return connection;
}

@NonNull
private HttpURLConnection getConnection(boolean authenticate, URL url) throws IOException {
return createConnection(
url,
mProxy,
mHttpProxy,
mProxyAuthenticator,
mHttpMethod,
mHeaders,
authenticate,
mSslSocketFactory,
mProxyCredentialsProvider,
mBody);
}

private static HttpResponse buildResponse(HttpURLConnection connection) throws IOException {
int responseCode = connection.getResponseCode();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class HttpResponseConnectionAdapter extends HttpsURLConnection {
private final HttpResponse mResponse;
private final URL mUrl;
private final Certificate[] mServerCertificates;
private OutputStream mOutputStream;
private final OutputStream mOutputStream;
private InputStream mInputStream;
private InputStream mErrorStream;
private boolean mDoOutput = false;
Expand All @@ -57,7 +57,7 @@ class HttpResponseConnectionAdapter extends HttpsURLConnection {
@NonNull OutputStream outputStream) {
this(url, response, serverCertificates, outputStream, null, null);
}

@VisibleForTesting
HttpResponseConnectionAdapter(@NonNull URL url,
@NonNull HttpResponse response,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.split.android.client.network;

import java.io.IOException;

public interface HttpStreamRequest {
void addHeader(String name, String value);
HttpStreamResponse execute() throws HttpException;
HttpStreamResponse execute() throws HttpException, IOException;
void close();
}
Loading
Loading