diff --git a/core/src/main/java/io/ably/lib/rest/AblyBase.java b/core/src/main/java/io/ably/lib/rest/AblyBase.java index e19195d4c..15e3ba5a2 100644 --- a/core/src/main/java/io/ably/lib/rest/AblyBase.java +++ b/core/src/main/java/io/ably/lib/rest/AblyBase.java @@ -1,6 +1,7 @@ package io.ably.lib.rest; import io.ably.annotation.Experimental; +import io.ably.lib.debug.DebugOptions; import io.ably.lib.http.AsyncHttpScheduler; import io.ably.lib.http.Http; import io.ably.lib.http.HttpCore; @@ -83,17 +84,22 @@ public AblyBase(ClientOptions options, PlatformAgentProvider platformAgentProvid Log.e(getClass().getName(), msg); throw AblyException.fromErrorInfo(new ErrorInfo(msg, 400, 40000)); } - this.options = options; + + if (options instanceof DebugOptions) { + this.options = options; + } else { + this.options = options.copy(); + } /* process options */ - Log.setLevel(options.logLevel); - Log.setHandler(options.logHandler); + Log.setLevel(this.options.logLevel); + Log.setHandler(this.options.logHandler); Log.i(getClass().getName(), "started"); this.platformAgentProvider = platformAgentProvider; - auth = new Auth(this, options); - httpCore = new HttpCore(options, auth, this.platformAgentProvider); - http = new Http(new AsyncHttpScheduler(httpCore, options), new SyncHttpScheduler(httpCore)); + auth = new Auth(this, this.options); + httpCore = new HttpCore(this.options, auth, this.platformAgentProvider); + http = new Http(new AsyncHttpScheduler(httpCore, this.options), new SyncHttpScheduler(httpCore)); channels = (Channels) new InternalChannels(); diff --git a/core/src/main/java/io/ably/lib/rest/Auth.java b/core/src/main/java/io/ably/lib/rest/Auth.java index c9d7eec2f..73a98be4a 100644 --- a/core/src/main/java/io/ably/lib/rest/Auth.java +++ b/core/src/main/java/io/ably/lib/rest/Auth.java @@ -269,6 +269,10 @@ public String asJson() { */ @Override public boolean equals(Object obj) { + if (!(obj instanceof TokenDetails)) { + return false; + } + TokenDetails details = (TokenDetails)obj; return equalNullableStrings(this.token, details.token) & equalNullableStrings(this.capability, details.capability) & @@ -360,7 +364,7 @@ private TokenParams storedValues() { * * @return copied object */ - private TokenParams copy() { + public TokenParams copy() { TokenParams result = new TokenParams(); result.ttl = this.ttl; result.capability = this.capability; diff --git a/core/src/main/java/io/ably/lib/types/ClientOptions.java b/core/src/main/java/io/ably/lib/types/ClientOptions.java index c97a426ee..9f095b3f8 100644 --- a/core/src/main/java/io/ably/lib/types/ClientOptions.java +++ b/core/src/main/java/io/ably/lib/types/ClientOptions.java @@ -1,12 +1,15 @@ package io.ably.lib.types; import io.ably.lib.push.Storage; +import io.ably.lib.rest.Auth; import io.ably.lib.rest.Auth.AuthOptions; import io.ably.lib.rest.Auth.TokenParams; import io.ably.lib.transport.Defaults; import io.ably.lib.util.Log; import io.ably.lib.util.Log.LogHandler; +import java.util.Arrays; +import java.util.HashMap; import java.util.Map; /** @@ -220,4 +223,95 @@ public ClientOptions(String key) throws AblyException { * Agent versions are optional, if you don't want to specify it pass `null` as the map entry value. */ public Map agents; + + /** + * Creates new copy of this object + * @return copy of ClientOptions + */ + public ClientOptions copy() { + final ClientOptions copyOptions = new ClientOptions(); + copyOptions.clientId = this.clientId; + copyOptions.logLevel = this.logLevel; + copyOptions.logHandler = this.logHandler; + copyOptions.tls = this.tls; + copyOptions.restHost = this.restHost; + copyOptions.realtimeHost = this.realtimeHost; + copyOptions.port = this.port; + copyOptions.tlsPort = this.tlsPort; + copyOptions.autoConnect = this.autoConnect; + copyOptions.useBinaryProtocol = this.useBinaryProtocol; + copyOptions.queueMessages = this.queueMessages; + copyOptions.echoMessages = this.echoMessages; + copyOptions.recover = this.recover; + copyOptions.idempotentRestPublishing = this.idempotentRestPublishing; + copyOptions.httpOpenTimeout = this.httpOpenTimeout; + copyOptions.httpRequestTimeout = this.httpRequestTimeout; + copyOptions.httpMaxRetryCount = this.httpMaxRetryCount; + copyOptions.realtimeRequestTimeout = this.realtimeRequestTimeout; + copyOptions.fallbackHostsUseDefault = this.fallbackHostsUseDefault; + copyOptions.fallbackRetryTimeout = this.fallbackRetryTimeout; + copyOptions.defaultTokenParams = this.defaultTokenParams.copy(); + copyOptions.channelRetryTimeout = this.channelRetryTimeout; + copyOptions.asyncHttpThreadpoolSize = this.asyncHttpThreadpoolSize; + copyOptions.pushFullWait = this.pushFullWait; + copyOptions.localStorage = this.localStorage; + copyOptions.addRequestIds = this.addRequestIds; + copyOptions.environment = this.environment; + + //params from AuthOptions + copyOptions.authCallback = this.authCallback; + copyOptions.authUrl = this.authUrl; + copyOptions.authMethod = this.authMethod; + copyOptions.key = this.key; + copyOptions.token = this.token; + copyOptions.queryTime = this.queryTime; + copyOptions.useTokenAuth = this.useTokenAuth; + + if (this.headers != null) { + copyOptions.headers = new HashMap<>(this.headers); + } + + if (this.agents != null) { + copyOptions.agents = new HashMap<>(this.agents); + } + + if (this.authParams != null) { + copyOptions.authParams = Arrays.copyOf(this.authParams, this.authParams.length); + } + + if (this.authHeaders != null) { + copyOptions.authHeaders = Arrays.copyOf(this.authHeaders, this.authHeaders.length); + } + + if (this.transportParams != null) { + copyOptions.transportParams = Arrays.copyOf(this.transportParams, this.transportParams.length); + } + + if (this.fallbackHosts != null) { + copyOptions.fallbackHosts = Arrays.copyOf(this.fallbackHosts, this.fallbackHosts.length); + } + + if (this.proxy != null) { + ProxyOptions po = new ProxyOptions(); + po.host = this.proxy.host; + po.port = this.proxy.port; + po.username = this.proxy.username; + po.password = this.proxy.password; + po.nonProxyHosts = this.proxy.nonProxyHosts; + po.prefAuthType = this.proxy.prefAuthType; + copyOptions.proxy = po; + } + + if (this.tokenDetails != null) { + Auth.TokenDetails tokenDetails = new Auth.TokenDetails(); + tokenDetails.token = this.tokenDetails.token; + tokenDetails.expires = this.tokenDetails.expires; + tokenDetails.issued = this.tokenDetails.issued; + tokenDetails.capability = this.tokenDetails.capability; + tokenDetails.clientId = this.tokenDetails.clientId; + copyOptions.tokenDetails = tokenDetails; + } + + return copyOptions; + } } diff --git a/core/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java b/core/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java index 13bb55020..3c2a72c01 100644 --- a/core/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java +++ b/core/src/test/java/io/ably/lib/test/realtime/RealtimeAuthTest.java @@ -24,17 +24,25 @@ import io.ably.lib.types.Message; import io.ably.lib.types.Param; import io.ably.lib.types.ProtocolMessage; +import io.ably.lib.types.ProxyOptions; +import io.ably.lib.util.Log; + import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; +import static org.junit.Assert.assertArrayEquals; 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.util.HashMap; +import java.util.Map; + public abstract class RealtimeAuthTest extends ParameterizedTest { @Rule @@ -956,4 +964,115 @@ public Object getTokenRequest(Auth.TokenParams params) throws AblyException { } } + /** + * Verify that instance of ClientOptions cannot change AblyRealtime options once it is provided to constructor + */ + @Test + public void auth_client_options_immutable() { + try { + /* create token with clientId */ + ClientOptions optsForToken = createOptions(testVars.keys[0].keyStr); + optsForToken.clientId = "token_clientId"; + AblyBase ablyForToken = createAblyRest(optsForToken); + TokenDetails tokenDetails = ablyForToken.auth.requestToken(null, null); + + /* create ably realtime */ + ClientOptions opts = new ClientOptions(); + opts.clientId = null; + opts.token = tokenDetails.token; + opts.autoConnect = false; + opts.headers = new HashMap<>(); + opts.headers.put("old_key", "old_value"); + opts.tokenDetails = new TokenDetails("my_old_details_token"); + opts.logHandler = new Log.DefaultHandler(); + opts.authCallback = new Auth.TokenCallback() { + @Override + public Object getTokenRequest(Auth.TokenParams params) { + return null; + } + }; + opts.authHeaders = new Param[1]; + opts.authHeaders[0] = new Param("old_key", "old_key"); + opts.proxy = new ProxyOptions(); + opts.proxy.host = "https://ably.com"; + opts.proxy.port = 8080; + AblyRealtimeBase ablyRealtime = createAblyRealtime(opts); + + assertEquals("clientId given for options must be equal to the one in in AblyRealtime before change", opts.clientId, ablyRealtime.options.clientId); + String origClientId = ablyRealtime.options.clientId; + opts.clientId = "my_new_clientId"; + assertNotEquals("clientId given for options should not be equal to the one in in AblyRealtime after change", opts.clientId, ablyRealtime.options.clientId); + assertEquals("clientId in AblyRealtime should be equal to original one after ClientOptions change", origClientId, ablyRealtime.options.clientId); + + assertEquals("logLevel given for options must be equal to the one in in AblyRealtime before change", opts.logLevel, ablyRealtime.options.logLevel); + int origLogLevel = ablyRealtime.options.logLevel; + opts.logLevel = 33; + assertNotEquals("logLevel given for options should not be equal to the one in in AblyRealtime after change", opts.logLevel, ablyRealtime.options.logLevel); + assertEquals("logLevel in AblyRealtime should be equal to original one after ClientOptions change", origLogLevel, ablyRealtime.options.logLevel); + + assertEquals("autoConnect given for options must be equal to the one in in AblyRealtime before change", opts.autoConnect, ablyRealtime.options.autoConnect); + boolean origAutoConnect = ablyRealtime.options.autoConnect; + opts.autoConnect = true; + assertNotEquals("autoConnect given for options should not be equal to the one in in AblyRealtime after change", opts.autoConnect, ablyRealtime.options.autoConnect); + assertEquals("autoConnect in AblyRealtime should be equal to original one after ClientOptions change", origAutoConnect, ablyRealtime.options.autoConnect); + + assertEquals("logHandler given for options must be equal to the one in in AblyRealtime before change", opts.logHandler, ablyRealtime.options.logHandler); + Log.LogHandler origHandler = ablyRealtime.options.logHandler; + opts.logHandler = new Log.DefaultHandler(); + assertNotEquals("logHandler given for options should not be equal to the one in in AblyRealtime after change", opts.logHandler, ablyRealtime.options.logHandler); + assertEquals("logHandler in AblyRealtime should be equal to original one after ClientOptions change", origHandler, ablyRealtime.options.logHandler); + + assertEquals("authCallback given for options must be equal to the one in in AblyRealtime before change", opts.authCallback, ablyRealtime.options.authCallback); + Auth.TokenCallback origTokenCallback = ablyRealtime.options.authCallback; + opts.authCallback = new Auth.TokenCallback() { + @Override + public Object getTokenRequest(Auth.TokenParams params) { + return null; + } + }; + assertNotEquals("authCallback given for options should not be equal to the one in in AblyRealtime after change", opts.authCallback, ablyRealtime.options.authCallback); + assertEquals("authCallback in AblyRealtime should be equal to original one after ClientOptions change", origTokenCallback, ablyRealtime.options.authCallback); + + assertEquals("token given for options must be equal to the one in in AblyRealtime before change", opts.token, ablyRealtime.options.token); + String origToken = ablyRealtime.options.token; + opts.token = "my_new_token"; + assertNotEquals("token given for options should not be equal to the one in in AblyRealtime after change", opts.token, ablyRealtime.options.token); + assertEquals("token in AblyRealtime should be equal to original one after ClientOptions change", origToken, ablyRealtime.options.token); + + assertEquals("tokenDetails given for options must be equal to the one in in AblyRealtime before change", opts.tokenDetails, ablyRealtime.options.tokenDetails); + TokenDetails origTokenDetails = ablyRealtime.options.tokenDetails; + opts.tokenDetails = new TokenDetails("my_new_details_token"); + assertNotEquals("tokenDetails given for options should not be equal to the one in in AblyRealtime after change", opts.tokenDetails, ablyRealtime.options.tokenDetails); + assertEquals("tokenDetails in AblyRealtime should be equal to original one after ClientOptions change", origTokenDetails, ablyRealtime.options.tokenDetails); + + assertEquals("headers given for options must be equal to the one in in AblyRealtime before change", opts.headers, ablyRealtime.options.headers); + Map origHeaders = ablyRealtime.options.headers; + String origHeaderElementValue = origHeaders.entrySet().iterator().next().getValue(); + opts.headers.clear(); + opts.headers.put("new_key", "new_value"); + assertNotEquals("headers given for options should not be equal to the one in in AblyRealtime after change", opts.headers, ablyRealtime.options.headers); + assertEquals("headers in AblyRealtime should be equal to original one after ClientOptions change", origHeaders, ablyRealtime.options.headers); + assertEquals("first header in AblyRealtime should be equal to original one after ClientOptions change", origHeaderElementValue, ablyRealtime.options.headers.entrySet().iterator().next().getValue()); + + assertArrayEquals("authHeaders given for options must be equal to the one in in AblyRealtime before change", opts.authHeaders, ablyRealtime.options.authHeaders); + Param[] origParams = ablyRealtime.options.authHeaders; + String origParamValue = ablyRealtime.options.authHeaders[0].value; + opts.authHeaders[0] = new Param("new_key", "new_key"); + assertNotEquals("authHeaders given for options should not be equal to the one in in AblyRealtime after change", opts.authHeaders, ablyRealtime.options.authHeaders); + assertArrayEquals("authHeaders in AblyRealtime should be equal to original one after ClientOptions change", origParams, ablyRealtime.options.authHeaders); + assertEquals("first authHeader in AblyRealtime should be equal to original one after ClientOptions change", origParamValue, ablyRealtime.options.authHeaders[0].value); + + assertEquals("proxy.port given for options must be equal to the one in in AblyRealtime before change", opts.proxy.port, ablyRealtime.options.proxy.port); + int origProxyPort = ablyRealtime.options.proxy.port; + opts.proxy.port = 9090; + assertNotEquals("proxy.port given for options should not be equal to the one in in AblyRealtime after change", opts.proxy.port, ablyRealtime.options.proxy.port); + assertEquals("proxy.port in AblyRealtime should be equal to original one after ClientOptions change", origProxyPort, ablyRealtime.options.proxy.port); + + ablyRealtime.close(); + } catch (AblyException e) { + e.printStackTrace(); + fail(); + } + } + }