Skip to content

Commit

Permalink
feat: OkHttp implementation for making HTTP calls and WebSocket conne…
Browse files Browse the repository at this point in the history
…ctions
  • Loading branch information
ttypic committed Sep 27, 2024
1 parent 6f38c17 commit 30b7385
Show file tree
Hide file tree
Showing 19 changed files with 407 additions and 24 deletions.
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
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dexmaker = "1.4"
android-retrostreams = "1.7.4"
maven-publish = "0.29.0"
lombok = "8.10"
okhttp = "4.12.0"

[libraries]
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
Expand All @@ -38,6 +39,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 Down
56 changes: 34 additions & 22 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,42 @@ public class WebSocketTransport implements ITransport {
private String wsUri;
private ConnectListener connectListener;
private WebSocketClient webSocketClient;
private final WebSocketEngine webSocketEngine;

/******************
* 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.isSupportPingListener();

}

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 +107,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
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 isSupportPingListener();
}
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 isSupportPingListener() {
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.ably.lib.network;

import okhttp3.Call;
import okhttp3.Response;

import java.io.IOException;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;

public class OkHttpCall implements HttpCall {
private final Call call;

public OkHttpCall(Call call) {
this.call = call;
}

@Override
public HttpResponse execute() {
try (Response response = call.execute()) {
return HttpResponse.builder()
.headers(response.headers().toMultimap())
.code(response.code())
.message(response.message())
.body(
response.body() != null && response.body().contentType() != null
? new HttpBody(response.body().contentType().toString(), response.body().bytes())
: null
)
.build();

} catch (ConnectException | SocketTimeoutException | UnknownHostException | NoRouteToHostException fce) {
throw new FailedConnectionException(fce);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}

}

@Override
public void cancel() {
call.cancel();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.ably.lib.network;

import okhttp3.Call;
import okhttp3.OkHttpClient;

import java.util.concurrent.TimeUnit;

public class OkHttpEngine implements HttpEngine {

private final OkHttpClient client;
private final HttpEngineConfig config;

public OkHttpEngine(OkHttpClient client, HttpEngineConfig config) {
this.client = client;
this.config = config;
}

@Override
public HttpCall call(HttpRequest request) {
Call call = client.newBuilder()
.connectTimeout(request.getHttpOpenTimeout(), TimeUnit.MILLISECONDS)
.readTimeout(request.getHttpReadTimeout(), TimeUnit.MILLISECONDS)
.build()
.newCall(OkHttpUtils.toOkhttpRequest(request));
return new OkHttpCall(call);
}

@Override
public boolean isUsingProxy() {
return config.getProxy() != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.ably.lib.network;

import okhttp3.OkHttpClient;

public class OkHttpEngineFactory implements HttpEngineFactory {
@Override
public HttpEngine create(HttpEngineConfig config) {
OkHttpClient.Builder connectionBuilder = new OkHttpClient.Builder();
OkHttpUtils.injectProxySetting(config.getProxy(), connectionBuilder);
return new OkHttpEngine(connectionBuilder.build(), config);
}

@Override
public EngineType getEngineType() {
return EngineType.OKHTTP;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.ably.lib.network;

import okhttp3.Credentials;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;

import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.List;
import java.util.Map;

public class OkHttpUtils {
public static void injectProxySetting(ProxyConfig proxyConfig, OkHttpClient.Builder connectionBuilder) {
if (proxyConfig == null) return;
connectionBuilder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyConfig.getHost(), proxyConfig.getPort())));
if (proxyConfig.getUsername() == null || proxyConfig.getAuthType() != ProxyAuthType.BASIC) return;
String username = proxyConfig.getUsername();
String password = proxyConfig.getPassword();
connectionBuilder.proxyAuthenticator((route, response) -> {
String credential = Credentials.basic(username, password);
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
});
}

public static Request toOkhttpRequest(HttpRequest request) {
Request.Builder builder = new Request.Builder()
.url(request.getUrl());

RequestBody body = null;

if (request.getBody() != null) {
body = RequestBody.create(request.getBody().getContent(), MediaType.parse(request.getBody().getContentType()));
}

builder.method(request.getMethod(), body);
for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
String headerName = entry.getKey();
List<String> values = entry.getValue();
for (String headerValue : values) {
builder.addHeader(headerName, headerValue);
}
}

return builder.build();
}
}
Loading

0 comments on commit 30b7385

Please sign in to comment.