Skip to content

Commit

Permalink
[CHAT-5063] feat: sdk proxy wrapper for agent tracking
Browse files Browse the repository at this point in the history
We want to be able to distinguish requests made inside the wrapper from those made by the core PubSub SDK without the wrapper, allowing us to track agents across wrapper SDKs such as the Chat SDK or Asset Tracking. To achieve this, we introduce special proxy Realtime and REST clients that inject additional `DerivedClientOptions` parameters into the underlying SDK.
  • Loading branch information
ttypic committed Feb 5, 2025
1 parent 6f8fa54 commit a06e5ba
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 3 deletions.
16 changes: 16 additions & 0 deletions android/src/main/java/io/ably/lib/rest/AblyRest.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ public AblyRest(ClientOptions options) throws AblyException {
super(options, new AndroidPlatformAgentProvider());
}

/**
* Constructor implementation to be able to have proxy based on this class
*/
protected AblyRest(AblyRest underlyingClient, DerivedClientOptions derivedOptions) {
super(underlyingClient, derivedOptions);
}

/**
* Retrieves a {@link LocalDevice} object that represents the current state of the device as a target for push notifications.
* <p>
Expand All @@ -56,6 +63,15 @@ public void setAndroidContext(Context context) throws AblyException {
this.push.tryRequestRegistrationToken();
}

/**
* [Internal Method]
* <p/>
* We use this method to implement proxy Realtime / Rest clients that add additional data to the underlying client.
*/
public AblyRest createDerivedClient(DerivedClientOptions derivedOptions) {
return new AblyRest(this, derivedOptions);
}

/**
* clientId set by late initialisation
*/
Expand Down
16 changes: 16 additions & 0 deletions java/src/main/java/io/ably/lib/rest/AblyRest.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,20 @@ public AblyRest(String key) throws AblyException {
public AblyRest(ClientOptions options) throws AblyException {
super(options, new JavaPlatformAgentProvider());
}

/**
* Constructor implementation to be able to have proxy based on this class
*/
protected AblyRest(AblyRest underlyingClient, DerivedClientOptions derivedOptions) {
super(underlyingClient, derivedOptions);
}

/**
* [Internal Method]
* <p/>
* We use this method to implement proxy Realtime / Rest clients that add additional data to the underlying client.
*/
public AblyRest createDerivedClient(DerivedClientOptions derivedOptions) {
return new AblyRest(this, derivedOptions);
}
}
13 changes: 13 additions & 0 deletions lib/src/main/java/io/ably/lib/http/AsyncHttpScheduler.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,23 @@ public AsyncHttpScheduler(HttpCore httpCore, ClientOptions options) {
super(httpCore, new CloseableThreadPoolExecutor(options));
}

private AsyncHttpScheduler(HttpCore httpCore, CloseableExecutor executor) {
super(httpCore, executor);
}

private static final long KEEP_ALIVE_TIME = 2000L;

protected static final String TAG = AsyncHttpScheduler.class.getName();

/**
* [Internal Method]
* <p>
* We use this method to implement proxy Realtime / Rest clients that add additional data to the underlying client.
*/
public AsyncHttpScheduler exchangeHttpCore(HttpCore httpCore) {
return new AsyncHttpScheduler(httpCore, this.executor);
}

private static class CloseableThreadPoolExecutor implements CloseableExecutor {
private final ThreadPoolExecutor executor;

Expand Down
9 changes: 9 additions & 0 deletions lib/src/main/java/io/ably/lib/http/Http.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ public void close() throws Exception {
asyncHttp.close();
}

/**
* [Internal Method]
* <p>
* We use this method to implement proxy Realtime / Rest clients that add additional data to the underlying client.
*/
public Http exchangeHttpCore(HttpCore httpCore) {
return new Http(asyncHttp.exchangeHttpCore(httpCore), new SyncHttpScheduler(httpCore));
}

public class Request<Result> {
private final Execute<Result> execute;

Expand Down
26 changes: 25 additions & 1 deletion lib/src/main/java/io/ably/lib/http/HttpCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.ably.lib.network.HttpRequest;
import io.ably.lib.network.HttpResponse;
import io.ably.lib.rest.Auth;
import io.ably.lib.rest.DerivedClientOptions;
import io.ably.lib.transport.Defaults;
import io.ably.lib.transport.Hosts;
import io.ably.lib.types.AblyException;
Expand Down Expand Up @@ -68,6 +69,8 @@ public class HttpCore {
private final HttpEngine engine;
private HttpAuth proxyAuth;

private DerivedClientOptions derivedOptions;

/*************************
* Public API
*************************/
Expand Down Expand Up @@ -103,6 +106,18 @@ public HttpCore(ClientOptions options, Auth auth, PlatformAgentProvider platform
this.engine = engineFactory.create(new HttpEngineConfig(ClientOptionsUtils.convertToProxyConfig(options)));
}

private HttpCore(HttpCore underlyingHttpCore, DerivedClientOptions derivedOptions) {
this.options = underlyingHttpCore.options;
this.auth = underlyingHttpCore.auth;
this.platformAgentProvider = underlyingHttpCore.platformAgentProvider;
this.scheme = underlyingHttpCore.scheme;
this.port = underlyingHttpCore.port;
this.hosts = underlyingHttpCore.hosts;
this.proxyAuth = underlyingHttpCore.proxyAuth;
this.engine = underlyingHttpCore.engine;
this.derivedOptions = derivedOptions;
}

/**
* Make a synchronous HTTP request specified by URL and proxy, retrying if necessary on WWW-Authenticate
*
Expand Down Expand Up @@ -307,7 +322,7 @@ private Map<String, String> collectRequestHeaders(URL url, String method, Param[

/* pass required headers */
requestHeaders.put(Defaults.ABLY_PROTOCOL_VERSION_HEADER, Defaults.ABLY_PROTOCOL_VERSION); // RSC7a
requestHeaders.put(Defaults.ABLY_AGENT_HEADER, AgentHeaderCreator.create(options.agents, platformAgentProvider));
requestHeaders.put(Defaults.ABLY_AGENT_HEADER, AgentHeaderCreator.create(options.agents, platformAgentProvider, derivedOptions));
if (options.clientId != null)
requestHeaders.put(Defaults.ABLY_CLIENT_ID_HEADER, Base64Coder.encodeString(options.clientId));

Expand Down Expand Up @@ -455,6 +470,15 @@ private Response executeRequest(HttpRequest request) {
return response;
}

/**
* [Internal Method]
* <p>
* We use this method to implement proxy Realtime / Rest clients that add additional data to the underlying client.
*/
public HttpCore applyDerivedOptions(DerivedClientOptions derivedOptions) {
return new HttpCore(this, derivedOptions);
}

/**
* Interface for an entity that supplies an httpCore request body
*/
Expand Down
2 changes: 1 addition & 1 deletion lib/src/main/java/io/ably/lib/http/HttpScheduler.java
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ public <T> Future<T> ablyHttpExecuteWithRetry(
return request;
}

private final CloseableExecutor executor;
protected final CloseableExecutor executor;
private final HttpCore httpCore;

protected static final String TAG = HttpScheduler.class.getName();
Expand Down
19 changes: 19 additions & 0 deletions lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import io.ably.lib.rest.AblyRest;
import io.ably.lib.rest.Auth;
import io.ably.lib.rest.DerivedClientOptions;
import io.ably.lib.transport.ConnectionManager;
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ChannelOptions;
Expand Down Expand Up @@ -83,6 +84,15 @@ public void onConnectionStateChanged(ConnectionStateListener.ConnectionStateChan
if(options.autoConnect) connection.connect();
}

/**
* Package-private constructor implementation to be able to have proxy based on this class
*/
AblyRealtime(AblyRealtime underlyingClient, DerivedClientOptions derivedOptions) {
super(underlyingClient, derivedOptions);
this.channels = underlyingClient.channels;
this.connection = underlyingClient.connection;
}

/**
* Calls {@link Connection#connect} and causes the connection to open,
* entering the connecting state. Explicitly calling connect() is unnecessary
Expand Down Expand Up @@ -118,6 +128,15 @@ public void close() {
connection.close();
}

/**
* [Internal Method]
* <p/>
* We use this method to implement proxy Realtime / Rest clients that add additional data to the underlying client.
*/
public AblyRealtime createDerivedClient(DerivedClientOptions derivedOptions) {
return new AblyRealtime(this, derivedOptions);
}

/**
* Authentication token has changed.
*/
Expand Down
14 changes: 14 additions & 0 deletions lib/src/main/java/io/ably/lib/rest/AblyBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ public AblyBase(ClientOptions options, PlatformAgentProvider platformAgentProvid
push = new Push(this);
}

/**
* We use empty constructor to be able to create proxy implementation of Realtime and Rest client
*/
protected AblyBase(AblyBase underlyingClient, DerivedClientOptions derivedOptions) {
this.options = underlyingClient.options;
this.auth = underlyingClient.auth;
this.httpCore = underlyingClient.httpCore.applyDerivedOptions(derivedOptions);
this.http = underlyingClient.http.exchangeHttpCore(this.httpCore);
this.platform = underlyingClient.platform;
this.push = underlyingClient.push;
this.channels = underlyingClient.channels;
this.platformAgentProvider = underlyingClient.platformAgentProvider;
}

/**
* Causes the connection to close, entering the [{@link io.ably.lib.realtime.ConnectionState#closing} state.
* Once closed, the library does not attempt to re-establish the connection without an explicit call to
Expand Down
33 changes: 33 additions & 0 deletions lib/src/main/java/io/ably/lib/rest/DerivedClientOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.ably.lib.rest;

import java.util.HashMap;
import java.util.Map;

public final class DerivedClientOptions {
private final Map<String, String> agents;

DerivedClientOptions(Map<String, String> agents) {
this.agents = agents;
}

public static DerivedClientOptionsBuilder builder() {
return new DerivedClientOptionsBuilder();
}

public Map<String, String> getAgents() {
return this.agents;
}

public static class DerivedClientOptionsBuilder {
private final Map<String, String> agents = new HashMap<>();

public DerivedClientOptionsBuilder addAgent(String agent, String version) {
this.agents.put(agent, version);
return this;
}

public DerivedClientOptions build() {
return new DerivedClientOptions(this.agents);
}
}
}
17 changes: 16 additions & 1 deletion lib/src/main/java/io/ably/lib/util/AgentHeaderCreator.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.ably.lib.util;

import io.ably.lib.rest.DerivedClientOptions;
import io.ably.lib.transport.Defaults;

import java.util.Map;
Expand All @@ -15,7 +16,7 @@ public class AgentHeaderCreator {
*/
public static final String AGENT_DIVIDER = "/";

public static String create(Map<String, String> additionalAgents, PlatformAgentProvider platformAgentProvider) {
public static String create(Map<String, String> additionalAgents, PlatformAgentProvider platformAgentProvider, DerivedClientOptions derivedOptions) {
StringBuilder agentStringBuilder = new StringBuilder();
agentStringBuilder.append(Defaults.ABLY_AGENT_VERSION);
if (additionalAgents != null && !additionalAgents.isEmpty()) {
Expand All @@ -27,9 +28,23 @@ public static String create(Map<String, String> additionalAgents, PlatformAgentP
agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
agentStringBuilder.append(platformAgent);
}

if (derivedOptions != null) {
derivedOptions.getAgents().entrySet().forEach(entry -> {
agentStringBuilder.append(AGENT_ENTRY_SEPARATOR);
agentStringBuilder.append(entry.getKey());
agentStringBuilder.append(AGENT_DIVIDER);
agentStringBuilder.append(entry.getValue());
});
}

return agentStringBuilder.toString();
}

public static String create(Map<String, String> additionalAgents, PlatformAgentProvider platformAgentProvider) {
return create(additionalAgents, platformAgentProvider, null);
}

private static String getAdditionalAgentEntries(Map<String, String> additionalAgents) {
StringBuilder additionalAgentsBuilder = new StringBuilder();
for (String additionalAgentName : additionalAgents.keySet()) {
Expand Down
Loading

0 comments on commit a06e5ba

Please sign in to comment.