Skip to content

Commit b6831e7

Browse files
authored
Merge pull request #1035 from ably/ECO-4208/proxy-support-okhttp
feat: OkHttp implementation for making HTTP calls and WebSocket connections
2 parents 8dfc73a + 1d04fc4 commit b6831e7

File tree

23 files changed

+470
-26
lines changed

23 files changed

+470
-26
lines changed

.github/workflows/integration-test.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,32 @@ jobs:
4949
with:
5050
name: java-build-reports-realtime
5151
path: java/build/reports/
52+
check-rest-okhttp:
53+
runs-on: ubuntu-latest
54+
steps:
55+
- uses: actions/checkout@v3
56+
with:
57+
submodules: 'recursive'
58+
59+
- name: Set up the JDK
60+
uses: actions/setup-java@v3
61+
with:
62+
java-version: '17'
63+
distribution: 'temurin'
64+
65+
- run: ./gradlew :java:testRestSuite -Pokhttp
66+
67+
check-realtime-okhttp:
68+
runs-on: ubuntu-latest
69+
steps:
70+
- uses: actions/checkout@v3
71+
with:
72+
submodules: 'recursive'
73+
74+
- name: Set up the JDK
75+
uses: actions/setup-java@v3
76+
with:
77+
java-version: '17'
78+
distribution: 'temurin'
79+
80+
- run: ./gradlew :java:testRealtimeSuite -Pokhttp

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,67 @@ realtime.setAndroidContext(context);
500500
realtime.push.activate();
501501
```
502502

503+
## Using Ably SDK Under a Proxy
504+
505+
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.
506+
507+
### Add the Required Dependency
508+
509+
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.
510+
511+
Add the following dependency to your `build.gradle` file:
512+
513+
```groovy
514+
dependencies {
515+
runtimeOnly("io.ably:network-client-okhttp:1.2.43")
516+
}
517+
```
518+
519+
### Configure Proxy Settings
520+
521+
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.
522+
523+
Here’s an example of how to configure and use a proxy:
524+
525+
#### Java Example
526+
527+
```java
528+
import io.ably.lib.realtime.AblyRealtime;
529+
import io.ably.lib.rest.AblyRest;
530+
import io.ably.lib.transport.Defaults;
531+
import io.ably.lib.types.ClientOptions;
532+
import io.ably.lib.types.ProxyOptions;
533+
import io.ably.lib.http.HttpAuth;
534+
535+
public class AblyWithProxy {
536+
public static void main(String[] args) throws Exception {
537+
// Configure Ably Client options
538+
ClientOptions options = new ClientOptions();
539+
540+
// Setup proxy settings
541+
ProxyOptions proxy = new ProxyOptions();
542+
proxy.host = "your-proxy-host"; // Replace with your proxy host
543+
proxy.port = 8080; // Replace with your proxy port
544+
545+
// Optional: If the proxy requires authentication
546+
proxy.username = "your-username"; // Replace with proxy username
547+
proxy.password = "your-password"; // Replace with proxy password
548+
proxy.prefAuthType = HttpAuth.Type.BASIC; // Choose your preferred authentication type (e.g., BASIC or DIGEST)
549+
550+
// Attach the proxy settings to the client options
551+
options.proxy = proxy;
552+
553+
// Create an instance of Ably using the configured options
554+
AblyRest ably = new AblyRest(options);
555+
556+
// Alternatively, for real-time connections
557+
AblyRealtime ablyRealtime = new AblyRealtime(options);
558+
559+
// Use the Ably client as usual
560+
}
561+
}
562+
```
563+
503564
## Resources
504565

505566
Visit https://www.ably.com/docs for a complete API reference and more examples.

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ plugins {
77
alias(libs.plugins.android.library) apply false
88
alias(libs.plugins.maven.publish) apply false
99
alias(libs.plugins.lombok) apply false
10+
alias(libs.plugins.test.retry) apply false
1011
}
1112

1213
subprojects {

gradle/libs.versions.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ dexmaker = "1.4"
1717
android-retrostreams = "1.7.4"
1818
maven-publish = "0.29.0"
1919
lombok = "8.10"
20+
okhttp = "4.12.0"
21+
test-retry = "1.6.0"
2022

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

4245
[bundles]
4346
common = ["msgpack", "vcdiff-core"]
@@ -49,3 +52,4 @@ android-library = { id = "com.android.library", version.ref = "agp" }
4952
build-config = { id = "com.github.gmazzo.buildconfig", version.ref = "build-config" }
5053
maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" }
5154
lombok = { id = "io.freefair.lombok", version.ref = "lombok" }
55+
test-retry = { id = "org.gradle.test-retry", version.ref = "test-retry" }

java/build.gradle.kts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat
33
plugins {
44
alias(libs.plugins.build.config)
55
alias(libs.plugins.maven.publish)
6+
alias(libs.plugins.test.retry)
67
checkstyle
78
`java-library`
89
}
@@ -20,7 +21,11 @@ dependencies {
2021
api(libs.gson)
2122
implementation(libs.bundles.common)
2223
implementation(project(":network-client-core"))
23-
runtimeOnly(project(":network-client-default"))
24+
if (findProperty("okhttp") == null) {
25+
runtimeOnly(project(":network-client-default"))
26+
} else {
27+
runtimeOnly(project(":network-client-okhttp"))
28+
}
2429
testImplementation(libs.bundles.tests)
2530
}
2631

@@ -59,6 +64,12 @@ tasks.register<Test>("testRealtimeSuite") {
5964
testLogging {
6065
exceptionFormat = TestExceptionFormat.FULL
6166
}
67+
retry {
68+
maxRetries.set(3)
69+
maxFailures.set(8)
70+
failOnPassedAfterRetry.set(false)
71+
failOnSkippedAfterRetry.set(false)
72+
}
6273
}
6374

6475
tasks.register<Test>("testRestSuite") {
@@ -72,6 +83,12 @@ tasks.register<Test>("testRestSuite") {
7283
testLogging {
7384
exceptionFormat = TestExceptionFormat.FULL
7485
}
86+
retry {
87+
maxRetries.set(3)
88+
maxFailures.set(8)
89+
failOnPassedAfterRetry.set(false)
90+
failOnSkippedAfterRetry.set(false)
91+
}
7592
}
7693

7794
/*

lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package io.ably.lib.transport;
22

33
import io.ably.lib.http.HttpUtils;
4+
import io.ably.lib.network.EngineType;
5+
import io.ably.lib.network.NotConnectedException;
46
import io.ably.lib.network.WebSocketClient;
57
import io.ably.lib.network.WebSocketEngine;
68
import io.ably.lib.network.WebSocketEngineConfig;
79
import io.ably.lib.network.WebSocketEngineFactory;
810
import io.ably.lib.network.WebSocketListener;
9-
import io.ably.lib.network.NotConnectedException;
1011
import io.ably.lib.types.AblyException;
1112
import io.ably.lib.types.ErrorInfo;
1213
import io.ably.lib.types.Param;
@@ -17,6 +18,8 @@
1718

1819
import javax.net.ssl.SSLContext;
1920
import java.nio.ByteBuffer;
21+
import java.security.KeyManagementException;
22+
import java.security.NoSuchAlgorithmException;
2023
import java.util.Timer;
2124
import java.util.TimerTask;
2225

@@ -48,16 +51,43 @@ public class WebSocketTransport implements ITransport {
4851
private String wsUri;
4952
private ConnectListener connectListener;
5053
private WebSocketClient webSocketClient;
54+
private final WebSocketEngine webSocketEngine;
55+
private boolean activityCheckTurnedOff = false;
56+
5157
/******************
5258
* protected constructor
5359
******************/
54-
5560
protected WebSocketTransport(TransportParams params, ConnectionManager connectionManager) {
5661
this.params = params;
5762
this.connectionManager = connectionManager;
5863
this.channelBinaryMode = params.options.useBinaryProtocol;
59-
/* We do not require Ably heartbeats, as we can use WebSocket pings instead. */
60-
params.heartbeats = false;
64+
this.webSocketEngine = createWebSocketEngine(params);
65+
params.heartbeats = !this.webSocketEngine.isPingListenerSupported();
66+
67+
}
68+
69+
private static WebSocketEngine createWebSocketEngine(TransportParams params) {
70+
WebSocketEngineFactory engineFactory = WebSocketEngineFactory.getFirstAvailable();
71+
Log.v(TAG, String.format("Using %s WebSocket Engine", engineFactory.getEngineType().name()));
72+
WebSocketEngineConfig.WebSocketEngineConfigBuilder configBuilder = WebSocketEngineConfig.builder();
73+
configBuilder
74+
.tls(params.options.tls)
75+
.host(params.host)
76+
.proxy(ClientOptionsUtils.convertToProxyConfig(params.getClientOptions()));
77+
78+
// OkHttp supports modern TLS algorithms by default
79+
if (params.options.tls && engineFactory.getEngineType() != EngineType.OKHTTP) {
80+
try {
81+
SSLContext sslContext = SSLContext.getInstance("TLS");
82+
sslContext.init(null, null, null);
83+
SafeSSLSocketFactory factory = new SafeSSLSocketFactory(sslContext.getSocketFactory());
84+
configBuilder.sslSocketFactory(factory);
85+
} catch (NoSuchAlgorithmException | KeyManagementException e) {
86+
throw new IllegalStateException("Can't get safe tls algorithms", e);
87+
}
88+
}
89+
90+
return engineFactory.create(configBuilder.build());
6191
}
6292

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

79109
Log.d(TAG, "connect(); wsUri = " + wsUri);
80110
synchronized (this) {
81-
WebSocketEngineFactory engineFactory = WebSocketEngineFactory.getFirstAvailable();
82-
Log.v(TAG, String.format("Using %s WebSocket Engine", engineFactory.getEngineType().name()));
83-
84-
WebSocketEngineConfig.WebSocketEngineConfigBuilder configBuilder = WebSocketEngineConfig.builder();
85-
configBuilder
86-
.tls(isTls)
87-
.host(params.host)
88-
.proxy(ClientOptionsUtils.convertToProxyConfig(params.getClientOptions()));
89-
90-
if (isTls) {
91-
SSLContext sslContext = SSLContext.getInstance("TLS");
92-
sslContext.init(null, null, null);
93-
SafeSSLSocketFactory factory = new SafeSSLSocketFactory(sslContext.getSocketFactory());
94-
configBuilder.sslSocketFactory(factory);
95-
}
96-
97-
WebSocketEngine engine = engineFactory.create(configBuilder.build());
98-
webSocketClient = engine.create(wsUri, new WebSocketHandler(this::receive));
111+
webSocketClient = this.webSocketEngine.create(wsUri, new WebSocketHandler(this::receive));
99112
}
100113
webSocketClient.connect();
101114
} catch (AblyException e) {
@@ -161,6 +174,16 @@ protected void preProcessReceivedMessage(ProtocolMessage message) {
161174
//Gives the chance to child classes to do message pre-processing
162175
}
163176

177+
/**
178+
* Visible For Testing
179+
* </p>
180+
* 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])
181+
* Those tests expects that activity checks are passing, but protocol messages are not coming
182+
*/
183+
protected void turnOffActivityCheckIfPingListenerIsNotSupported() {
184+
if (!webSocketEngine.isPingListenerSupported()) activityCheckTurnedOff = true;
185+
}
186+
164187
public String toString() {
165188
return WebSocketTransport.class.getName() + " {" + getURL() + "}";
166189
}
@@ -307,7 +330,7 @@ private synchronized void dispose() {
307330
private synchronized void flagActivity() {
308331
lastActivityTime = System.currentTimeMillis();
309332
connectionManager.setLastActivity(lastActivityTime);
310-
if (activityTimerTask == null && connectionManager.maxIdleInterval != 0) {
333+
if (activityTimerTask == null && connectionManager.maxIdleInterval != 0 && !activityCheckTurnedOff) {
311334
/* No timer currently running because previously there was no
312335
* maxIdleInterval configured, but now there is a
313336
* maxIdleInterval configured. Call checkActivity so a timer

lib/src/test/java/io/ably/lib/test/util/MockWebsocketFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ private MockWebsocketTransport(TransportParams givenTransportParams, TransportPa
155155
super(transformedTransportParams, connectionManager);
156156
this.givenTransportParams = givenTransportParams;
157157
this.transformedTransportParams = transformedTransportParams;
158+
turnOffActivityCheckIfPingListenerIsNotSupported();
158159
}
159160

160161
public List<ProtocolMessage> getSentMessages() {

network-client-core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins {
22
`java-library`
33
alias(libs.plugins.lombok)
4+
alias(libs.plugins.maven.publish)
45
}
56

67
java {

network-client-core/gradle.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
POM_ARTIFACT_ID=network-client-core
2+
POM_NAME=Core HTTP client abstraction
3+
POM_DESCRIPTION=Core HTTP client abstraction
4+
POM_PACKAGING=jar

network-client-core/src/main/java/io/ably/lib/network/WebSocketEngine.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
*/
66
public interface WebSocketEngine {
77
WebSocketClient create(String url, WebSocketListener listener);
8+
boolean isPingListenerSupported();
89
}

network-client-core/src/main/java/io/ably/lib/network/WebSocketEngineFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ static WebSocketEngineFactory getFirstAvailable() {
1919

2020
static WebSocketEngineFactory tryGetOkWebSocketFactory() {
2121
try {
22-
Class<?> okWebSocketFactoryClass = Class.forName("io.ably.lib.network.OkWebSocketEngineFactory");
22+
Class<?> okWebSocketFactoryClass = Class.forName("io.ably.lib.network.OkHttpWebSocketEngineFactory");
2323
return (WebSocketEngineFactory) okWebSocketFactoryClass.getDeclaredConstructor().newInstance();
2424
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException |
2525
InvocationTargetException e) {

network-client-default/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ java {
1010
}
1111

1212
dependencies {
13-
api(project(":network-client-core"))
13+
implementation(project(":network-client-core"))
1414
implementation(libs.java.websocket)
1515
}

network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketEngine.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ public WebSocketClient create(String url, WebSocketListener listener) {
1717
}
1818
return client;
1919
}
20+
21+
@Override
22+
public boolean isPingListenerSupported() {
23+
return true;
24+
}
2025
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
plugins {
2+
`java-library`
3+
alias(libs.plugins.lombok)
4+
alias(libs.plugins.maven.publish)
5+
}
6+
7+
java {
8+
sourceCompatibility = JavaVersion.VERSION_1_8
9+
targetCompatibility = JavaVersion.VERSION_1_8
10+
}
11+
12+
dependencies {
13+
implementation(project(":network-client-core"))
14+
implementation(libs.okhttp)
15+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
POM_ARTIFACT_ID=network-client-okhttp
2+
POM_NAME=Default HTTP client
3+
POM_DESCRIPTION=Default implementation for HTTP client
4+
POM_PACKAGING=jar

0 commit comments

Comments
 (0)