Convert remote config apis to WebSocket.

This commit is contained in:
Cody Henthorne
2025-03-19 11:03:48 -04:00
parent 1e866a1e86
commit 095ae82483
13 changed files with 106 additions and 44 deletions

View File

@@ -57,6 +57,7 @@ import org.whispersystems.signalservice.api.profiles.ProfileApi
import org.whispersystems.signalservice.api.provisioning.ProvisioningApi
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi
import org.whispersystems.signalservice.api.registration.RegistrationApi
import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigApi
import org.whispersystems.signalservice.api.services.DonationsService
import org.whispersystems.signalservice.api.services.ProfileService
import org.whispersystems.signalservice.api.storage.StorageServiceApi
@@ -346,6 +347,9 @@ object AppDependencies {
val profileApi: ProfileApi
get() = networkModule.profileApi
val remoteConfigApi: RemoteConfigApi
get() = networkModule.remoteConfigApi
@JvmStatic
val okHttpClient: OkHttpClient
get() = networkModule.okHttpClient
@@ -426,5 +430,6 @@ object AppDependencies {
fun provideProvisioningApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): ProvisioningApi
fun provideCertificateApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): CertificateApi
fun provideProfileApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, pushServiceSocket: PushServiceSocket): ProfileApi
fun provideRemoteConfigApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): RemoteConfigApi
}
}

View File

@@ -99,6 +99,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi;
import org.whispersystems.signalservice.api.registration.RegistrationApi;
import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigApi;
import org.whispersystems.signalservice.api.services.DonationsService;
import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
@@ -552,6 +553,11 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
return new ProfileApi(authWebSocket, pushServiceSocket);
}
@Override
public @NonNull RemoteConfigApi provideRemoteConfigApi(@NonNull SignalWebSocket.AuthenticatedWebSocket authWebSocket) {
return new RemoteConfigApi(authWebSocket);
}
@VisibleForTesting
static class DynamicCredentialsProvider implements CredentialsProvider {

View File

@@ -43,6 +43,7 @@ import org.whispersystems.signalservice.api.provisioning.ProvisioningApi
import org.whispersystems.signalservice.api.push.TrustStore
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi
import org.whispersystems.signalservice.api.registration.RegistrationApi
import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigApi
import org.whispersystems.signalservice.api.services.DonationsService
import org.whispersystems.signalservice.api.services.ProfileService
import org.whispersystems.signalservice.api.storage.StorageServiceApi
@@ -208,6 +209,10 @@ class NetworkDependenciesModule(
provider.provideProfileApi(authWebSocket, pushServiceSocket)
}
val remoteConfigApi: RemoteConfigApi by lazy {
provider.provideRemoteConfigApi(authWebSocket)
}
val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.addInterceptor(StandardUserAgentInterceptor())

View File

@@ -7,15 +7,14 @@ package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.RemoteConfigResult
import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigResult
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds
/**
* If we have reason to believe a build is expired, we run this job to double-check by fetching the server time. This prevents false positives from people
@@ -57,13 +56,9 @@ class BuildExpirationConfirmationJob private constructor(params: Parameters) : J
return Result.success()
}
val result: NetworkResult<RemoteConfigResult> = NetworkResult.fromFetch {
AppDependencies.signalServiceAccountManager.remoteConfig
}
return when (result) {
return when (val result: NetworkResult<RemoteConfigResult> = SignalNetwork.remoteConfig.getRemoteConfig()) {
is NetworkResult.Success -> {
val serverTimeMs = result.result.serverEpochTimeSeconds.seconds.inWholeMilliseconds
val serverTimeMs = result.result.serverEpochTimeMilliseconds
SignalStore.misc.setLastKnownServerTime(serverTimeMs, System.currentTimeMillis())
if (Util.getTimeUntilBuildExpiry(serverTimeMs) <= 0) {

View File

@@ -8,8 +8,11 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.SignalNetwork;
import org.thoughtcrime.securesms.util.ExceptionHelper;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.whispersystems.signalservice.api.RemoteConfigResult;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigResult;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.util.concurrent.TimeUnit;
@@ -51,14 +54,14 @@ public class RemoteConfigRefreshJob extends BaseJob {
return;
}
RemoteConfigResult result = AppDependencies.getSignalServiceAccountManager().getRemoteConfig();
RemoteConfigResult result = NetworkResultUtil.toBasicLegacy(SignalNetwork.remoteConfig().getRemoteConfig());
RemoteConfig.update(result.getConfig());
SignalStore.misc().setLastKnownServerTime(TimeUnit.SECONDS.toMillis(result.getServerEpochTimeSeconds()), System.currentTimeMillis());
SignalStore.misc().setLastKnownServerTime(result.getServerEpochTimeMilliseconds(), System.currentTimeMillis());
}
@Override
protected boolean onShouldRetry(@NonNull Exception e) {
return e instanceof PushNetworkException;
return ExceptionHelper.isRetryableIOException(e);
}
@Override

View File

@@ -19,6 +19,7 @@ import org.whispersystems.signalservice.api.payments.PaymentsApi
import org.whispersystems.signalservice.api.profiles.ProfileApi
import org.whispersystems.signalservice.api.provisioning.ProvisioningApi
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi
import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigApi
import org.whispersystems.signalservice.api.storage.StorageServiceApi
import org.whispersystems.signalservice.api.username.UsernameApi
@@ -81,6 +82,11 @@ object SignalNetwork {
val rateLimitChallenge: RateLimitChallengeApi
get() = AppDependencies.rateLimitChallengeApi
@JvmStatic
@get:JvmName("remoteConfig")
val remoteConfig: RemoteConfigApi
get() = AppDependencies.remoteConfigApi
val storageService: StorageServiceApi
get() = AppDependencies.storageServiceApi

View File

@@ -15,9 +15,11 @@ import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob
import org.thoughtcrime.securesms.jobs.Svr3MirrorJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchReceiver
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.util.RemoteConfig.Config
import org.thoughtcrime.securesms.util.RemoteConfig.remoteBoolean
import org.thoughtcrime.securesms.util.RemoteConfig.remoteValue
import org.whispersystems.signalservice.api.NetworkResultUtil
import java.io.IOException
import java.util.TreeMap
import java.util.concurrent.locks.ReentrantLock
@@ -90,7 +92,7 @@ object RemoteConfig {
@WorkerThread
@Throws(IOException::class)
fun refreshSync() {
val result = AppDependencies.signalServiceAccountManager.getRemoteConfig()
val result = NetworkResultUtil.toBasicLegacy(SignalNetwork.remoteConfig.getRemoteConfig())
update(result.config)
}

View File

@@ -51,6 +51,7 @@ import org.whispersystems.signalservice.api.profiles.ProfileApi
import org.whispersystems.signalservice.api.provisioning.ProvisioningApi
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi
import org.whispersystems.signalservice.api.registration.RegistrationApi
import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigApi
import org.whispersystems.signalservice.api.services.DonationsService
import org.whispersystems.signalservice.api.services.ProfileService
import org.whispersystems.signalservice.api.storage.StorageServiceApi
@@ -290,4 +291,8 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
override fun provideProfileApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, pushServiceSocket: PushServiceSocket): ProfileApi {
return mockk(relaxed = true)
}
override fun provideRemoteConfigApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): RemoteConfigApi {
return mockk(relaxed = true)
}
}

View File

@@ -19,13 +19,10 @@ import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.RemoteConfigResponse;
import org.whispersystems.signalservice.internal.push.WhoAmIResponse;
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -115,17 +112,6 @@ public class SignalServiceAccountManager {
pushServiceSocket.requestPushChallenge(sessionId, gcmRegistrationId);
}
public RemoteConfigResult getRemoteConfig() throws IOException {
RemoteConfigResponse response = this.pushServiceSocket.getRemoteConfig();
Map<String, Object> out = new HashMap<>();
for (RemoteConfigResponse.Config config : response.getConfig()) {
out.put(config.getName(), config.getValue() != null ? config.getValue() : config.isEnabled());
}
return new RemoteConfigResult(out, response.getServerEpochTime());
}
public void checkNetworkConnection() throws IOException {
this.pushServiceSocket.pingStorageService();
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.remoteconfig
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
import org.whispersystems.signalservice.internal.get
import org.whispersystems.signalservice.internal.util.JsonUtil
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
import org.whispersystems.signalservice.internal.websocket.WebsocketResponse
/**
* Remote configuration is a list of namespaced keys that clients may use for consistent configuration or behavior.
*
* Configuration values change over time, and the list should be refreshed periodically.
*/
class RemoteConfigApi(val authWebSocket: SignalWebSocket.AuthenticatedWebSocket) {
/**
* Get remote config data from the server.
*
* GET /v1/config
* - 200: Success
*/
fun getRemoteConfig(): NetworkResult<RemoteConfigResult> {
val request = WebSocketRequestMessage.get("/v1/config")
return NetworkResult.fromWebSocketRequest(signalWebSocket = authWebSocket, request = request, webSocketResponseConverter = RemoteConfigResultWebSocketResponseConverter())
}
/**
* Custom converter for [RemoteConfigResult] as it needs the value of the timestamp header to construct the
* complete result, not just the JSON body.
*/
private class RemoteConfigResultWebSocketResponseConverter : NetworkResult.WebSocketResponseConverter<RemoteConfigResult> {
override fun convert(response: WebsocketResponse): NetworkResult<RemoteConfigResult> {
return if (response.status < 200 || response.status > 299) {
response.toStatusCodeError()
} else {
val remoteConfigResponse = JsonUtil.fromJson(response.body, RemoteConfigResponse::class.java)
val transformed = remoteConfigResponse.config.associate { it.name to (it.value ?: it.isEnabled) }
NetworkResult.Success(
RemoteConfigResult(
config = transformed,
serverEpochTimeMilliseconds = response.getHeader(SignalWebSocket.SERVER_DELIVERED_TIMESTAMP_HEADER).toLongOrNull() ?: System.currentTimeMillis()
)
)
}
}
}
}

View File

@@ -1,24 +1,24 @@
package org.whispersystems.signalservice.internal.push;
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.remoteconfig;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import javax.annotation.Nullable;
public class RemoteConfigResponse {
@JsonProperty
private List<Config> config;
@JsonProperty
private long serverEpochTime;
public List<Config> getConfig() {
return config;
}
public long getServerEpochTime() {
return serverEpochTime;
}
public static class Config {
@JsonProperty
private String name;
@@ -37,7 +37,7 @@ public class RemoteConfigResponse {
return enabled;
}
public String getValue() {
public @Nullable String getValue() {
return value;
}
}

View File

@@ -3,9 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api
package org.whispersystems.signalservice.api.remoteconfig
data class RemoteConfigResult(
val config: Map<String, Any>,
val serverEpochTimeSeconds: Long
val serverEpochTimeMilliseconds: Long
)

View File

@@ -718,11 +718,6 @@ public class PushServiceSocket {
}
}
public RemoteConfigResponse getRemoteConfig() throws IOException {
String response = makeServiceRequest("/v1/config", "GET", null);
return JsonUtil.fromJson(response, RemoteConfigResponse.class);
}
public void cancelInFlightRequests() {
synchronized (connections) {
Log.w(TAG, "Canceling: " + connections.size());