diff --git a/common.gradle b/common.gradle index 63d030daf..c54ade489 100644 --- a/common.gradle +++ b/common.gradle @@ -1,5 +1,6 @@ repositories { jcenter() + mavenCentral() } group = 'io.ably' diff --git a/dependencies.gradle b/dependencies.gradle index 9f4740d84..39452428b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -2,7 +2,7 @@ // in java/build.gradle and android/build.gradle for maven. dependencies { implementation 'org.msgpack:msgpack-core:0.8.11' - implementation 'io.ably:Java-WebSocket:1.3.1' + implementation 'org.java-websocket:Java-WebSocket:1.4.0' implementation 'com.google.code.gson:gson:2.5' testImplementation 'org.hamcrest:hamcrest-all:1.3' testImplementation 'junit:junit:4.12' diff --git a/lib/src/main/java/io/ably/lib/debug/DebugOptions.java b/lib/src/main/java/io/ably/lib/debug/DebugOptions.java index 0dd54682f..d2f6cf6cc 100644 --- a/lib/src/main/java/io/ably/lib/debug/DebugOptions.java +++ b/lib/src/main/java/io/ably/lib/debug/DebugOptions.java @@ -11,6 +11,7 @@ public class DebugOptions extends ClientOptions { public interface RawProtocolListener { + public void onRawConnectRequested(String url); public void onRawConnect(String url); public void onRawMessageSend(ProtocolMessage message); public void onRawMessageRecv(ProtocolMessage message); diff --git a/lib/src/main/java/io/ably/lib/http/HttpUtils.java b/lib/src/main/java/io/ably/lib/http/HttpUtils.java index 38a717f24..c415c62f7 100644 --- a/lib/src/main/java/io/ably/lib/http/HttpUtils.java +++ b/lib/src/main/java/io/ably/lib/http/HttpUtils.java @@ -8,6 +8,7 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -138,19 +139,29 @@ public static String getParam(Param[] params, String key) { return result; } - public static String encodeURIComponent(String input) { - try { - return URLEncoder.encode(input, "UTF-8") - .replaceAll(" ", "%20") - .replaceAll("!", "%21") - .replaceAll("'", "%27") - .replaceAll("\\(", "%28") - .replaceAll("\\)", "%29") - .replaceAll("\\+", "%2B") - .replaceAll("\\:", "%3A") - .replaceAll("~", "%7E"); - } catch (UnsupportedEncodingException e) {} - return null; + /* copied from https://stackoverflow.com/a/52378025 */ + private static final String HEX = "0123456789ABCDEF"; + + public static String encodeURIComponent(String str) { + if (str == null) { + return null; + } + + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + StringBuilder builder = new StringBuilder(bytes.length); + + for (byte c : bytes) { + if (c >= 'a' ? c <= 'z' || c == '~' : + c >= 'A' ? c <= 'Z' || c == '_' : + c >= '0' ? c <= '9' : c == '-' || c == '.') + builder.append((char)c); + else + builder.append('%') + .append(HEX.charAt(c >> 4 & 0xf)) + .append(HEX.charAt(c & 0xf)); + } + + return builder.toString(); } private static void appendParams(StringBuilder uri, Param[] params) { diff --git a/lib/src/main/java/io/ably/lib/rest/Auth.java b/lib/src/main/java/io/ably/lib/rest/Auth.java index 67fa790c5..9c38bfaf1 100644 --- a/lib/src/main/java/io/ably/lib/rest/Auth.java +++ b/lib/src/main/java/io/ably/lib/rest/Auth.java @@ -964,9 +964,9 @@ private TokenDetails assertValidToken(TokenParams params, AuthOptions options, b return tokenDetails; } - private static boolean tokenValid(TokenDetails tokenDetails) { + private boolean tokenValid(TokenDetails tokenDetails) { /* RSA4b1: only perform a local check for token validity if we have time sync with the server */ - return (timeDelta == Long.MAX_VALUE) || (tokenDetails.expires > Auth.serverTimestamp()); + return (timeDelta == Long.MAX_VALUE) || (tokenDetails.expires > serverTimestamp()); } /** @@ -1078,7 +1078,7 @@ public String checkClientId(BaseMessage msg, boolean allowNullClientId, boolean /** * Using time delta obtained before guess current server time */ - public static long serverTimestamp() { + public long serverTimestamp() { long clientTime = timestamp(); long delta = timeDelta; return delta != Long.MAX_VALUE ? clientTime + timeDelta : clientTime; @@ -1097,18 +1097,18 @@ public static long serverTimestamp() { /** * Time delta is server time minus client time, in milliseconds, MAX_VALUE if not obtained yet */ - private static long timeDelta = Long.MAX_VALUE; + private long timeDelta = Long.MAX_VALUE; /** * Time delta between System.nanoTime() and System.currentTimeMillis. If it changes significantly it * suggests device time/date has changed */ - private static long nanoTimeDelta = System.currentTimeMillis() - System.nanoTime()/(1000*1000); + private long nanoTimeDelta = System.currentTimeMillis() - System.nanoTime()/(1000*1000); public static final String WILDCARD_CLIENTID = "*"; /** * For testing purposes we need method to clear cached timeDelta */ - public static void clearCachedServerTime() { + public void clearCachedServerTime() { timeDelta = Long.MAX_VALUE; } } diff --git a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java index e1bf95772..e3f29d84e 100644 --- a/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java +++ b/lib/src/main/java/io/ably/lib/transport/ConnectionManager.java @@ -53,16 +53,18 @@ public static class StateIndication { final ErrorInfo reason; final String fallback; final String currentHost; + final boolean retryImmediately; public StateIndication(ConnectionState state, ErrorInfo reason) { - this(state, reason, null, null); + this(state, reason, null, null, false); } - public StateIndication(ConnectionState state, ErrorInfo reason, String fallback, String currentHost) { + public StateIndication(ConnectionState state, ErrorInfo reason, String fallback, String currentHost, boolean retryImmediately) { this.state = state; this.reason = reason; this.fallback = fallback; this.currentHost = currentHost; + this.retryImmediately = retryImmediately; } } @@ -375,34 +377,44 @@ public void run() { */ public void onAuthUpdated(String token, boolean waitForResponse) throws AblyException { ConnectionWaiter waiter = new ConnectionWaiter(); - if (state.state == ConnectionState.connected) { - /* (RTC8a) If the connection is in the CONNECTED state and - * auth.authorize is called or Ably requests a re-authentication - * (see RTN22), the client must obtain a new token, then send an - * AUTH ProtocolMessage to Ably with an auth attribute - * containing an AuthDetails object with the token string. */ - try { - ProtocolMessage msg = new ProtocolMessage(ProtocolMessage.Action.auth); - msg.auth = new ProtocolMessage.AuthDetails(token); - send(msg, false, null); - } catch (AblyException e) { - /* The send failed. Close the transport; if a subsequent - * reconnect succeeds, it will be with the new token. */ - Log.v(TAG, "onAuthUpdated: closing transport after send failure"); - transport.close(/*sendDisconnect=*/false); - } - } else { - if (state.state == ConnectionState.connecting) { + switch(state.state) { + case connected: + /* (RTC8a) If the connection is in the CONNECTED state and + * auth.authorize is called or Ably requests a re-authentication + * (see RTN22), the client must obtain a new token, then send an + * AUTH ProtocolMessage to Ably with an auth attribute + * containing an AuthDetails object with the token string. */ + try { + ProtocolMessage msg = new ProtocolMessage(ProtocolMessage.Action.auth); + msg.auth = new ProtocolMessage.AuthDetails(token); + send(msg, false, null); + } catch (AblyException e) { + /* The send failed. Close the transport; if a subsequent + * reconnect succeeds, it will be with the new token. */ + Log.v(TAG, "onAuthUpdated: closing transport after send failure"); + transport.close(/*sendDisconnect=*/false); + } + break; + + case connecting: /* Close the connecting transport. */ Log.v(TAG, "onAuthUpdated: closing connecting transport"); - transport.close(/*sendDisconnect=*/false); - } - /* Start a new connection attempt. */ - connect(); + clearTransport(); + /* request a state change that triggers an immediate retry */ + Log.v(TAG, "onAuthUpdated: requesting immediate new connection attempt"); + ErrorInfo disconnectError = new ErrorInfo("Aborting incomplete connection with superseded auth params", 503, 80003); + requestState(new StateIndication(ConnectionState.disconnected, disconnectError, null, null, true)); + break; + + default: + /* Start a new connection attempt. */ + connect(); + break; } - if(!waitForResponse) + if(!waitForResponse) { return; + } /* Wait for a state transition into anything other than connecting or * disconnected. Note that this includes the case that the connection @@ -676,7 +688,7 @@ private StateIndication handleStateRequest() { if(connectImpl(transitionState)) { return transitionState; } else { - return new StateIndication(ConnectionState.failed, new ErrorInfo("Connection failed; no host available", 404, 80000), null, requestedState.currentHost); + return new StateIndication(ConnectionState.failed, new ErrorInfo("Connection failed; no host available", 404, 80000), null, requestedState.currentHost, false); } } /* no other requests can move from a terminal state */ @@ -738,7 +750,11 @@ private void handleStateChange(StateIndication stateChange) { } switch(state.state) { case connecting: - stateChange = checkSuspend(stateChange); + if(stateChange.retryImmediately && !suppressRetry) { + requestState(ConnectionState.connecting); + } else { + stateChange = checkSuspend(stateChange); + } pendingConnect = null; break; case closing: @@ -798,7 +814,7 @@ private StateIndication checkSuspend(StateIndication stateChange) { String hostFallback = hosts.getFallback(pendingConnect.host); if (hostFallback != null) { Log.v(TAG, "checkSuspend: fallback to " + hostFallback); - requestState(new StateIndication(ConnectionState.connecting, null, hostFallback, pendingConnect.host)); + requestState(new StateIndication(ConnectionState.connecting, null, hostFallback, pendingConnect.host, false)); /* returning null ensures we stay in the connecting state */ return null; } @@ -943,7 +959,7 @@ public synchronized void onTransportUnavailable(ITransport transport, TransportP Log.i(TAG, "onTransportUnavailable: disconnected: " + reason.message); } ably.auth.onAuthError(reason); - requestState(new StateIndication(state, reason, null, transport.getHost())); + requestState(new StateIndication(state, reason, null, transport.getHost(), false)); this.transport = null; } @@ -987,9 +1003,13 @@ private boolean connectImpl(StateIndication request) { oldTransport = this.transport; this.transport = transport; } - if (oldTransport != null) + if (oldTransport != null) { oldTransport.abort(REASON_TIMEDOUT); + } transport.connect(this); + if(protocolListener != null) { + protocolListener.onRawConnectRequested(transport.getURL()); + } return true; } @@ -1300,10 +1320,14 @@ void disconnectAndSuppressRetries() { * internal ******************/ + private boolean isTokenError(ErrorInfo err) { + return (err.code >= 40140) && (err.code < 40150); + } + private boolean isFatalError(ErrorInfo err) { if(err.code != 0) { /* token errors are assumed to be recoverable */ - if((err.code >= 40140) && (err.code < 40150)) { return false; } + if(isTokenError(err)) { return false; } /* 400 codes assumed to be fatal */ if((err.code >= 40000) && (err.code < 50000)) { return true; } } diff --git a/lib/src/main/java/io/ably/lib/transport/Defaults.java b/lib/src/main/java/io/ably/lib/transport/Defaults.java index 55a725b2e..b20f8ddea 100644 --- a/lib/src/main/java/io/ably/lib/transport/Defaults.java +++ b/lib/src/main/java/io/ably/lib/transport/Defaults.java @@ -28,7 +28,7 @@ public class Defaults { /* Timeouts */ public static int TIMEOUT_CONNECT = 15000; - public static int TIMEOUT_DISCONNECT = 30000; + public static int TIMEOUT_DISCONNECT = 15000; public static int TIMEOUT_CHANNEL_RETRY = 15000; /* TO313 */ diff --git a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java index 09d17f834..fd0227686 100644 --- a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java +++ b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java @@ -75,7 +75,7 @@ public void connect(ConnectListener connectListener) { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init( null, null, null ); SSLSocketFactory factory = sslContext.getSocketFactory();// (SSLSocketFactory) SSLSocketFactory.getDefault(); - wsConnection.setSocket( factory.createSocket() ); + wsConnection.setSocketFactory(factory); } } wsConnection.connect(); @@ -157,8 +157,12 @@ public WsClient(URI serverUri) { @Override public void onOpen(ServerHandshake handshakedata) { Log.d(TAG, "onOpen()"); - connectListener.onTransportAvailable(WebSocketTransport.this, params); - flagActivity(); + schedule(new TimerTask() { + public void run() { + connectListener.onTransportAvailable(WebSocketTransport.this, params); + flagActivity(); + } + }); } @Override @@ -197,69 +201,77 @@ public void onWebsocketPing( WebSocket conn, Framedata f ) { } @Override - public void onClose(int wsCode, String wsReason, boolean remote) { + public void onClose(final int wsCode, final String wsReason, final boolean remote) { Log.d(TAG, "onClose(): wsCode = " + wsCode + "; wsReason = " + wsReason + "; remote = " + remote); - ConnectionState newState; - ErrorInfo reason; - switch(wsCode) { - case NEVER_CONNECTED: - newState = ConnectionState.disconnected; - reason = ConnectionManager.REASON_NEVER_CONNECTED; - break; - case CLOSE_NORMAL: - case BUGGYCLOSE: - case GOING_AWAY: - case ABNORMAL_CLOSE: - /* we don't know the specific reason that the connection closed in these cases, - * but we have to assume it's a problem with connectivity rather than some other - * application problem */ - newState = ConnectionState.disconnected; - reason = ConnectionManager.REASON_DISCONNECTED; - break; - case REFUSE: - case POLICY_VALIDATION: - newState = ConnectionState.failed; - reason = ConnectionManager.REASON_REFUSED; - break; - case TOOBIG: - newState = ConnectionState.failed; - reason = ConnectionManager.REASON_TOO_BIG; - break; - case NO_UTF8: - case CLOSE_PROTOCOL_ERROR: - case UNEXPECTED_CONDITION: - case EXTENSION: - case TLS_ERROR: - default: - /* we don't know the specific reason that the connection closed in these cases, - * but we have to assume it's an application problem, and the problem will - * recur if we try again. The failed state means that we won't automatically - * try again. */ - newState = ConnectionState.failed; - reason = ConnectionManager.REASON_FAILED; - break; - } - connectListener.onTransportUnavailable(WebSocketTransport.this, params, reason, newState); + schedule(new TimerTask() { + public void run() { + ConnectionState newState; + ErrorInfo reason; + switch(wsCode) { + case NEVER_CONNECTED: + newState = ConnectionState.disconnected; + reason = ConnectionManager.REASON_NEVER_CONNECTED; + break; + case CLOSE_NORMAL: + case BUGGYCLOSE: + case GOING_AWAY: + case ABNORMAL_CLOSE: + /* we don't know the specific reason that the connection closed in these cases, + * but we have to assume it's a problem with connectivity rather than some other + * application problem */ + newState = ConnectionState.disconnected; + reason = ConnectionManager.REASON_DISCONNECTED; + break; + case REFUSE: + case POLICY_VALIDATION: + newState = ConnectionState.failed; + reason = ConnectionManager.REASON_REFUSED; + break; + case TOOBIG: + newState = ConnectionState.failed; + reason = ConnectionManager.REASON_TOO_BIG; + break; + case NO_UTF8: + case CLOSE_PROTOCOL_ERROR: + case UNEXPECTED_CONDITION: + case EXTENSION: + case TLS_ERROR: + default: + /* we don't know the specific reason that the connection closed in these cases, + * but we have to assume it's an application problem, and the problem will + * recur if we try again. The failed state means that we won't automatically + * try again. */ + newState = ConnectionState.failed; + reason = ConnectionManager.REASON_FAILED; + break; + } + connectListener.onTransportUnavailable(WebSocketTransport.this, params, reason, newState); + } + }); dispose(); } @Override - public void onError(Exception e) { - connectListener.onTransportUnavailable(WebSocketTransport.this, params, new ErrorInfo(e.getMessage(), 503, 80000)); + public void onError(final Exception e) { + schedule(new TimerTask() { + public void run() { + connectListener.onTransportUnavailable(WebSocketTransport.this, params, new ErrorInfo(e.getMessage(), 503, 80000)); + } + }); } - private void dispose() { + private synchronized void dispose() { /* dispose timer */ - if(timer != null) { + try { timer.cancel(); timer = null; - } + } catch(IllegalStateException e) {} } - private void flagActivity() { + private synchronized void flagActivity() { lastActivityTime = System.currentTimeMillis(); connectionManager.setLastActivity(lastActivityTime); - if (timer == null && connectionManager.maxIdleInterval != 0) { + if (activityTimerTask == null && connectionManager.maxIdleInterval != 0) { /* No timer currently running because previously there was no * maxIdleInterval configured, but now there is a * maxIdleInterval configured. Call checkActivity so a timer @@ -270,11 +282,14 @@ private void flagActivity() { } } - private void checkActivity() { + private synchronized void checkActivity() { long timeout = connectionManager.maxIdleInterval; if (timeout == 0) { Log.v(TAG, "checkActivity: infinite timeout"); - timer = null; + return; + } + if(activityTimerTask != null) { + /* timer already running */ return; } timeout += connectionManager.ably.options.realtimeRequestTimeout; @@ -285,24 +300,15 @@ private void checkActivity() { * of inactivity. Schedule a new timer for that long after the * last activity time. */ Log.v(TAG, "checkActivity: ok"); - if (timer == null) { - synchronized(this) { - if (timer == null) { - try { - timer = new Timer(); - } catch(Throwable t) { - Log.e(TAG, "Unexpected exception creating activity timer", t); - } + schedule((activityTimerTask = new TimerTask() { + public void run() { + try { + checkActivity(); + } catch(Throwable t) { + Log.e(TAG, "Unexpected exception in activity timer handler", t); } } - } - if(timer != null) { - try { - timer.schedule(new WsClientTimerTask(this), next - now); - } catch(IllegalStateException ise) { - Log.e(TAG, "Unexpected exception scheduling activity timer", ise); - } - } + }), next - now); } else { /* Timeout has been reached. Close the connection. */ Log.e(TAG, "No activity for " + timeout + "ms, closing connection"); @@ -310,21 +316,16 @@ private void checkActivity() { } } - /* The TimerTask used to implement disconnection if no activity (inc - * pings) is seen within a certain time. - */ - class WsClientTimerTask extends TimerTask { - private final WsClient client; - - public WsClientTimerTask(WsClient client) { - this.client = client; - } + private void schedule(TimerTask task) { + schedule(task, 0); + } - public void run() { + private synchronized void schedule(TimerTask task, long delay) { + if(timer != null) { try { - client.checkActivity(); - } catch(Throwable t) { - Log.e(TAG, "Unexpected exception in activity timer handler", t); + timer.schedule(task, delay); + } catch(IllegalStateException ise) { + Log.e(TAG, "Unexpected exception scheduling activity timer", ise); } } } @@ -333,9 +334,9 @@ public void run() { * WsClient private members ***************************/ - private Timer timer; + private Timer timer = new Timer(); + private TimerTask activityTimerTask = null; private long lastActivityTime; - } public String toString() { diff --git a/lib/src/test/java/io/ably/lib/test/common/Helpers.java b/lib/src/test/java/io/ably/lib/test/common/Helpers.java index 257226db3..6ba613a8b 100644 --- a/lib/src/test/java/io/ably/lib/test/common/Helpers.java +++ b/lib/src/test/java/io/ably/lib/test/common/Helpers.java @@ -416,12 +416,17 @@ public synchronized boolean waitFor(ConnectionState state, int count, long time) long targetTime = System.currentTimeMillis() + time; long remaining = time; while(getStateCount(state) < count && remaining > 0) { + Log.d(TAG, "waitFor(state=" + state.getConnectionEvent().name() + ", waiting for=" + remaining + ")"); try { wait(remaining); } catch(InterruptedException e) {} remaining = targetTime - System.currentTimeMillis(); } int stateCount = getStateCount(state); - Log.d(TAG, "waitFor done: state=" + connection.state.getConnectionEvent().name() + - ", count=" + Integer.toString(stateCount)+ ")"); + if(remaining <= 0) { + Log.d(TAG, "waitFor timed out: current state=" + connection.state.getConnectionEvent().name()); + } else { + Log.d(TAG, "waitFor done: state=" + connection.state.getConnectionEvent().name() + + ", count=" + Integer.toString(stateCount)); + } return stateCount >= count; } @@ -457,6 +462,9 @@ public void onConnectionStateChanged(ConnectionStateListener.ConnectionStateChan Counter counter = stateCounts.get(state.current); if(counter == null) stateCounts.put(state.current, (counter = new Counter())); counter.incr(); Log.d(TAG, "onConnectionStateChanged(" + state.current + "): count now " + counter.value); + if(state.current == ConnectionState.connected) { + Log.d(TAG, "onConnectionStateChanged(connected): count now " + counter.value); + } notify(); } } @@ -635,6 +643,9 @@ public void onRawMessageSend(ProtocolMessage message) { } } + @Override + public void onRawConnectRequested(String url) {} + @Override public void onRawConnect(String url) { connectUrl = url; diff --git a/lib/src/test/java/io/ably/lib/test/realtime/ConnectionManagerTest.java b/lib/src/test/java/io/ably/lib/test/realtime/ConnectionManagerTest.java index aa2f2800c..04b0d986a 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/ConnectionManagerTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/ConnectionManagerTest.java @@ -19,6 +19,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; @@ -65,7 +66,7 @@ public void connectionmanager_fallback_none() throws AblyException { AblyRealtime ably = new AblyRealtime(opts); ConnectionManager connectionManager = ably.connection.connectionManager; - new Helpers.ConnectionManagerWaiter(connectionManager).waitFor(ConnectionState.connected); + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); /* Verify that, * - connectionManager is connected @@ -96,7 +97,7 @@ public void connectionmanager_fallback_none_customhost() throws AblyException { AblyRealtime ably = new AblyRealtime(opts); ConnectionManager connectionManager = ably.connection.connectionManager; - new Helpers.ConnectionManagerWaiter(connectionManager).waitFor(ConnectionState.disconnected); + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.disconnected); /* Verify that, * - connectionManager is disconnected @@ -163,6 +164,7 @@ protected boolean checkConnectivity() { * @throws AblyException */ @Test + @Ignore("Invalid test") public void connectionmanager_fallback_applied() throws AblyException { ClientOptions opts = createOptions(testVars.keys[0].keyStr); // Use a host that supports fallback @@ -176,7 +178,7 @@ public void connectionmanager_fallback_applied() throws AblyException { AblyRealtime ably = new AblyRealtime(opts); ConnectionManager connectionManager = ably.connection.connectionManager; - new Helpers.ConnectionManagerWaiter(connectionManager).waitFor(ConnectionState.disconnected); + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.disconnected); /* Verify that, * - connectionManager is disconnected @@ -196,6 +198,7 @@ public void connectionmanager_fallback_applied() throws AblyException { *
*/ @Test + @Ignore("Invalid test") public void connectionmanager_reconnect_default_endpoint() throws AblyException { ClientOptions opts = createOptions(testVars.keys[0].keyStr); // Use the default host, supporting fallback @@ -210,7 +213,7 @@ public void connectionmanager_reconnect_default_endpoint() throws AblyException ConnectionManager connectionManager = ably.connection.connectionManager; System.out.println("waiting for disconnected"); - new Helpers.ConnectionManagerWaiter(connectionManager).waitFor(ConnectionState.disconnected); + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.disconnected); System.out.println("got disconnected"); /* Verify that, @@ -225,7 +228,7 @@ public void connectionmanager_reconnect_default_endpoint() throws AblyException System.out.println("about to connect"); ably.connection.connect(); - new Helpers.ConnectionManagerWaiter(connectionManager).waitFor(ConnectionState.failed); + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.failed); /* Verify that, * - connectionManager is failed, because we are using an application key @@ -245,6 +248,7 @@ public void connectionmanager_reconnect_default_endpoint() throws AblyException * fallbackHostsUseDefault is set. */ @Test + @Ignore("Invalid test") public void connectionmanager_reconnect_default_fallback() throws AblyException { ClientOptions opts = createOptions(testVars.keys[0].keyStr); // Use a host that does not normally support fallback. @@ -260,7 +264,7 @@ public void connectionmanager_reconnect_default_fallback() throws AblyException ConnectionManager connectionManager = ably.connection.connectionManager; System.out.println("waiting for disconnected"); - new Helpers.ConnectionManagerWaiter(connectionManager).waitFor(ConnectionState.disconnected); + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.disconnected); System.out.println("got disconnected"); ably.close(); @@ -470,7 +474,7 @@ public void onConnectionStateChanged(ConnectionStateChange state) { } } }); - new Helpers.ConnectionManagerWaiter(ably.connection.connectionManager).waitFor(ConnectionState.connected); + new Helpers.ConnectionWaiter(ably.connection).waitFor(ConnectionState.connected); assertTrue("Connected callback was not run", callbackWasRun[0]); ably.close(); } catch (AblyException e) { diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java index 8e0d3b1bb..199b048fc 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java @@ -1,6 +1,7 @@ package io.ably.lib.test.realtime; import io.ably.lib.realtime.*; +import io.ably.lib.test.common.Setup; import io.ably.lib.transport.ConnectionManager; import io.ably.lib.transport.Defaults; import io.ably.lib.util.Log; @@ -19,12 +20,17 @@ import io.ably.lib.types.ErrorInfo; import io.ably.lib.types.Message; import io.ably.lib.types.ProtocolMessage; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.Timeout; import static org.junit.Assert.*; public class RealtimeAuthTest extends ParameterizedTest { + @Rule + public Timeout testTimeout = Timeout.seconds(30); + /** * RSA12a: The clientId attribute of a TokenRequest or TokenDetails * used for authentication is null, or ConnectionDetails#clientId is null @@ -697,6 +703,76 @@ public void auth_clientid_publish_explicit_before_identified() { } } + /** + * Call renew() whilst connecting; verify there's no crash (see https://github.com/ably/ably-java/issues/503) + */ + @Test + public void auth_renew_whilst_connecting() { + try { + /* get a TokenDetails */ + final String testKey = testVars.keys[0].keyStr; + ClientOptions optsForToken = createOptions(testKey); + final AblyRest ablyForToken = new AblyRest(optsForToken); + + final TokenDetails tokenDetails = ablyForToken.auth.requestToken(new Auth.TokenParams(){{ ttl = 1000L; }}, null); + assertNotNull("Expected token value", tokenDetails.token); + + /* create Ably realtime instance with token and authCallback */ + class ProtocolListener extends DebugOptions implements DebugOptions.RawProtocolListener { + ProtocolListener() { + Setup.getTestVars().fillInOptions(this); + protocolListener = this; + } + @Override + public void onRawConnectRequested(String url) { + synchronized(this) { + notify(); + } + } + + @Override + public void onRawConnect(String url) {} + @Override + public void onRawMessageSend(ProtocolMessage message) {} + @Override + public void onRawMessageRecv(ProtocolMessage message) {} + } + + ProtocolListener opts = new ProtocolListener(); + opts.logLevel = Log.VERBOSE; + opts.autoConnect = false; + opts.tokenDetails = tokenDetails; + opts.authCallback = new Auth.TokenCallback() { + /* implement callback, using Ably instance with key */ + @Override + public Object getTokenRequest(Auth.TokenParams params) { + return tokenDetails; + } + }; + + final AblyRealtime ably = new AblyRealtime(opts); + synchronized (opts) { + ably.connect(); + try { + opts.wait(); + } catch(InterruptedException ie) {} + ably.auth.renew(); + } + + Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ably.connection); + boolean isConnected = connectionWaiter.waitFor(ConnectionState.connected, 1, 4000L); + if(isConnected) { + /* done */ + ably.close(); + } else { + fail("auth_expired_token_expire_renew: unable to connect; final state = " + ably.connection.state); + } + } catch (AblyException e) { + e.printStackTrace(); + fail("auth_expired_token_expire_renew: Unexpected exception instantiating library"); + } + } + /** * Verify that with queryTime=false, when instancing with an already-expired token and authCallback, * connection can succeed @@ -716,9 +792,6 @@ public void auth_expired_token_expire_before_connect_renew() { /* allow to expire */ try { Thread.sleep(200L); } catch(InterruptedException ie) {} - /* clear the cached server time (it is static so shared between library instances) */ - Auth.clearCachedServerTime(); - /* create Ably realtime instance with token and authCallback */ ClientOptions opts = createOptions(); opts.tokenDetails = tokenDetails; @@ -735,17 +808,10 @@ public Object getTokenRequest(Auth.TokenParams params) throws AblyException { opts.logLevel = Log.VERBOSE; - /* temporarily override the disconnected retry interval */ - ConnectionManager.states.get(ConnectionState.disconnected).timeout = 1000L; - int oldDisconnectTimeout = Defaults.TIMEOUT_DISCONNECT; - Defaults.TIMEOUT_DISCONNECT = 1000; - final AblyRealtime ably = new AblyRealtime(opts); Helpers.ConnectionWaiter connectionWaiter = new Helpers.ConnectionWaiter(ably.connection); - boolean isConnected = connectionWaiter.waitFor(ConnectionState.connected, 1, 4000L); - - ConnectionManager.states.get(ConnectionState.disconnected).timeout = Defaults.TIMEOUT_DISCONNECT; + boolean isConnected = connectionWaiter.waitFor(ConnectionState.connected, 1, 30000L); if(isConnected) { /* done */ @@ -774,9 +840,6 @@ public void auth_expired_token_expire_after_connect_renew() { TokenDetails tokenDetails = ablyForToken.auth.requestToken(new Auth.TokenParams(){{ ttl = 2000L; }}, null); assertNotNull("Expected token value", tokenDetails.token); - /* clear the cached server time (it is static so shared between library instances) */ - Auth.clearCachedServerTime(); - /* create Ably realtime with token and authCallback */ ClientOptions opts = createOptions(); opts.tokenDetails = tokenDetails; diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectTest.java index ce05367cb..ae2422ece 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectTest.java @@ -163,6 +163,8 @@ public void onRawConnect(String url) { urlWrapper[0] = url; } @Override + public void onRawConnectRequested(String url) {} + @Override public void onRawMessageSend(ProtocolMessage message) {} @Override public void onRawMessageRecv(ProtocolMessage message) {} diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeJWTTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeJWTTest.java index 076eb8a58..d41cebea1 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeJWTTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeJWTTest.java @@ -205,6 +205,8 @@ public void auth_jwt_with_client_than_reauths_without_disconnecting() { options.authUrl = echoServer; options.authParams = mergeParams(keys, mediumTokenTtl); options.protocolListener = new RawProtocolListener() { + @Override + public void onRawConnectRequested(String url) {} @Override public void onRawConnect(String url) { } @Override @@ -303,6 +305,8 @@ public Object handleResponse(HttpCore.Response response, ErrorInfo error) throws options.environment = createOptions().environment; options.authCallback = authCallback; options.protocolListener = new RawProtocolListener() { + @Override + public void onRawConnectRequested(String url) {} @Override public void onRawConnect(String url) { } @Override diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestAuthAttributeTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestAuthAttributeTest.java index 6245d0d11..87655e736 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestAuthAttributeTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestAuthAttributeTest.java @@ -118,7 +118,6 @@ public Object getTokenRequest(TokenParams params) throws AblyException { @Test public void auth_stores_options_exception_querytime() { try { - Auth.clearCachedServerTime(); final long fakeServerTime = -1000; final String expectedClientId = "testClientId"; ClientOptions opts = createOptions(testVars.keys[0].keyStr); diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java index d2b02dc84..f88eaba42 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java @@ -1,12 +1,5 @@ package io.ably.lib.test.rest; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; @@ -43,6 +36,8 @@ import io.ably.lib.test.common.Helpers.RawHttpTracker; import io.ably.lib.test.util.TokenServer; +import static org.junit.Assert.*; + public class RestAuthTest extends ParameterizedTest { @Rule @@ -1628,7 +1623,6 @@ public void auth_useTokenAuth() { @Test public void auth_testQueryTime() { try { - Auth.clearCachedServerTime(); nanoHTTPD.clearRequestHistory(); ClientOptions opts = new ClientOptions(testVars.keys[0].keyStr); opts.tls = false; @@ -1651,7 +1645,7 @@ public void auth_testQueryTime() { if (request.contains("/time")) timeRequestCount++; - assertEquals("Verify number of time requests", timeRequestCount, 1); + assertEquals("Verify number of time requests", timeRequestCount, 2); } catch (AblyException e) { e.printStackTrace(); fail("Unexpected exception"); @@ -1788,7 +1782,7 @@ public void auth_local_token_expiry_check_sync() { AblyRest ably = new AblyRest(opts); /* sync this library instance to server by creating a token request */ - ably.auth.createTokenRequest(null, new Auth.AuthOptions() {{ key = testKey; }}); + ably.auth.createTokenRequest(null, new Auth.AuthOptions() {{ key = testKey; queryTime = true; }}); /* wait for the token to expire */ try { Thread.sleep(200L); } catch(InterruptedException ie) {} @@ -1800,7 +1794,9 @@ public void auth_local_token_expiry_check_sync() { return; } catch (AblyException e) { assertEquals("Verify that API request failed with credentials error", e.errorInfo.code, 40106); - assertEquals("Verify no API request attempted", httpListener.size(), 0); + for(Helpers.RawHttpRequest req : httpListener.values()) { + assertFalse("Verify no API request attempted", req.url.getPath().contains("stats")); + } } } catch (AblyException e) { e.printStackTrace(); @@ -1824,9 +1820,6 @@ public void auth_local_token_expiry_check_nosync() { TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams(){{ ttl = 100L; }}, null); - /* clear the cached server time (it is static so shared between library instances) */ - Auth.clearCachedServerTime(); - /* create Ably instance with token details */ DebugOptions opts = new DebugOptions(); fillInOptions(opts); @@ -1871,9 +1864,6 @@ public void auth_local_token_expiry_check_nosync_retried() { TokenDetails tokenDetails = ablyForToken.auth.requestToken(new TokenParams(){{ ttl = 100L; }}, null); - /* clear the cached server time (it is static so shared between library instances) */ - Auth.clearCachedServerTime(); - /* create Ably instance with token details */ DebugOptions opts = new DebugOptions(); fillInOptions(opts); diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestJWTTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestJWTTest.java index 4df8bed8e..f40495676 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestJWTTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestJWTTest.java @@ -52,7 +52,7 @@ public void auth_jwt_request_wrong_keys() { } catch (AblyException e) { assertEquals("Unexpected code from exception", 40144, e.errorInfo.code); assertEquals("Unexpected statusCode from exception", 401, e.errorInfo.statusCode); - assertTrue("Error message not matching the expected one", e.errorInfo.message.contains("Error verifying JWT; err = invalid signature")); + assertTrue("Error message not matching the expected one", e.errorInfo.message.contains("signature verification failed")); } } diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestPushTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestPushTest.java index 457238261..948a27456 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestPushTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestPushTest.java @@ -7,10 +7,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.junit.rules.Timeout; import java.util.Arrays; @@ -639,6 +636,7 @@ public void then(Helpers.AblyFunction