Skip to content

Commit 6f38c17

Browse files
committed
refactor: decouple HTTP and WebSocket engines
- Extracted HTTP calls and WebSocket listeners into a separate module. - Introduced an abstraction layer for easier implementation swapping.
1 parent 7d70cd9 commit 6f38c17

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+990
-333
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ bin/
88
.project
99

1010
local.properties
11+
12+
lombok.config

android/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ dependencies {
5252
api(libs.gson)
5353
implementation(libs.bundles.common)
5454
testImplementation(libs.bundles.tests)
55+
implementation(project(":network-client-core"))
56+
runtimeOnly(project(":network-client-default"))
5557
implementation(libs.firebase.messaging)
5658
androidTestImplementation(libs.bundles.instrumental.android)
5759
}

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.vanniktech.maven.publish.SonatypeHost
66
plugins {
77
alias(libs.plugins.android.library) apply false
88
alias(libs.plugins.maven.publish) apply false
9+
alias(libs.plugins.lombok) apply false
910
}
1011

1112
subprojects {

gradle/libs.versions.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ android-test = "0.5"
1616
dexmaker = "1.4"
1717
android-retrostreams = "1.7.4"
1818
maven-publish = "0.29.0"
19+
lombok = "8.10"
1920

2021
[libraries]
2122
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
@@ -39,11 +40,12 @@ dexmaker-mockito = { group = "com.crittercism.dexmaker", name = "dexmaker-mockit
3940
android-retrostreams = { group = "net.sourceforge.streamsupport", name = "android-retrostreams", version.ref = "android-retrostreams" }
4041

4142
[bundles]
42-
common = ["msgpack", "java-websocket", "vcdiff-core"]
43+
common = ["msgpack", "vcdiff-core"]
4344
tests = ["junit","hamcrest-all", "nanohttpd", "nanohttpd-nanolets", "nanohttpd-websocket", "mockito-core", "concurrentunit", "slf4j-simple"]
4445
instrumental-android = ["android-test-runner", "android-test-rules", "dexmaker", "dexmaker-dx", "dexmaker-mockito", "android-retrostreams"]
4546

4647
[plugins]
4748
android-library = { id = "com.android.library", version.ref = "agp" }
4849
build-config = { id = "com.github.gmazzo.buildconfig", version.ref = "build-config" }
4950
maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" }
51+
lombok = { id = "io.freefair.lombok", version.ref = "lombok" }

java/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ tasks.withType<Jar> {
1919
dependencies {
2020
api(libs.gson)
2121
implementation(libs.bundles.common)
22+
implementation(project(":network-client-core"))
23+
runtimeOnly(project(":network-client-default"))
2224
testImplementation(libs.bundles.tests)
2325
}
2426

lib/src/main/java/io/ably/lib/debug/DebugOptions.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package io.ably.lib.debug;
22

3-
import java.net.HttpURLConnection;
43
import java.util.List;
54
import java.util.Map;
65

76
import io.ably.lib.http.HttpCore;
7+
import io.ably.lib.network.HttpRequest;
88
import io.ably.lib.transport.ITransport;
99
import io.ably.lib.types.AblyException;
1010
import io.ably.lib.types.ClientOptions;
@@ -19,7 +19,7 @@ public interface RawProtocolListener {
1919
}
2020

2121
public interface RawHttpListener {
22-
HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map<String, List<String>> requestHeaders, HttpCore.RequestBody requestBody);
22+
HttpCore.Response onRawHttpRequest(String id, HttpRequest request, String authHeader, Map<String, List<String>> requestHeaders, HttpCore.RequestBody requestBody);
2323
void onRawHttpResponse(String id, String method, HttpCore.Response response);
2424
void onRawHttpException(String id, String method, Throwable t);
2525
}

lib/src/main/java/io/ably/lib/http/HttpCore.java

Lines changed: 144 additions & 213 deletions
Large diffs are not rendered by default.

lib/src/main/java/io/ably/lib/http/HttpScheduler.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.ably.lib.http;
22

3-
import java.net.HttpURLConnection;
43
import java.net.URL;
54
import java.util.Locale;
65
import java.util.concurrent.ExecutionException;
@@ -9,6 +8,7 @@
98
import java.util.concurrent.TimeUnit;
109
import java.util.concurrent.TimeoutException;
1110

11+
import io.ably.lib.network.HttpCall;
1212
import io.ably.lib.types.AblyException;
1313
import io.ably.lib.types.Callback;
1414
import io.ably.lib.types.ErrorInfo;
@@ -331,15 +331,15 @@ protected void setError(ErrorInfo err) {
331331
}
332332
}
333333
protected synchronized boolean disposeConnection() {
334-
boolean hasConnection = conn != null;
334+
boolean hasConnection = httpCall != null;
335335
if(hasConnection) {
336-
conn.disconnect();
337-
conn = null;
336+
httpCall.cancel();
337+
httpCall = null;
338338
}
339339
return hasConnection;
340340
}
341341

342-
protected HttpURLConnection conn;
342+
protected HttpCall httpCall;
343343
protected T result;
344344
protected ErrorInfo err;
345345

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

Lines changed: 43 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,21 @@
11
package io.ably.lib.transport;
22

33
import io.ably.lib.http.HttpUtils;
4+
import io.ably.lib.network.WebSocketClient;
5+
import io.ably.lib.network.WebSocketEngine;
6+
import io.ably.lib.network.WebSocketEngineConfig;
7+
import io.ably.lib.network.WebSocketEngineFactory;
8+
import io.ably.lib.network.WebSocketListener;
9+
import io.ably.lib.network.NotConnectedException;
410
import io.ably.lib.types.AblyException;
511
import io.ably.lib.types.ErrorInfo;
612
import io.ably.lib.types.Param;
713
import io.ably.lib.types.ProtocolMessage;
814
import io.ably.lib.types.ProtocolSerializer;
15+
import io.ably.lib.util.ClientOptionsUtils;
916
import io.ably.lib.util.Log;
10-
import org.java_websocket.WebSocket;
11-
import org.java_websocket.client.WebSocketClient;
12-
import org.java_websocket.exceptions.WebsocketNotConnectedException;
13-
import org.java_websocket.framing.CloseFrame;
14-
import org.java_websocket.framing.Framedata;
15-
import org.java_websocket.handshake.ServerHandshake;
16-
17-
import javax.net.ssl.HttpsURLConnection;
17+
1818
import javax.net.ssl.SSLContext;
19-
import javax.net.ssl.SSLParameters;
20-
import javax.net.ssl.SSLSession;
21-
import java.net.URI;
2219
import java.nio.ByteBuffer;
2320
import java.util.Timer;
2421
import java.util.TimerTask;
@@ -50,7 +47,7 @@ public class WebSocketTransport implements ITransport {
5047
private final boolean channelBinaryMode;
5148
private String wsUri;
5249
private ConnectListener connectListener;
53-
private WsClient wsConnection;
50+
private WebSocketClient webSocketClient;
5451
/******************
5552
* protected constructor
5653
******************/
@@ -81,15 +78,26 @@ public void connect(ConnectListener connectListener) {
8178

8279
Log.d(TAG, "connect(); wsUri = " + wsUri);
8380
synchronized (this) {
84-
wsConnection = new WsClient(URI.create(wsUri), this::receive);
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+
8590
if (isTls) {
8691
SSLContext sslContext = SSLContext.getInstance("TLS");
8792
sslContext.init(null, null, null);
8893
SafeSSLSocketFactory factory = new SafeSSLSocketFactory(sslContext.getSocketFactory());
89-
wsConnection.setSocketFactory(factory);
94+
configBuilder.sslSocketFactory(factory);
9095
}
96+
97+
WebSocketEngine engine = engineFactory.create(configBuilder.build());
98+
webSocketClient = engine.create(wsUri, new WebSocketHandler(this::receive));
9199
}
92-
wsConnection.connect();
100+
webSocketClient.connect();
93101
} catch (AblyException e) {
94102
Log.e(TAG, "Unexpected exception attempting connection; wsUri = " + wsUri, e);
95103
connectListener.onTransportUnavailable(this, e.errorInfo);
@@ -103,9 +111,9 @@ public void connect(ConnectListener connectListener) {
103111
public void close() {
104112
Log.d(TAG, "close()");
105113
synchronized (this) {
106-
if (wsConnection != null) {
107-
wsConnection.close();
108-
wsConnection = null;
114+
if (webSocketClient != null) {
115+
webSocketClient.close();
116+
webSocketClient = null;
109117
}
110118
}
111119
}
@@ -127,14 +135,14 @@ public void send(ProtocolMessage msg) throws AblyException {
127135
ProtocolMessage decodedMsg = ProtocolSerializer.readMsgpack(encodedMsg);
128136
Log.v(TAG, "send(): " + decodedMsg.action + ": " + new String(ProtocolSerializer.writeJSON(decodedMsg)));
129137
}
130-
wsConnection.send(encodedMsg);
138+
webSocketClient.send(encodedMsg);
131139
} else {
132140
// Check the logging level to avoid performance hit associated with building the message
133141
if (Log.level <= Log.VERBOSE)
134142
Log.v(TAG, "send(): " + new String(ProtocolSerializer.writeJSON(msg)));
135-
wsConnection.send(ProtocolSerializer.writeJSON(msg));
143+
webSocketClient.send(ProtocolSerializer.writeJSON(msg));
136144
}
137-
} catch (WebsocketNotConnectedException e) {
145+
} catch (NotConnectedException e) {
138146
if (connectListener != null) {
139147
connectListener.onTransportUnavailable(this, AblyException.fromThrowable(e).errorInfo);
140148
} else
@@ -180,7 +188,7 @@ public WebSocketTransport getTransport(TransportParams params, ConnectionManager
180188
* WebSocketHandler methods
181189
**************************/
182190

183-
class WsClient extends WebSocketClient {
191+
class WebSocketHandler implements WebSocketListener {
184192
private final WebSocketReceiver receiver;
185193
/***************************
186194
* WsClient private members
@@ -189,38 +197,16 @@ class WsClient extends WebSocketClient {
189197
private Timer timer = new Timer();
190198
private TimerTask activityTimerTask = null;
191199
private long lastActivityTime;
192-
private boolean shouldExplicitlyVerifyHostname = true;
193200

194-
WsClient(URI serverUri, WebSocketReceiver receiver) {
195-
super(serverUri);
201+
WebSocketHandler(WebSocketReceiver receiver) {
196202
this.receiver = receiver;
197203
}
198204

199205
@Override
200-
public void onOpen(ServerHandshake handshakedata) {
206+
public void onOpen() {
201207
Log.d(TAG, "onOpen()");
202-
if (params.options.tls && shouldExplicitlyVerifyHostname && !isHostnameVerified(params.host)) {
203-
close();
204-
} else {
205-
connectListener.onTransportAvailable(WebSocketTransport.this);
206-
flagActivity();
207-
}
208-
}
209-
210-
/**
211-
* Added because we had to override the onSetSSLParameters() that usually performs this verification.
212-
* When the minSdkVersion will be updated to 24 we should remove this method and its usages.
213-
* https://github.com/TooTallNate/Java-WebSocket/wiki/No-such-method-error-setEndpointIdentificationAlgorithm#workaround
214-
*/
215-
private boolean isHostnameVerified(String hostname) {
216-
final SSLSession session = getSSLSession();
217-
if (HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)) {
218-
Log.v(TAG, "Successfully verified hostname");
219-
return true;
220-
} else {
221-
Log.e(TAG, "Hostname verification failed, expected " + hostname + ", found " + session.getPeerHost());
222-
return false;
223-
}
208+
connectListener.onTransportAvailable(WebSocketTransport.this);
209+
flagActivity();
224210
}
225211

226212
@Override
@@ -253,16 +239,14 @@ public void onMessage(String string) {
253239

254240
/* This allows us to detect a websocket ping, so we don't need Ably pings. */
255241
@Override
256-
public void onWebsocketPing(WebSocket conn, Framedata f) {
242+
public void onWebsocketPing() {
257243
Log.d(TAG, "onWebsocketPing()");
258-
/* Call superclass to ensure the pong is sent. */
259-
super.onWebsocketPing(conn, f);
260244
flagActivity();
261245
}
262246

263247
@Override
264-
public void onClose(final int wsCode, final String wsReason, final boolean remote) {
265-
Log.d(TAG, "onClose(): wsCode = " + wsCode + "; wsReason = " + wsReason + "; remote = " + remote);
248+
public void onClose(final int wsCode, final String wsReason) {
249+
Log.d(TAG, "onClose(): wsCode = " + wsCode + "; wsReason = " + wsReason + "; remote = " + false);
266250

267251
ErrorInfo reason;
268252
switch (wsCode) {
@@ -301,23 +285,14 @@ public void onClose(final int wsCode, final String wsReason, final boolean remot
301285
}
302286

303287
@Override
304-
public void onError(final Exception e) {
305-
Log.e(TAG, "Connection error ", e);
306-
connectListener.onTransportUnavailable(WebSocketTransport.this, new ErrorInfo(e.getMessage(), 503, 80000));
288+
public void onError(Throwable throwable) {
289+
Log.e(TAG, "Connection error ", throwable);
290+
connectListener.onTransportUnavailable(WebSocketTransport.this, new ErrorInfo(throwable.getMessage(), 503, 80000));
307291
}
308292

309293
@Override
310-
protected void onSetSSLParameters(SSLParameters sslParameters) {
311-
try {
312-
super.onSetSSLParameters(sslParameters);
313-
shouldExplicitlyVerifyHostname = false;
314-
} catch (NoSuchMethodError exception) {
315-
// This error will be thrown on Android below level 24.
316-
// When the minSdkVersion will be updated to 24 we should remove this overridden method.
317-
// https://github.com/TooTallNate/Java-WebSocket/wiki/No-such-method-error-setEndpointIdentificationAlgorithm#workaround
318-
Log.w(TAG, "Error when trying to set SSL parameters, most likely due to an old Java API version", exception);
319-
shouldExplicitlyVerifyHostname = true;
320-
}
294+
public void onOldJavaVersionDetected(Throwable throwable) {
295+
Log.w(TAG, "Error when trying to set SSL parameters, most likely due to an old Java API version", throwable);
321296
}
322297

323298
private synchronized void dispose() {
@@ -391,7 +366,7 @@ private synchronized void onActivityTimerExpiry() {
391366
// If we have no time remaining, then close the connection
392367
if (timeRemaining <= 0) {
393368
Log.e(TAG, "No activity for " + getActivityTimeout() + "ms, closing connection");
394-
closeConnection(CloseFrame.ABNORMAL_CLOSE, "timed out");
369+
webSocketClient.cancel(ABNORMAL_CLOSE, "timed out");
395370
return;
396371
}
397372

lib/src/main/java/io/ably/lib/types/AblyException.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.ably.lib.types;
22

3+
import io.ably.lib.network.FailedConnectionException;
34
import java.net.ConnectException;
45
import java.net.NoRouteToHostException;
56
import java.net.SocketTimeoutException;
@@ -50,6 +51,8 @@ public static AblyException fromThrowable(Throwable t) {
5051
return (AblyException)t;
5152
if(t instanceof ConnectException || t instanceof SocketTimeoutException || t instanceof UnknownHostException || t instanceof NoRouteToHostException)
5253
return new HostFailedException(t, ErrorInfo.fromThrowable(t));
54+
if (t instanceof FailedConnectionException)
55+
return new HostFailedException(t.getCause(), ErrorInfo.fromThrowable(t.getCause()));
5356

5457
return new AblyException(t, ErrorInfo.fromThrowable(t));
5558
}
@@ -61,4 +64,4 @@ public static class HostFailedException extends AblyException {
6164
super(throwable, reason);
6265
}
6366
}
64-
}
67+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.ably.lib.util;
2+
3+
import io.ably.lib.network.ProxyAuthType;
4+
import io.ably.lib.network.ProxyConfig;
5+
import io.ably.lib.types.ClientOptions;
6+
7+
import java.util.Arrays;
8+
9+
public class ClientOptionsUtils {
10+
11+
public static ProxyConfig convertToProxyConfig(ClientOptions clientOptions) {
12+
if (clientOptions.proxy == null) return null;
13+
14+
ProxyConfig.ProxyConfigBuilder builder = ProxyConfig.builder();
15+
16+
builder
17+
.host(clientOptions.proxy.host)
18+
.port(clientOptions.proxy.port)
19+
.username(clientOptions.proxy.username)
20+
.password(clientOptions.proxy.password);
21+
22+
if (clientOptions.proxy.nonProxyHosts != null) {
23+
builder.nonProxyHosts(Arrays.asList(clientOptions.proxy.nonProxyHosts));
24+
}
25+
26+
switch (clientOptions.proxy.prefAuthType) {
27+
case BASIC:
28+
builder.authType(ProxyAuthType.BASIC);
29+
break;
30+
case DIGEST:
31+
builder.authType(ProxyAuthType.DIGEST);
32+
break;
33+
}
34+
35+
return builder.build();
36+
}
37+
}

0 commit comments

Comments
 (0)