From 095ae82483acd4c3474aa6c0072caedbdb33cbc6 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 19 Mar 2025 11:03:48 -0400 Subject: [PATCH] Convert remote config apis to WebSocket. --- .../securesms/dependencies/AppDependencies.kt | 5 ++ .../ApplicationDependencyProvider.java | 6 +++ .../dependencies/NetworkDependenciesModule.kt | 5 ++ .../jobs/BuildExpirationConfirmationJob.kt | 13 ++--- .../jobs/RemoteConfigRefreshJob.java | 11 ++-- .../securesms/net/SignalNetwork.kt | 6 +++ .../securesms/util/RemoteConfig.kt | 4 +- .../MockApplicationDependencyProvider.kt | 5 ++ .../api/SignalServiceAccountManager.java | 14 ----- .../api/remoteconfig/RemoteConfigApi.kt | 54 +++++++++++++++++++ .../remoteconfig}/RemoteConfigResponse.java | 18 +++---- .../{ => remoteconfig}/RemoteConfigResult.kt | 4 +- .../internal/push/PushServiceSocket.java | 5 -- 13 files changed, 106 insertions(+), 44 deletions(-) create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/remoteconfig/RemoteConfigApi.kt rename libsignal-service/src/main/java/org/whispersystems/signalservice/{internal/push => api/remoteconfig}/RemoteConfigResponse.java (70%) rename libsignal-service/src/main/java/org/whispersystems/signalservice/api/{ => remoteconfig}/RemoteConfigResult.kt (61%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt index d4e1b534d6..22db0c8ba4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt @@ -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 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 59b7d12242..f4c70fbf61 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt index 8c9c6348d6..718e325f73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt @@ -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()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BuildExpirationConfirmationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BuildExpirationConfirmationJob.kt index 3d30bc330f..e4f5898579 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BuildExpirationConfirmationJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BuildExpirationConfirmationJob.kt @@ -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 = NetworkResult.fromFetch { - AppDependencies.signalServiceAccountManager.remoteConfig - } - - return when (result) { + return when (val result: NetworkResult = 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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteConfigRefreshJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteConfigRefreshJob.java index 277859d613..528821068d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteConfigRefreshJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteConfigRefreshJob.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/SignalNetwork.kt b/app/src/main/java/org/thoughtcrime/securesms/net/SignalNetwork.kt index dd9a52bb73..e86f977a46 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/net/SignalNetwork.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/net/SignalNetwork.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt index 7e52ad15c5..8a81e5193c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt @@ -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) } diff --git a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt index ca98ca8dc9..684c608194 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt @@ -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) + } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 3703ba47b1..0cffceabcd 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -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 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(); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/remoteconfig/RemoteConfigApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/remoteconfig/RemoteConfigApi.kt new file mode 100644 index 0000000000..08ba54e90c --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/remoteconfig/RemoteConfigApi.kt @@ -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 { + 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 { + override fun convert(response: WebsocketResponse): NetworkResult { + 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() + ) + ) + } + } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RemoteConfigResponse.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/remoteconfig/RemoteConfigResponse.java similarity index 70% rename from libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RemoteConfigResponse.java rename to libsignal-service/src/main/java/org/whispersystems/signalservice/api/remoteconfig/RemoteConfigResponse.java index dc055f5196..a7c5cb5a83 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RemoteConfigResponse.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/remoteconfig/RemoteConfigResponse.java @@ -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; - @JsonProperty - private long serverEpochTime; - public List 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; } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/RemoteConfigResult.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/remoteconfig/RemoteConfigResult.kt similarity index 61% rename from libsignal-service/src/main/java/org/whispersystems/signalservice/api/RemoteConfigResult.kt rename to libsignal-service/src/main/java/org/whispersystems/signalservice/api/remoteconfig/RemoteConfigResult.kt index aa86d7008b..8c9a71091b 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/RemoteConfigResult.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/remoteconfig/RemoteConfigResult.kt @@ -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, - val serverEpochTimeSeconds: Long + val serverEpochTimeMilliseconds: Long ) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 794283f35d..018925b5c9 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -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());