Skip to content

Commit

Permalink
Merge pull request #1035 from ably/ECO-4208/proxy-support-okhttp
Browse files Browse the repository at this point in the history
feat: OkHttp implementation for making HTTP calls and WebSocket connections
  • Loading branch information
ttypic authored Oct 7, 2024
2 parents 8dfc73a + 1d04fc4 commit b6831e7
Show file tree
Hide file tree
Showing 23 changed files with 470 additions and 26 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,32 @@ jobs:
with:
name: java-build-reports-realtime
path: java/build/reports/
check-rest-okhttp:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: 'recursive'

- name: Set up the JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'

- run: ./gradlew :java:testRestSuite -Pokhttp

check-realtime-okhttp:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: 'recursive'

- name: Set up the JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'

- run: ./gradlew :java:testRealtimeSuite -Pokhttp
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,67 @@ realtime.setAndroidContext(context);
realtime.push.activate();
```

## Using Ably SDK Under a Proxy

When working in environments where outbound internet access is restricted, such as behind a corporate proxy, the Ably SDK allows you to configure a proxy server for HTTP and WebSocket connections.

### Add the Required Dependency

You need to use **OkHttp** library for making HTTP calls and WebSocket connections in the Ably SDK to get proxy support both for your Rest and Realtime clients.

Add the following dependency to your `build.gradle` file:

```groovy
dependencies {
runtimeOnly("io.ably:network-client-okhttp:1.2.43")
}
```

### Configure Proxy Settings

After adding the required OkHttp dependency, you need to configure the proxy settings for your Ably client. This can be done by setting the proxy options in the `ClientOptions` object when you instantiate the Ably SDK.

Here’s an example of how to configure and use a proxy:

#### Java Example

```java
import io.ably.lib.realtime.AblyRealtime;
import io.ably.lib.rest.AblyRest;
import io.ably.lib.transport.Defaults;
import io.ably.lib.types.ClientOptions;
import io.ably.lib.types.ProxyOptions;
import io.ably.lib.http.HttpAuth;

public class AblyWithProxy {
public static void main(String[] args) throws Exception {
// Configure Ably Client options
ClientOptions options = new ClientOptions();

// Setup proxy settings
ProxyOptions proxy = new ProxyOptions();
proxy.host = "your-proxy-host"; // Replace with your proxy host
proxy.port = 8080; // Replace with your proxy port

// Optional: If the proxy requires authentication
proxy.username = "your-username"; // Replace with proxy username
proxy.password = "your-password"; // Replace with proxy password
proxy.prefAuthType = HttpAuth.Type.BASIC; // Choose your preferred authentication type (e.g., BASIC or DIGEST)

// Attach the proxy settings to the client options
options.proxy = proxy;

// Create an instance of Ably using the configured options
AblyRest ably = new AblyRest(options);

// Alternatively, for real-time connections
AblyRealtime ablyRealtime = new AblyRealtime(options);

// Use the Ably client as usual
}
}
```

## Resources

Visit https://www.ably.com/docs for a complete API reference and more examples.
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.android.library) apply false
alias(libs.plugins.maven.publish) apply false
alias(libs.plugins.lombok) apply false
alias(libs.plugins.test.retry) apply false
}

subprojects {
Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ dexmaker = "1.4"
android-retrostreams = "1.7.4"
maven-publish = "0.29.0"
lombok = "8.10"
okhttp = "4.12.0"
test-retry = "1.6.0"

[libraries]
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
Expand All @@ -38,6 +40,7 @@ dexmaker = { group = "com.crittercism.dexmaker", name = "dexmaker", version.ref
dexmaker-dx = { group = "com.crittercism.dexmaker", name = "dexmaker-dx", version.ref = "dexmaker" }
dexmaker-mockito = { group = "com.crittercism.dexmaker", name = "dexmaker-mockito", version.ref = "dexmaker" }
android-retrostreams = { group = "net.sourceforge.streamsupport", name = "android-retrostreams", version.ref = "android-retrostreams" }
okhttp = { group ="com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }

[bundles]
common = ["msgpack", "vcdiff-core"]
Expand All @@ -49,3 +52,4 @@ android-library = { id = "com.android.library", version.ref = "agp" }
build-config = { id = "com.github.gmazzo.buildconfig", version.ref = "build-config" }
maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" }
lombok = { id = "io.freefair.lombok", version.ref = "lombok" }
test-retry = { id = "org.gradle.test-retry", version.ref = "test-retry" }
19 changes: 18 additions & 1 deletion java/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat
plugins {
alias(libs.plugins.build.config)
alias(libs.plugins.maven.publish)
alias(libs.plugins.test.retry)
checkstyle
`java-library`
}
Expand All @@ -20,7 +21,11 @@ dependencies {
api(libs.gson)
implementation(libs.bundles.common)
implementation(project(":network-client-core"))
runtimeOnly(project(":network-client-default"))
if (findProperty("okhttp") == null) {
runtimeOnly(project(":network-client-default"))
} else {
runtimeOnly(project(":network-client-okhttp"))
}
testImplementation(libs.bundles.tests)
}

Expand Down Expand Up @@ -59,6 +64,12 @@ tasks.register<Test>("testRealtimeSuite") {
testLogging {
exceptionFormat = TestExceptionFormat.FULL
}
retry {
maxRetries.set(3)
maxFailures.set(8)
failOnPassedAfterRetry.set(false)
failOnSkippedAfterRetry.set(false)
}
}

tasks.register<Test>("testRestSuite") {
Expand All @@ -72,6 +83,12 @@ tasks.register<Test>("testRestSuite") {
testLogging {
exceptionFormat = TestExceptionFormat.FULL
}
retry {
maxRetries.set(3)
maxFailures.set(8)
failOnPassedAfterRetry.set(false)
failOnSkippedAfterRetry.set(false)
}
}

/*
Expand Down
69 changes: 46 additions & 23 deletions lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package io.ably.lib.transport;

import io.ably.lib.http.HttpUtils;
import io.ably.lib.network.EngineType;
import io.ably.lib.network.NotConnectedException;
import io.ably.lib.network.WebSocketClient;
import io.ably.lib.network.WebSocketEngine;
import io.ably.lib.network.WebSocketEngineConfig;
import io.ably.lib.network.WebSocketEngineFactory;
import io.ably.lib.network.WebSocketListener;
import io.ably.lib.network.NotConnectedException;
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ErrorInfo;
import io.ably.lib.types.Param;
Expand All @@ -17,6 +18,8 @@

import javax.net.ssl.SSLContext;
import java.nio.ByteBuffer;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Timer;
import java.util.TimerTask;

Expand Down Expand Up @@ -48,16 +51,43 @@ public class WebSocketTransport implements ITransport {
private String wsUri;
private ConnectListener connectListener;
private WebSocketClient webSocketClient;
private final WebSocketEngine webSocketEngine;
private boolean activityCheckTurnedOff = false;

/******************
* protected constructor
******************/

protected WebSocketTransport(TransportParams params, ConnectionManager connectionManager) {
this.params = params;
this.connectionManager = connectionManager;
this.channelBinaryMode = params.options.useBinaryProtocol;
/* We do not require Ably heartbeats, as we can use WebSocket pings instead. */
params.heartbeats = false;
this.webSocketEngine = createWebSocketEngine(params);
params.heartbeats = !this.webSocketEngine.isPingListenerSupported();

}

private static WebSocketEngine createWebSocketEngine(TransportParams params) {
WebSocketEngineFactory engineFactory = WebSocketEngineFactory.getFirstAvailable();
Log.v(TAG, String.format("Using %s WebSocket Engine", engineFactory.getEngineType().name()));
WebSocketEngineConfig.WebSocketEngineConfigBuilder configBuilder = WebSocketEngineConfig.builder();
configBuilder
.tls(params.options.tls)
.host(params.host)
.proxy(ClientOptionsUtils.convertToProxyConfig(params.getClientOptions()));

// OkHttp supports modern TLS algorithms by default
if (params.options.tls && engineFactory.getEngineType() != EngineType.OKHTTP) {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, null, null);
SafeSSLSocketFactory factory = new SafeSSLSocketFactory(sslContext.getSocketFactory());
configBuilder.sslSocketFactory(factory);
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new IllegalStateException("Can't get safe tls algorithms", e);
}
}

return engineFactory.create(configBuilder.build());
}

/******************
Expand All @@ -78,24 +108,7 @@ public void connect(ConnectListener connectListener) {

Log.d(TAG, "connect(); wsUri = " + wsUri);
synchronized (this) {
WebSocketEngineFactory engineFactory = WebSocketEngineFactory.getFirstAvailable();
Log.v(TAG, String.format("Using %s WebSocket Engine", engineFactory.getEngineType().name()));

WebSocketEngineConfig.WebSocketEngineConfigBuilder configBuilder = WebSocketEngineConfig.builder();
configBuilder
.tls(isTls)
.host(params.host)
.proxy(ClientOptionsUtils.convertToProxyConfig(params.getClientOptions()));

if (isTls) {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, null, null);
SafeSSLSocketFactory factory = new SafeSSLSocketFactory(sslContext.getSocketFactory());
configBuilder.sslSocketFactory(factory);
}

WebSocketEngine engine = engineFactory.create(configBuilder.build());
webSocketClient = engine.create(wsUri, new WebSocketHandler(this::receive));
webSocketClient = this.webSocketEngine.create(wsUri, new WebSocketHandler(this::receive));
}
webSocketClient.connect();
} catch (AblyException e) {
Expand Down Expand Up @@ -161,6 +174,16 @@ protected void preProcessReceivedMessage(ProtocolMessage message) {
//Gives the chance to child classes to do message pre-processing
}

/**
* Visible For Testing
* </p>
* We need to turn off activity check for some tests (e.g. io.ably.lib.test.realtime.RealtimeConnectFailTest.disconnect_retry_channel_timeout_jitter_after_consistent_detach[binary_protocol])
* Those tests expects that activity checks are passing, but protocol messages are not coming
*/
protected void turnOffActivityCheckIfPingListenerIsNotSupported() {
if (!webSocketEngine.isPingListenerSupported()) activityCheckTurnedOff = true;
}

public String toString() {
return WebSocketTransport.class.getName() + " {" + getURL() + "}";
}
Expand Down Expand Up @@ -307,7 +330,7 @@ private synchronized void dispose() {
private synchronized void flagActivity() {
lastActivityTime = System.currentTimeMillis();
connectionManager.setLastActivity(lastActivityTime);
if (activityTimerTask == null && connectionManager.maxIdleInterval != 0) {
if (activityTimerTask == null && connectionManager.maxIdleInterval != 0 && !activityCheckTurnedOff) {
/* No timer currently running because previously there was no
* maxIdleInterval configured, but now there is a
* maxIdleInterval configured. Call checkActivity so a timer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ private MockWebsocketTransport(TransportParams givenTransportParams, TransportPa
super(transformedTransportParams, connectionManager);
this.givenTransportParams = givenTransportParams;
this.transformedTransportParams = transformedTransportParams;
turnOffActivityCheckIfPingListenerIsNotSupported();
}

public List<ProtocolMessage> getSentMessages() {
Expand Down
1 change: 1 addition & 0 deletions network-client-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
`java-library`
alias(libs.plugins.lombok)
alias(libs.plugins.maven.publish)
}

java {
Expand Down
4 changes: 4 additions & 0 deletions network-client-core/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
POM_ARTIFACT_ID=network-client-core
POM_NAME=Core HTTP client abstraction
POM_DESCRIPTION=Core HTTP client abstraction
POM_PACKAGING=jar
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
*/
public interface WebSocketEngine {
WebSocketClient create(String url, WebSocketListener listener);
boolean isPingListenerSupported();
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ static WebSocketEngineFactory getFirstAvailable() {

static WebSocketEngineFactory tryGetOkWebSocketFactory() {
try {
Class<?> okWebSocketFactoryClass = Class.forName("io.ably.lib.network.OkWebSocketEngineFactory");
Class<?> okWebSocketFactoryClass = Class.forName("io.ably.lib.network.OkHttpWebSocketEngineFactory");
return (WebSocketEngineFactory) okWebSocketFactoryClass.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException |
InvocationTargetException e) {
Expand Down
2 changes: 1 addition & 1 deletion network-client-default/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ java {
}

dependencies {
api(project(":network-client-core"))
implementation(project(":network-client-core"))
implementation(libs.java.websocket)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ public WebSocketClient create(String url, WebSocketListener listener) {
}
return client;
}

@Override
public boolean isPingListenerSupported() {
return true;
}
}
15 changes: 15 additions & 0 deletions network-client-okhttp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
plugins {
`java-library`
alias(libs.plugins.lombok)
alias(libs.plugins.maven.publish)
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

dependencies {
implementation(project(":network-client-core"))
implementation(libs.okhttp)
}
4 changes: 4 additions & 0 deletions network-client-okhttp/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
POM_ARTIFACT_ID=network-client-okhttp
POM_NAME=Default HTTP client
POM_DESCRIPTION=Default implementation for HTTP client
POM_PACKAGING=jar
Loading

0 comments on commit b6831e7

Please sign in to comment.