Skip to content

Commit 30b7385

Browse files
committed
feat: OkHttp implementation for making HTTP calls and WebSocket connections
1 parent 6f38c17 commit 30b7385

File tree

19 files changed

+407
-24
lines changed

19 files changed

+407
-24
lines changed

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.

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dexmaker = "1.4"
1717
android-retrostreams = "1.7.4"
1818
maven-publish = "0.29.0"
1919
lombok = "8.10"
20+
okhttp = "4.12.0"
2021

2122
[libraries]
2223
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
@@ -38,6 +39,7 @@ dexmaker = { group = "com.crittercism.dexmaker", name = "dexmaker", version.ref
3839
dexmaker-dx = { group = "com.crittercism.dexmaker", name = "dexmaker-dx", version.ref = "dexmaker" }
3940
dexmaker-mockito = { group = "com.crittercism.dexmaker", name = "dexmaker-mockito", version.ref = "dexmaker" }
4041
android-retrostreams = { group = "net.sourceforge.streamsupport", name = "android-retrostreams", version.ref = "android-retrostreams" }
42+
okhttp = { group ="com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
4143

4244
[bundles]
4345
common = ["msgpack", "vcdiff-core"]

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

Lines changed: 34 additions & 22 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,42 @@ public class WebSocketTransport implements ITransport {
4851
private String wsUri;
4952
private ConnectListener connectListener;
5053
private WebSocketClient webSocketClient;
54+
private final WebSocketEngine webSocketEngine;
55+
5156
/******************
5257
* protected constructor
5358
******************/
54-
5559
protected WebSocketTransport(TransportParams params, ConnectionManager connectionManager) {
5660
this.params = params;
5761
this.connectionManager = connectionManager;
5862
this.channelBinaryMode = params.options.useBinaryProtocol;
59-
/* We do not require Ably heartbeats, as we can use WebSocket pings instead. */
60-
params.heartbeats = false;
63+
this.webSocketEngine = createWebSocketEngine(params);
64+
params.heartbeats = !this.webSocketEngine.isSupportPingListener();
65+
66+
}
67+
68+
private static WebSocketEngine createWebSocketEngine(TransportParams params) {
69+
WebSocketEngineFactory engineFactory = WebSocketEngineFactory.getFirstAvailable();
70+
Log.v(TAG, String.format("Using %s WebSocket Engine", engineFactory.getEngineType().name()));
71+
WebSocketEngineConfig.WebSocketEngineConfigBuilder configBuilder = WebSocketEngineConfig.builder();
72+
configBuilder
73+
.tls(params.options.tls)
74+
.host(params.host)
75+
.proxy(ClientOptionsUtils.convertToProxyConfig(params.getClientOptions()));
76+
77+
// OkHttp supports modern TLS algorithms by default
78+
if (params.options.tls && engineFactory.getEngineType() != EngineType.OKHTTP) {
79+
try {
80+
SSLContext sslContext = SSLContext.getInstance("TLS");
81+
sslContext.init(null, null, null);
82+
SafeSSLSocketFactory factory = new SafeSSLSocketFactory(sslContext.getSocketFactory());
83+
configBuilder.sslSocketFactory(factory);
84+
} catch (NoSuchAlgorithmException | KeyManagementException e) {
85+
throw new IllegalStateException("Can't get safe tls algorithms", e);
86+
}
87+
}
88+
89+
return engineFactory.create(configBuilder.build());
6190
}
6291

6392
/******************
@@ -78,24 +107,7 @@ public void connect(ConnectListener connectListener) {
78107

79108
Log.d(TAG, "connect(); wsUri = " + wsUri);
80109
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));
110+
webSocketClient = this.webSocketEngine.create(wsUri, new WebSocketHandler(this::receive));
99111
}
100112
webSocketClient.connect();
101113
} catch (AblyException e) {

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
@@ -2,4 +2,5 @@
22

33
public interface WebSocketEngine {
44
WebSocketClient create(String url, WebSocketListener listener);
5+
boolean isSupportPingListener();
56
}

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
@@ -16,7 +16,7 @@ static WebSocketEngineFactory getFirstAvailable() {
1616

1717
static WebSocketEngineFactory tryGetOkWebSocketFactory() {
1818
try {
19-
Class<?> okWebSocketFactoryClass = Class.forName("io.ably.lib.network.OkWebSocketEngineFactory");
19+
Class<?> okWebSocketFactoryClass = Class.forName("io.ably.lib.network.OkHttpWebSocketEngineFactory");
2020
return (WebSocketEngineFactory) okWebSocketFactoryClass.getDeclaredConstructor().newInstance();
2121
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException |
2222
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 isSupportPingListener() {
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
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.ably.lib.network;
2+
3+
import okhttp3.Call;
4+
import okhttp3.Response;
5+
6+
import java.io.IOException;
7+
import java.net.ConnectException;
8+
import java.net.NoRouteToHostException;
9+
import java.net.SocketTimeoutException;
10+
import java.net.UnknownHostException;
11+
12+
public class OkHttpCall implements HttpCall {
13+
private final Call call;
14+
15+
public OkHttpCall(Call call) {
16+
this.call = call;
17+
}
18+
19+
@Override
20+
public HttpResponse execute() {
21+
try (Response response = call.execute()) {
22+
return HttpResponse.builder()
23+
.headers(response.headers().toMultimap())
24+
.code(response.code())
25+
.message(response.message())
26+
.body(
27+
response.body() != null && response.body().contentType() != null
28+
? new HttpBody(response.body().contentType().toString(), response.body().bytes())
29+
: null
30+
)
31+
.build();
32+
33+
} catch (ConnectException | SocketTimeoutException | UnknownHostException | NoRouteToHostException fce) {
34+
throw new FailedConnectionException(fce);
35+
} catch (IOException ioe) {
36+
throw new RuntimeException(ioe);
37+
}
38+
39+
}
40+
41+
@Override
42+
public void cancel() {
43+
call.cancel();
44+
}
45+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.ably.lib.network;
2+
3+
import okhttp3.Call;
4+
import okhttp3.OkHttpClient;
5+
6+
import java.util.concurrent.TimeUnit;
7+
8+
public class OkHttpEngine implements HttpEngine {
9+
10+
private final OkHttpClient client;
11+
private final HttpEngineConfig config;
12+
13+
public OkHttpEngine(OkHttpClient client, HttpEngineConfig config) {
14+
this.client = client;
15+
this.config = config;
16+
}
17+
18+
@Override
19+
public HttpCall call(HttpRequest request) {
20+
Call call = client.newBuilder()
21+
.connectTimeout(request.getHttpOpenTimeout(), TimeUnit.MILLISECONDS)
22+
.readTimeout(request.getHttpReadTimeout(), TimeUnit.MILLISECONDS)
23+
.build()
24+
.newCall(OkHttpUtils.toOkhttpRequest(request));
25+
return new OkHttpCall(call);
26+
}
27+
28+
@Override
29+
public boolean isUsingProxy() {
30+
return config.getProxy() != null;
31+
}
32+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.ably.lib.network;
2+
3+
import okhttp3.OkHttpClient;
4+
5+
public class OkHttpEngineFactory implements HttpEngineFactory {
6+
@Override
7+
public HttpEngine create(HttpEngineConfig config) {
8+
OkHttpClient.Builder connectionBuilder = new OkHttpClient.Builder();
9+
OkHttpUtils.injectProxySetting(config.getProxy(), connectionBuilder);
10+
return new OkHttpEngine(connectionBuilder.build(), config);
11+
}
12+
13+
@Override
14+
public EngineType getEngineType() {
15+
return EngineType.OKHTTP;
16+
}
17+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.ably.lib.network;
2+
3+
import okhttp3.Credentials;
4+
import okhttp3.Headers;
5+
import okhttp3.MediaType;
6+
import okhttp3.OkHttpClient;
7+
import okhttp3.Request;
8+
import okhttp3.RequestBody;
9+
10+
import java.net.InetSocketAddress;
11+
import java.net.Proxy;
12+
import java.util.List;
13+
import java.util.Map;
14+
15+
public class OkHttpUtils {
16+
public static void injectProxySetting(ProxyConfig proxyConfig, OkHttpClient.Builder connectionBuilder) {
17+
if (proxyConfig == null) return;
18+
connectionBuilder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyConfig.getHost(), proxyConfig.getPort())));
19+
if (proxyConfig.getUsername() == null || proxyConfig.getAuthType() != ProxyAuthType.BASIC) return;
20+
String username = proxyConfig.getUsername();
21+
String password = proxyConfig.getPassword();
22+
connectionBuilder.proxyAuthenticator((route, response) -> {
23+
String credential = Credentials.basic(username, password);
24+
return response.request().newBuilder()
25+
.header("Proxy-Authorization", credential)
26+
.build();
27+
});
28+
}
29+
30+
public static Request toOkhttpRequest(HttpRequest request) {
31+
Request.Builder builder = new Request.Builder()
32+
.url(request.getUrl());
33+
34+
RequestBody body = null;
35+
36+
if (request.getBody() != null) {
37+
body = RequestBody.create(request.getBody().getContent(), MediaType.parse(request.getBody().getContentType()));
38+
}
39+
40+
builder.method(request.getMethod(), body);
41+
for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
42+
String headerName = entry.getKey();
43+
List<String> values = entry.getValue();
44+
for (String headerValue : values) {
45+
builder.addHeader(headerName, headerValue);
46+
}
47+
}
48+
49+
return builder.build();
50+
}
51+
}

0 commit comments

Comments
 (0)