Skip to content
Open
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
3 changes: 0 additions & 3 deletions .github/workflows/sonarqube.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ on:
pull_request:
branches:
- '*'
push:
branches:
- master

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ dependencies {
include project(':events')
include project(':events-domain')
include project(':api')
include project(':http-domain')
include project(':http-api')
include project(':http')
}

Expand Down
File renamed without changes.
100 changes: 100 additions & 0 deletions http-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# HTTP API module

Public contracts and configuration types for the HTTP client.
These types are exposed to SDK consumers through the `:main` module's `api` dependency.

## `HttpClientConfiguration`

Bundles all HTTP client settings into a single object:

```java
HttpClientConfiguration config = HttpClientConfiguration.builder()
.connectionTimeout(15_000)
.readTimeout(15_000)
.proxy(proxy)
.proxyAuthenticator(authenticator)
.certificatePinningConfiguration(pinConfig)
.developmentSslConfig(devSsl)
.build();
```

## Proxy configuration

### Basic auth

```java
HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080)
.basicAuth("user", "pass")
.build();
```

### mTLS with custom CA

```java
HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8443)
.proxyCacert(caCertInputStream)
.mtls(clientCertInputStream, clientKeyInputStream)
.build();
```

### Custom credentials provider

```java
// Bearer token
HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080)
.credentialsProvider(() -> fetchBearerToken())
.build();

// Basic credentials
HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080)
.credentialsProvider(new BasicCredentialsProvider() {
public String getUsername() { return "user"; }
public String getPassword() { return "pass"; }
})
.build();
```

## Custom proxy authenticator

Implement `SplitAuthenticator` to handle proxy challenge/response flows:

```java
SplitAuthenticator authenticator = new SplitAuthenticator() {
@Override
public AuthenticatedRequest authenticate(@NonNull AuthenticatedRequest request) {
request.setHeader("Proxy-Authorization", "Bearer " + getToken());
return request;
}
};
```

The `AuthenticatedRequest` gives access to existing headers and the request URL, so the authenticator can make decisions based on context.

## Certificate pinning

```java
CertificatePinningConfiguration pinConfig = CertificatePinningConfiguration.builder()
// Pin by hash (sha256 or sha1)
.addPin("sdk.split.io", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
// Pin from a certificate file (derives hashes automatically)
.addPin("*.split.io", certInputStream)
// Optional: get notified on pin failures
.failureListener((host, certificateChain) -> {
Log.w("Split", "Pin failed for " + host
+ ", chain size: " + certificateChain.size());
})
.build();
```

Wildcard hosts are supported: `*.example.com` matches one subdomain, `**.example.com` matches any depth.

## Development SSL overrides

For test environments with self-signed certificates:

```java
DevelopmentSslConfig devSsl = new DevelopmentSslConfig(trustManager, hostnameVerifier);

// Or, if you already have an SSLSocketFactory:
DevelopmentSslConfig devSsl = new DevelopmentSslConfig(sslSocketFactory, trustManager, hostnameVerifier);
```
2 changes: 1 addition & 1 deletion http-domain/build.gradle → http-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
apply from: "$rootDir/gradle/common-android-library.gradle"

android {
namespace 'io.split.android.client.network.domain'
namespace 'io.split.android.client.network.api'

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.split.android.client.network;

public class Algorithm {
class Algorithm {

public static final String SHA256 = "sha256";
public static final String SHA1 = "sha1";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.split.android.client.network;

public interface Base64Decoder {
interface Base64Decoder {

byte[] decode(String base64);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

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

public class CertificateCheckerHelper {
class CertificateCheckerHelper {

@Nullable
public static Set<CertificatePin> getPinsForHost(String pattern, Map<String, Set<CertificatePin>> configuredPins) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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

public class DefaultBase64Decoder implements Base64Decoder {
class DefaultBase64Decoder implements Base64Decoder {

@Override
public byte[] decode(String base64) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ private HttpProxy(Builder builder, boolean isLegacy) {
mIsLegacy = isLegacy;
}

public @Nullable String getHost() {
public @NonNull String getHost() {
return mHost;
}

Expand Down Expand Up @@ -61,7 +61,7 @@ public int getPort() {
return mCredentialsProvider;
}

public static Builder newBuilder(@Nullable String host, int port) {
public static Builder newBuilder(@NonNull String host, int port) {
return new Builder(host, port);
}

Expand All @@ -70,7 +70,7 @@ public boolean isLegacy() {
}

public static class Builder {
private final @Nullable String mHost;
private final @NonNull String mHost;
private final int mPort;
private @Nullable String mUsername;
private @Nullable String mPassword;
Expand All @@ -80,7 +80,7 @@ public static class Builder {
@Nullable
private ProxyCredentialsProvider mCredentialsProvider;

private Builder(@Nullable String host, int port) {
private Builder(@NonNull String host, int port) {
mHost = host;
mPort = port;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import androidx.annotation.NonNull;

public interface PinEncoder {
interface PinEncoder {

@NonNull
byte[] encodeCertPin(String algorithm, byte[] encodedPublicKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

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

public class PinEncoderImpl implements PinEncoder {
class PinEncoderImpl implements PinEncoder {

@Override
@NonNull
Expand Down
5 changes: 0 additions & 5 deletions http-domain/README.md

This file was deleted.

92 changes: 79 additions & 13 deletions http/README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,100 @@
# HTTP module

Internal HTTP client for the Split SDK. Not exposed to SDK consumers.
HTTP client for the Split SDK.

## Building an `HttpClient`

Use `HttpClientImpl.Builder` to create an instance:
### Minimal

```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();
```

### With `HttpClientConfiguration` (preferred)

Bundle all settings into a single config object from `:http-api`:

```java
HttpClientConfiguration config = HttpClientConfiguration.builder()
.connectionTimeout(15_000)
.readTimeout(15_000)
.proxy(proxy) // optional
.proxyAuthenticator(authenticator) // optional
.certificatePinningConfiguration(pinConfig) // optional
.developmentSslConfig(devSsl) // optional
.build();

HttpClient client = new HttpClientImpl.Builder()
.setConfiguration(config)
.setTlsUpdater(tlsUpdater) // optional – TlsUpdater
.build();
```

Individual setter calls on the builder take precedence over the configuration object.

### Proxy

```java
// Basic auth proxy
HttpProxy proxy = HttpProxy.newBuilder("proxy.example.com", 8080)
.basicAuth("user", "pass")
.build();

// mTLS proxy with custom CA
HttpProxy mtlsProxy = HttpProxy.newBuilder("proxy.example.com", 8443)
.proxyCacert(caCertInputStream)
.mtls(clientCertInputStream, clientKeyInputStream)
.build();

// With a credentials provider (e.g. bearer token)
HttpProxy bearerProxy = HttpProxy.newBuilder("proxy.example.com", 8080)
.credentialsProvider(new BearerCredentialsProvider(tokenSupplier))
.build();
```

### Certificate pinning

```java
CertificatePinningConfiguration pinConfig = CertificatePinningConfiguration.builder()
.addPin("sdk.split.io", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.addPin("*.split.io", certInputStream) // derive pins from a certificate file
.failureListener(failedHost -> {
Log.w("Split", "Certificate pinning failed for " + failedHost);
})
.build();
```

### Development SSL overrides

For test environments where the server uses a self-signed certificate:

```java
DevelopmentSslConfig devSsl = new DevelopmentSslConfig(trustManager, hostnameVerifier);
```

### 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`.

```java
TlsUpdater tlsUpdater = new LegacyTlsUpdaterAdapter(context); // provided by :main
```

## Making requests

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

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

// POST with body and extra headers
HttpRequest post = client.request(uri, HttpMethod.POST, jsonBody, extraHeaders);
HttpResponse resp = post.execute();
Expand All @@ -42,17 +112,13 @@ client.addHeaders(commonHeaders);

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

## 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")
URI uri = new URIBuilder(new URI("https://sdk.split.io/api"), "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')
api project(':http-domain')
api project(':http-api')

testImplementation libs.junit4
testImplementation libs.mockitoCore
Expand Down
Loading
Loading