mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 19:00:26 +01:00
Use remote config v2.
This commit is contained in:
committed by
Jeffrey Starke
parent
dcce8ea35a
commit
7d35cf1374
@@ -1,77 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.jobs;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
|
||||||
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.NetworkResultUtil;
|
|
||||||
import org.whispersystems.signalservice.api.remoteconfig.RemoteConfigResult;
|
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class RemoteConfigRefreshJob extends BaseJob {
|
|
||||||
|
|
||||||
private static final String TAG = Log.tag(RemoteConfigRefreshJob.class);
|
|
||||||
|
|
||||||
public static final String KEY = "RemoteConfigRefreshJob";
|
|
||||||
|
|
||||||
public RemoteConfigRefreshJob() {
|
|
||||||
this(new Job.Parameters.Builder()
|
|
||||||
.setQueue("RemoteConfigRefreshJob")
|
|
||||||
.setMaxInstancesForFactory(1)
|
|
||||||
.addConstraint(NetworkConstraint.KEY)
|
|
||||||
.setMaxAttempts(Parameters.UNLIMITED)
|
|
||||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private RemoteConfigRefreshJob(@NonNull Parameters parameters) {
|
|
||||||
super(parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable byte[] serialize() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull String getFactoryKey() {
|
|
||||||
return KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onRun() throws Exception {
|
|
||||||
if (!SignalStore.account().isRegistered()) {
|
|
||||||
Log.w(TAG, "Not registered. Skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoteConfigResult result = NetworkResultUtil.toBasicLegacy(SignalNetwork.remoteConfig().getRemoteConfig());
|
|
||||||
RemoteConfig.update(result.getConfig());
|
|
||||||
SignalStore.misc().setLastKnownServerTime(result.getServerEpochTimeMilliseconds(), System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
|
||||||
return ExceptionHelper.isRetryableIOException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class Factory implements Job.Factory<RemoteConfigRefreshJob> {
|
|
||||||
@Override
|
|
||||||
public @NonNull RemoteConfigRefreshJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) {
|
|
||||||
return new RemoteConfigRefreshJob(parameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package org.thoughtcrime.securesms.jobs
|
||||||
|
|
||||||
|
import org.signal.core.util.isNotNullOrBlank
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
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.RemoteConfig
|
||||||
|
import org.whispersystems.signalservice.api.NetworkResult
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Job to refresh remote configs. Utilizes eTags so a 304 is returned if content is unchanged since last fetch.
|
||||||
|
*/
|
||||||
|
class RemoteConfigRefreshJob private constructor(parameters: Parameters) : Job(parameters) {
|
||||||
|
companion object {
|
||||||
|
const val KEY: String = "RemoteConfigRefreshJob"
|
||||||
|
private val TAG = Log.tag(RemoteConfigRefreshJob::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() : this(
|
||||||
|
Parameters.Builder()
|
||||||
|
.setQueue(KEY)
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setMaxInstancesForFactory(1)
|
||||||
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
|
.setLifespan(1.days.inWholeMilliseconds)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun serialize(): ByteArray? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFactoryKey(): String {
|
||||||
|
return KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run(): Result {
|
||||||
|
if (!SignalStore.account.isRegistered) {
|
||||||
|
Log.w(TAG, "Not registered. Skipping.")
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
return when (val result = SignalNetwork.remoteConfig.getRemoteConfig(SignalStore.remoteConfig.eTag)) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
RemoteConfig.update(result.result.config)
|
||||||
|
SignalStore.misc.setLastKnownServerTime(result.result.serverEpochTimeMilliseconds, System.currentTimeMillis())
|
||||||
|
if (result.result.eTag.isNotNullOrBlank()) {
|
||||||
|
SignalStore.remoteConfig.eTag = result.result.eTag
|
||||||
|
}
|
||||||
|
Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkResult.ApplicationError -> Result.failure()
|
||||||
|
is NetworkResult.NetworkError -> Result.retry(defaultBackoff())
|
||||||
|
is NetworkResult.StatusCodeError ->
|
||||||
|
if (result.code == 304) {
|
||||||
|
Log.i(TAG, "Remote config has not changed since last pull.")
|
||||||
|
Result.success()
|
||||||
|
} else {
|
||||||
|
Result.retry(defaultBackoff())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure() = Unit
|
||||||
|
|
||||||
|
class Factory : Job.Factory<RemoteConfigRefreshJob?> {
|
||||||
|
override fun create(parameters: Parameters, serializedData: ByteArray?): RemoteConfigRefreshJob {
|
||||||
|
return RemoteConfigRefreshJob(parameters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ public final class RemoteConfigValues extends SignalStoreValues {
|
|||||||
private static final String CURRENT_CONFIG = "remote_config";
|
private static final String CURRENT_CONFIG = "remote_config";
|
||||||
private static final String PENDING_CONFIG = "pending_remote_config";
|
private static final String PENDING_CONFIG = "pending_remote_config";
|
||||||
private static final String LAST_FETCH_TIME = "remote_config_last_fetch_time";
|
private static final String LAST_FETCH_TIME = "remote_config_last_fetch_time";
|
||||||
|
private static final String ETAG = "etag";
|
||||||
|
|
||||||
RemoteConfigValues(@NonNull KeyValueStore store) {
|
RemoteConfigValues(@NonNull KeyValueStore store) {
|
||||||
super(store);
|
super(store);
|
||||||
@@ -51,4 +52,12 @@ public final class RemoteConfigValues extends SignalStoreValues {
|
|||||||
public void setLastFetchTime(long time) {
|
public void setLastFetchTime(long time) {
|
||||||
putLong(LAST_FETCH_TIME, time);
|
putLong(LAST_FETCH_TIME, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getETag() {
|
||||||
|
return getString(ETAG, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setETag(String etag) {
|
||||||
|
putString(ETAG, etag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ class SignalStore(context: Application, private val store: KeyValueStore) {
|
|||||||
val pin: PinValues
|
val pin: PinValues
|
||||||
get() = instance!!.pinValues
|
get() = instance!!.pinValues
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@get:JvmName("remoteConfig")
|
||||||
val remoteConfig: RemoteConfigValues
|
val remoteConfig: RemoteConfigValues
|
||||||
get() = instance!!.remoteConfigValues
|
get() = instance!!.remoteConfigValues
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
|||||||
import org.whispersystems.signalservice.internal.util.JsonUtil
|
import org.whispersystems.signalservice.internal.util.JsonUtil
|
||||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
||||||
import org.whispersystems.signalservice.internal.websocket.WebsocketResponse
|
import org.whispersystems.signalservice.internal.websocket.WebsocketResponse
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remote configuration is a list of namespaced keys that clients may use for consistent configuration or behavior.
|
* Remote configuration is a list of namespaced keys that clients may use for consistent configuration or behavior.
|
||||||
@@ -23,19 +24,22 @@ class RemoteConfigApi(val authWebSocket: SignalWebSocket.AuthenticatedWebSocket,
|
|||||||
/**
|
/**
|
||||||
* Get remote config data from the server.
|
* Get remote config data from the server.
|
||||||
*
|
*
|
||||||
* GET /v1/config
|
* GET /v2/config
|
||||||
* - 200: Success
|
* - 200: Success
|
||||||
|
* - 304: No changes since the last fetch
|
||||||
|
* - 401: Requires authentication
|
||||||
*/
|
*/
|
||||||
fun getRemoteConfig(): NetworkResult<RemoteConfigResult> {
|
fun getRemoteConfig(eTag: String = ""): NetworkResult<RemoteConfigResult> {
|
||||||
val request = WebSocketRequestMessage.get("/v1/config")
|
val headers = if (eTag.isNotEmpty()) mapOf("If-None-Match" to eTag) else mapOf()
|
||||||
|
val request = WebSocketRequestMessage.get("/v2/config", headers = headers)
|
||||||
return NetworkResult.fromWebSocketRequest(signalWebSocket = authWebSocket, request = request, webSocketResponseConverter = RemoteConfigResultWebSocketResponseConverter())
|
return NetworkResult.fromWebSocketRequest(signalWebSocket = authWebSocket, request = request, webSocketResponseConverter = RemoteConfigResultWebSocketResponseConverter())
|
||||||
.fallback {
|
.fallback(predicate = { it is NetworkResult.StatusCodeError && it.code != 304 }) {
|
||||||
NetworkResult.fromFetch {
|
NetworkResult.fromFetch {
|
||||||
val response = pushServiceSocket.getRemoteConfig()
|
val response = pushServiceSocket.getRemoteConfig()
|
||||||
val transformed = response.config.associate { it.name to (it.value ?: it.isEnabled) }
|
val transformed = response.config.map { it.key to (it.value.lowercase(Locale.getDefault()).toBooleanStrictOrNull() ?: it.value) }.toMap()
|
||||||
RemoteConfigResult(
|
RemoteConfigResult(
|
||||||
config = transformed,
|
config = transformed,
|
||||||
serverEpochTimeMilliseconds = response.serverEpochTime.takeIf { it > 0 }?.times(1000) ?: System.currentTimeMillis()
|
serverEpochTimeMilliseconds = response.serverEpochTime
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,12 +55,13 @@ class RemoteConfigApi(val authWebSocket: SignalWebSocket.AuthenticatedWebSocket,
|
|||||||
response.toStatusCodeError()
|
response.toStatusCodeError()
|
||||||
} else {
|
} else {
|
||||||
val remoteConfigResponse = JsonUtil.fromJson(response.body, RemoteConfigResponse::class.java)
|
val remoteConfigResponse = JsonUtil.fromJson(response.body, RemoteConfigResponse::class.java)
|
||||||
val transformed = remoteConfigResponse.config.associate { it.name to (it.value ?: it.isEnabled) }
|
val transformed = remoteConfigResponse.config.map { it.key to (it.value.lowercase(Locale.getDefault()).toBooleanStrictOrNull() ?: it.value) }.toMap()
|
||||||
|
|
||||||
NetworkResult.Success(
|
NetworkResult.Success(
|
||||||
RemoteConfigResult(
|
RemoteConfigResult(
|
||||||
config = transformed,
|
config = transformed,
|
||||||
serverEpochTimeMilliseconds = response.getHeader(SignalWebSocket.SERVER_DELIVERED_TIMESTAMP_HEADER).toLongOrNull() ?: System.currentTimeMillis()
|
serverEpochTimeMilliseconds = response.getHeader(SignalWebSocket.SERVER_DELIVERED_TIMESTAMP_HEADER).toLongOrNull() ?: System.currentTimeMillis(),
|
||||||
|
eTag = response.headers["etag"]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
private boolean enabled;
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
private String value;
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable String getValue() {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package org.whispersystems.signalservice.api.remoteconfig
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response class used in /v2/config. [serverEpochTime] should only be used in REST calls.
|
||||||
|
*/
|
||||||
|
data class RemoteConfigResponse(
|
||||||
|
@JsonProperty
|
||||||
|
val config: Map<String, String> = emptyMap(),
|
||||||
|
var serverEpochTime: Long = 0
|
||||||
|
)
|
||||||
@@ -7,5 +7,6 @@ package org.whispersystems.signalservice.api.remoteconfig
|
|||||||
|
|
||||||
data class RemoteConfigResult(
|
data class RemoteConfigResult(
|
||||||
val config: Map<String, Any>,
|
val config: Map<String, Any>,
|
||||||
val serverEpochTimeMilliseconds: Long
|
val serverEpochTimeMilliseconds: Long,
|
||||||
|
val eTag: String? = ""
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ import okhttp3.Call;
|
|||||||
import okhttp3.ConnectionPool;
|
import okhttp3.ConnectionPool;
|
||||||
import okhttp3.ConnectionSpec;
|
import okhttp3.ConnectionSpec;
|
||||||
import okhttp3.Dns;
|
import okhttp3.Dns;
|
||||||
|
import okhttp3.Headers;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
import okhttp3.Interceptor;
|
import okhttp3.Interceptor;
|
||||||
import okhttp3.MediaType;
|
import okhttp3.MediaType;
|
||||||
@@ -178,6 +179,8 @@ public class PushServiceSocket {
|
|||||||
private static final String VERIFICATION_SESSION_PATH = "/v1/verification/session";
|
private static final String VERIFICATION_SESSION_PATH = "/v1/verification/session";
|
||||||
private static final String VERIFICATION_CODE_PATH = "/v1/verification/session/%s/code";
|
private static final String VERIFICATION_CODE_PATH = "/v1/verification/session/%s/code";
|
||||||
|
|
||||||
|
private static final String REMOTE_CONFIG = "/v2/config";
|
||||||
|
|
||||||
private static final String REGISTRATION_PATH = "/v1/registration";
|
private static final String REGISTRATION_PATH = "/v1/registration";
|
||||||
|
|
||||||
private static final String BACKUP_AUTH_CHECK_V2 = "/v2/svr/auth/check";
|
private static final String BACKUP_AUTH_CHECK_V2 = "/v2/svr/auth/check";
|
||||||
@@ -541,8 +544,11 @@ public class PushServiceSocket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public RemoteConfigResponse getRemoteConfig() throws IOException {
|
public RemoteConfigResponse getRemoteConfig() throws IOException {
|
||||||
String response = makeServiceRequest("/v1/config", "GET", null);
|
try (Response response = makeServiceRequest(REMOTE_CONFIG, "GET", jsonRequestBody(null), NO_HEADERS, NO_HANDLER, SealedSenderAccess.NONE, false)) {
|
||||||
return JsonUtil.fromJson(response, RemoteConfigResponse.class);
|
RemoteConfigResponse remoteConfigResponse = JsonUtil.fromJson(readBodyString(response), RemoteConfigResponse.class);
|
||||||
|
remoteConfigResponse.setServerEpochTime(response.headers().get("X-Signal-Timestamp") != null ? Long.parseLong(response.headers().get("X-Signal-Timestamp")) : System.currentTimeMillis());
|
||||||
|
return remoteConfigResponse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelInFlightRequests() {
|
public void cancelInFlightRequests() {
|
||||||
|
|||||||
Reference in New Issue
Block a user