Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: OkHttp implementation for making HTTP calls and WebSocket connections #1035

Merged
merged 5 commits into from
Oct 7, 2024
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
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);
}
}
ttypic marked this conversation as resolved.
Show resolved Hide resolved
ttypic marked this conversation as resolved.
Show resolved Hide resolved

return engineFactory.create(configBuilder.build());
ttypic marked this conversation as resolved.
Show resolved Hide resolved
}

/******************
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));
ttypic marked this conversation as resolved.
Show resolved Hide resolved
}
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 @@ -2,4 +2,5 @@

public interface WebSocketEngine {
WebSocketClient create(String url, WebSocketListener listener);
boolean isPingListenerSupported();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,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
Loading