Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions http/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,58 @@
# HTTP module

This module provides the HTTP client implementation used internally by the Split SDK.
Internal HTTP client for the Split SDK. Not exposed to SDK consumers.

Includes request/response lifecycle, certificate pinning runtime, proxy tunnelling, and TLS configuration. Hidden from SDK consumers.
## Building an `HttpClient`

Use `HttpClientImpl.Builder` to create an instance:

```java
HttpClient client = new HttpClientImpl.Builder()
.setConnectionTimeout(15_000)
.setReadTimeout(15_000)
.setTlsUpdater(tlsUpdater) // optional – TlsUpdater SPI
.setProxy(httpProxy) // optional – proxy config from :http-domain
.setProxyAuthenticator(authenticator) // optional – SplitAuthenticator from :http-domain
.setCertificatePinningConfiguration(pinConfig) // optional – cert pins from :http-domain
.setDevelopmentSslConfig(devSslConfig) // optional – dev/test SSL overrides
.build();
```

## Making requests

```java
// Simple GET
HttpRequest req = client.request(uri, HttpMethod.GET);
HttpResponse resp = req.execute();

// POST with body and extra headers
HttpRequest post = client.request(uri, HttpMethod.POST, jsonBody, extraHeaders);
HttpResponse resp = post.execute();

// SSE streaming
HttpStreamRequest stream = client.streamRequest(uri);
HttpStreamResponse streamResp = stream.execute();
```

## Global headers

```java
client.setHeader("Authorization", "Bearer " + apiKey);
client.addHeaders(commonHeaders);

// Streaming-specific headers (only applied to streamRequest calls)
client.setStreamingHeader("SplitSDKClientKey", clientKey);
```

## TLS on older devices

Implement the `TlsUpdater` SPI and pass it to the builder. The client calls `couldBeOld()` to decide whether to force TLS 1.2 via `Tls12OnlySocketFactory`.

## URI building

```java
URI uri = new URIBuilder("https://sdk.split.io/api")
.addPath("splitChanges")
.addParameter("since", "-1")
.build();
```
2 changes: 1 addition & 1 deletion http/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ android {
dependencies {
implementation libs.annotation
implementation project(':logger')
implementation project(':http-domain')
api project(':http-domain')

testImplementation libs.junit4
testImplementation libs.mockitoCore
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.split.android.client.network;

import android.util.Base64;

class DefaultBase64Encoder implements Base64Encoder {

@Override
public String encode(String value) {
if (value == null) {
return null;
}
return Base64.encodeToString(value.getBytes(), Base64.NO_WRAP);
}

@Override
public String encode(byte[] bytes) {
if (bytes == null) {
return null;
}
return Base64.encodeToString(bytes, Base64.NO_WRAP);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.split.android.client.network;

import android.content.Context;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
Expand All @@ -20,8 +18,6 @@

import javax.net.ssl.SSLSocketFactory;

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

public class HttpClientImpl implements HttpClient {
Expand Down Expand Up @@ -180,26 +176,15 @@ private SplitUrlConnectionAuthenticator initializeProxyAuthenticator(HttpProxy p
return null;
} else if (proxyAuthenticator != null) {
return new SplitUrlConnectionAuthenticator(proxyAuthenticator);
} else if (!Utils.isNullOrEmpty(proxy.getUsername())) {
} else if (proxy.getUsername() != null && !proxy.getUsername().isEmpty()) {
return createBasicAuthenticator(proxy.getUsername(), proxy.getPassword());
}

return null;
}

private static SplitUrlConnectionAuthenticator createBasicAuthenticator(String username, String password) {
return new SplitUrlConnectionAuthenticator(new SplitBasicAuthenticator(username, password, new Base64Encoder() {

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

@Override
public String encode(byte[] bytes) {
return Base64Util.encode(bytes);
}
}));
return new SplitUrlConnectionAuthenticator(new SplitBasicAuthenticator(username, password, new DefaultBase64Encoder()));
}

public static class Builder {
Expand All @@ -211,14 +196,15 @@ public static class Builder {
private long mConnectionTimeout = -1;
private DevelopmentSslConfig mDevelopmentSslConfig = null;
private SSLSocketFactory mSslSocketFactory = null;
private Context mHostAppContext;
@Nullable
private TlsUpdater mTlsUpdater;
private UrlSanitizer mUrlSanitizer;
private CertificatePinningConfiguration mCertificatePinningConfiguration;
private CertificateChecker mCertificateChecker;
private Base64Decoder mBase64Decoder = new DefaultBase64Decoder();

public Builder setContext(Context context) {
mHostAppContext = context;
public Builder setTlsUpdater(@Nullable TlsUpdater tlsUpdater) {
mTlsUpdater = tlsUpdater;
return this;
}

Expand Down Expand Up @@ -279,13 +265,13 @@ Builder setBase64Decoder(Base64Decoder base64Decoder) {

public HttpClient build() {
if (mDevelopmentSslConfig == null) {
if (LegacyTlsUpdater.couldBeOld()) {
LegacyTlsUpdater.update(mHostAppContext);
if (mTlsUpdater != null && mTlsUpdater.couldBeOld()) {
mTlsUpdater.update();
}

if (mProxy != null) {
mSslSocketFactory = createSslSocketFactoryFromProxy(mProxy);
} else if (LegacyTlsUpdater.couldBeOld()) {
} else if (mTlsUpdater != null && mTlsUpdater.couldBeOld()) {
try {
mSslSocketFactory = new Tls12OnlySocketFactory();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.split.android.client.network;

import static io.split.android.client.utils.Utils.getAsInt;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand Down Expand Up @@ -100,6 +99,13 @@ static void applyTimeouts(long readTimeout, long connectionTimeout, HttpURLConne
}
}

private static int getAsInt(long value) {
if (value > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
return (int) value;
}

static void applySslConfig(SSLSocketFactory sslSocketFactory, DevelopmentSslConfig developmentSslConfig, HttpURLConnection connection) {
if (sslSocketFactory != null) {
if (connection instanceof HttpsURLConnection) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.split.android.client.network;

import static io.split.android.client.utils.Utils.checkNotNull;
import static java.util.Objects.requireNonNull;

import static io.split.android.client.network.HttpRequestHelper.applySslConfig;
import static io.split.android.client.network.HttpRequestHelper.applyTimeouts;
Expand Down Expand Up @@ -29,14 +29,19 @@
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocketFactory;

import io.split.android.client.service.http.HttpStatus;
import io.split.android.client.utils.logger.Logger;

public class HttpRequestImpl implements HttpRequest {

public static final String CONTENT_TYPE = "Content-Type";
public static final String APPLICATION_JSON_CHARSET_UTF_8 = "application/json; charset=utf-8";

/**
* Non-retryable status code for SSL errors.
* Mirrors HttpStatus.INTERNAL_NON_RETRYABLE from :main.
*/
static final int NON_RETRYABLE_STATUS_CODE = 9009;

private final URI mUri;
private final String mBody;
private final HttpMethod mHttpMethod;
Expand Down Expand Up @@ -73,11 +78,11 @@ public class HttpRequestImpl implements HttpRequest {
@Nullable SSLSocketFactory sslSocketFactory,
@NonNull UrlSanitizer urlSanitizer,
@Nullable CertificateChecker certificateChecker) {
mUri = checkNotNull(uri);
mHttpMethod = checkNotNull(httpMethod);
mUri = requireNonNull(uri);
mHttpMethod = requireNonNull(httpMethod);
mBody = body;
mUrlSanitizer = checkNotNull(urlSanitizer);
mHeaders = new HashMap<>(checkNotNull(headers));
mUrlSanitizer = requireNonNull(urlSanitizer);
mHeaders = new HashMap<>(requireNonNull(headers));
mProxy = proxy;
mHttpProxy = httpProxy;
mProxyAuthenticator = proxyAuthenticator;
Expand Down Expand Up @@ -119,7 +124,7 @@ private HttpResponse getRequest(AtomicBoolean wasRetried) throws HttpException {
} catch (ProtocolException e) {
throw new HttpException("Http method not allowed: " + e.getLocalizedMessage());
} catch (SSLPeerUnverifiedException e) {
throw new HttpException("SSL Peer Unverified: " + e.getLocalizedMessage(), HttpStatus.INTERNAL_NON_RETRYABLE.getCode());
throw new HttpException("SSL Peer Unverified: " + e.getLocalizedMessage(), NON_RETRYABLE_STATUS_CODE);
} catch (IOException e) {
throw new HttpException("Something happened while retrieving data: " + e.getLocalizedMessage());
} finally {
Expand All @@ -146,7 +151,7 @@ private HttpResponse postRequest(AtomicBoolean wasRetried) throws HttpException
response = handleProxyAuthentication(response, false, wasRetried);
}
} catch (SSLPeerUnverifiedException e) {
throw new HttpException("SSL Peer Unverified: " + e.getLocalizedMessage(), HttpStatus.INTERNAL_NON_RETRYABLE.getCode());
throw new HttpException("SSL Peer Unverified: " + e.getLocalizedMessage(), NON_RETRYABLE_STATUS_CODE);
} catch (IOException e) {
throw new HttpException("Something happened while posting data: " + e.getLocalizedMessage());
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import static io.split.android.client.network.HttpRequestHelper.checkPins;
import static io.split.android.client.network.HttpRequestHelper.createConnection;
import static io.split.android.client.utils.Utils.checkNotNull;
import static java.util.Objects.requireNonNull;

import static io.split.android.client.network.HttpRequestHelper.applySslConfig;
import static io.split.android.client.network.HttpRequestHelper.applyTimeouts;
Expand All @@ -28,7 +28,6 @@
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocketFactory;

import io.split.android.client.service.http.HttpStatus;
import io.split.android.client.utils.logger.Logger;

public class HttpStreamRequestImpl implements HttpStreamRequest {
Expand Down Expand Up @@ -72,11 +71,11 @@ public class HttpStreamRequestImpl implements HttpStreamRequest {
@Nullable HttpProxy httpProxy,
@Nullable ProxyCredentialsProvider proxyCredentialsProvider,
@Nullable ProxyCacertConnectionHandler proxyCacertConnectionHandler) {
mUri = checkNotNull(uri);
mUri = requireNonNull(uri);
mHttpMethod = HttpMethod.GET;
mProxy = proxy;
mUrlSanitizer = checkNotNull(urlSanitizer);
mHeaders = new HashMap<>(checkNotNull(headers));
mUrlSanitizer = requireNonNull(urlSanitizer);
mHeaders = new HashMap<>(requireNonNull(headers));
mProxyAuthenticator = proxyAuthenticator;
mConnectionTimeout = connectionTimeout;
mDevelopmentSslConfig = developmentSslConfig;
Expand Down Expand Up @@ -141,7 +140,7 @@ private HttpStreamResponse getRequest() throws HttpException, IOException {
throw new HttpException("Http method not allowed: " + e.getLocalizedMessage());
} catch (SSLPeerUnverifiedException e) {
disconnect();
throw new HttpException("SSL peer not verified: " + e.getLocalizedMessage(), HttpStatus.INTERNAL_NON_RETRYABLE.getCode());
throw new HttpException("SSL peer not verified: " + e.getLocalizedMessage(), HttpRequestImpl.NON_RETRYABLE_STATUS_CODE);
} catch (SocketException e) {
disconnect();
// Let socket-related IOExceptions pass through unwrapped for consistent error handling
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.split.android.client.network;

import static io.split.android.client.utils.Utils.checkNotNull;
import static java.util.Objects.requireNonNull;

/**
* Based on Guava PercentEscaper
Expand Down Expand Up @@ -37,7 +37,7 @@ final class PercentEscaper extends UnicodeEscaper {
* @throws IllegalArgumentException if any of the parameters were invalid
*/
public PercentEscaper(String safeChars, boolean plusForSpace) {
checkNotNull(safeChars); // eager for GWT.
requireNonNull(safeChars); // eager for GWT.
// Avoid any misunderstandings about the behavior of this escaper
if (safeChars.matches(".*[0-9A-Za-z].*")) {
throw new IllegalArgumentException(
Expand Down Expand Up @@ -78,7 +78,7 @@ private static boolean[] createSafeOctets(String safeChars) {
*/
@Override
protected int nextEscapeIndex(CharSequence csq, int index, int end) {
checkNotNull(csq);
requireNonNull(csq);
for (; index < end; index++) {
char c = csq.charAt(index);
if (c >= safeOctets.length || !safeOctets[c]) {
Expand All @@ -94,7 +94,7 @@ protected int nextEscapeIndex(CharSequence csq, int index, int end) {
*/
@Override
public String escape(String s) {
checkNotNull(s);
requireNonNull(s);
int slen = s.length();
for (int index = 0; index < slen; index++) {
char c = s.charAt(index);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.split.android.client.network;

import static io.split.android.client.utils.Utils.checkNotNull;
import static java.util.Objects.requireNonNull;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand Down Expand Up @@ -43,7 +43,7 @@ class ProxySslSocketFactoryProviderImpl implements ProxySslSocketFactoryProvider
}

ProxySslSocketFactoryProviderImpl(@NonNull Base64Decoder base64Decoder) {
mBase64Decoder = checkNotNull(base64Decoder);
mBase64Decoder = requireNonNull(base64Decoder);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.split.android.client.network;

public interface TlsUpdater {

/**
* Return true if the device may need a TLS update.
*/
boolean couldBeOld();

/**
* Perform the TLS update.
*/
void update();
}
Loading
Loading