mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-05 01:10:13 +00:00
retire /v1/config
It's been supplanted by /v2/config for all clients beyond the 90-day window. We still have [some traffic](https://signal.grafana.net/goto/bf5tjk346v1moa?orgId=1) but it's all from expired/third-party clients (note the lack of a recognized version number in the client-version tag).
This commit is contained in:
committed by
Jonathan Klabunde Tomer
parent
389d44fd80
commit
6c3cfc88b5
@@ -125,7 +125,6 @@ import org.whispersystems.textsecuregcm.controllers.ProfileController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
|
||||
import org.whispersystems.textsecuregcm.controllers.RegistrationController;
|
||||
import org.whispersystems.textsecuregcm.controllers.RemoteConfigController;
|
||||
import org.whispersystems.textsecuregcm.controllers.RemoteConfigControllerV1;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
|
||||
import org.whispersystems.textsecuregcm.controllers.StickerController;
|
||||
@@ -1097,7 +1096,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new ProvisioningController(rateLimiters, provisioningManager),
|
||||
new RegistrationController(accountsManager, phoneVerificationTokenManager, registrationLockVerificationManager,
|
||||
rateLimiters),
|
||||
new RemoteConfigControllerV1(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig(), clock),
|
||||
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig(), clock),
|
||||
new SecureStorageController(storageCredentialsGenerator),
|
||||
new SecureValueRecovery2Controller(svr2CredentialsGenerator, accountsManager),
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Clock;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||
import org.whispersystems.textsecuregcm.util.Conversions;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
@Path("/v1/config")
|
||||
@Tag(name = "Remote Config")
|
||||
public class RemoteConfigControllerV1 {
|
||||
|
||||
private final RemoteConfigsManager remoteConfigsManager;
|
||||
private final Map<String, String> globalConfig;
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
private static final String GLOBAL_CONFIG_PREFIX = "global.";
|
||||
|
||||
public RemoteConfigControllerV1(RemoteConfigsManager remoteConfigsManager,
|
||||
Map<String, String> globalConfig,
|
||||
final Clock clock) {
|
||||
this.remoteConfigsManager = remoteConfigsManager;
|
||||
this.globalConfig = globalConfig;
|
||||
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Deprecated
|
||||
@Operation(
|
||||
summary = "Fetch remote configuration (deprecated)",
|
||||
description = """
|
||||
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, typically at client
|
||||
launch and every few hours thereafter.
|
||||
|
||||
This endpoint is deprecated; use GET /v2/config instead
|
||||
"""
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "Remote configuration values for the authenticated user", useReturnTypeSchema = true)
|
||||
public UserRemoteConfigList getAll(@Auth AuthenticatedDevice auth) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
final Stream<UserRemoteConfig> globalConfigStream = globalConfig.entrySet().stream()
|
||||
.map(entry -> new UserRemoteConfig(GLOBAL_CONFIG_PREFIX + entry.getKey(), true, entry.getValue()));
|
||||
return new UserRemoteConfigList(Stream.concat(remoteConfigsManager.getAll().stream().map(config -> {
|
||||
final byte[] hashKey = config.getHashKey() != null ? config.getHashKey().getBytes(StandardCharsets.UTF_8)
|
||||
: config.getName().getBytes(StandardCharsets.UTF_8);
|
||||
boolean inBucket = isInBucket(digest, auth.accountIdentifier(), hashKey, config.getPercentage(),
|
||||
config.getUuids());
|
||||
return new UserRemoteConfig(config.getName(), inBucket,
|
||||
inBucket ? config.getValue() : config.getDefaultValue());
|
||||
}), globalConfigStream).collect(Collectors.toList()), clock.instant());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static boolean isInBucket(MessageDigest digest, UUID uid, byte[] hashKey, int configPercentage,
|
||||
Set<UUID> uuidsInBucket) {
|
||||
if (uuidsInBucket.contains(uid)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
|
||||
bb.putLong(uid.getMostSignificantBits());
|
||||
bb.putLong(uid.getLeastSignificantBits());
|
||||
|
||||
digest.update(bb.array());
|
||||
|
||||
byte[] hash = digest.digest(hashKey);
|
||||
int bucket = (int) (Util.ensureNonNegativeLong(Conversions.byteArrayToLong(hash)) % 100);
|
||||
|
||||
return bucket < configPercentage;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.assertj.core.api.InstanceOfAssertFactory;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
|
||||
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
|
||||
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.TestClock;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
class RemoteConfigControllerV1Test {
|
||||
|
||||
private static final RemoteConfigsManager remoteConfigsManager = mock(RemoteConfigsManager.class);
|
||||
|
||||
private static final long PINNED_EPOCH_SECONDS = 1701287216L;
|
||||
private static final TestClock TEST_CLOCK = TestClock.pinned(Instant.ofEpochSecond(PINNED_EPOCH_SECONDS));
|
||||
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addProvider(new DeviceLimitExceededExceptionMapper())
|
||||
.addResource(new RemoteConfigControllerV1(remoteConfigsManager, Map.of("maxGroupSize", "42"), TEST_CLOCK))
|
||||
.build();
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
when(remoteConfigsManager.getAll()).thenReturn(new LinkedList<>() {{
|
||||
add(new RemoteConfig("android.stickers", 25, Set.of(AuthHelper.VALID_UUID_3, AuthHelper.INVALID_UUID), null,
|
||||
null, null));
|
||||
add(new RemoteConfig("ios.stickers", 50, Set.of(), null, null, null));
|
||||
add(new RemoteConfig("always.true", 100, Set.of(), null, null, null));
|
||||
add(new RemoteConfig("only.special", 0, Set.of(AuthHelper.VALID_UUID), null, null, null));
|
||||
add(new RemoteConfig("value.always.true", 100, Set.of(), "foo", "bar", null));
|
||||
add(new RemoteConfig("value.only.special", 0, Set.of(AuthHelper.VALID_UUID), "abc", "xyz", null));
|
||||
add(new RemoteConfig("value.always.false", 0, Set.of(), "red", "green", null));
|
||||
add(new RemoteConfig("linked.config.0", 50, Set.of(), null, null, null));
|
||||
add(new RemoteConfig("linked.config.1", 50, Set.of(), null, null, "linked.config.0"));
|
||||
add(new RemoteConfig("unlinked.config", 50, Set.of(), null, null, null));
|
||||
}});
|
||||
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void teardown() {
|
||||
reset(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveConfig() {
|
||||
UserRemoteConfigList configuration = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(UserRemoteConfigList.class);
|
||||
|
||||
verify(remoteConfigsManager, times(1)).getAll();
|
||||
|
||||
assertThat(configuration.getConfig()).hasSize(11);
|
||||
assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers");
|
||||
assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers");
|
||||
assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true");
|
||||
assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(2).getValue()).isNull();
|
||||
assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special");
|
||||
assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(2).getValue()).isNull();
|
||||
assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true");
|
||||
assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar");
|
||||
assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special");
|
||||
assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("xyz");
|
||||
assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false");
|
||||
assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false);
|
||||
assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red");
|
||||
assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0");
|
||||
assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1");
|
||||
assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config");
|
||||
assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testServerEpochTime() {
|
||||
Object serverEpochTime = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(Map.class)
|
||||
.get("serverEpochTime");
|
||||
|
||||
assertThat(serverEpochTime).asInstanceOf(new InstanceOfAssertFactory<>(Number.class, Assertions::assertThat))
|
||||
.extracting(Number::longValue)
|
||||
.isEqualTo(PINNED_EPOCH_SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveConfigNotSpecial() {
|
||||
UserRemoteConfigList configuration = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO))
|
||||
.get(UserRemoteConfigList.class);
|
||||
|
||||
verify(remoteConfigsManager, times(1)).getAll();
|
||||
|
||||
assertThat(configuration.getConfig()).hasSize(11);
|
||||
assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers");
|
||||
assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers");
|
||||
assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true");
|
||||
assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(2).getValue()).isNull();
|
||||
assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special");
|
||||
assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(false);
|
||||
assertThat(configuration.getConfig().get(2).getValue()).isNull();
|
||||
assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true");
|
||||
assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true);
|
||||
assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar");
|
||||
assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special");
|
||||
assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(false);
|
||||
assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("abc");
|
||||
assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false");
|
||||
assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false);
|
||||
assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red");
|
||||
assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0");
|
||||
assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1");
|
||||
assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config");
|
||||
assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHashKeyLinkedConfigs() {
|
||||
boolean allUnlinkedConfigsMatched = true;
|
||||
for (AuthHelper.TestAccount testAccount : AuthHelper.TEST_ACCOUNTS) {
|
||||
UserRemoteConfigList configuration = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", testAccount.getAuthHeader())
|
||||
.get(UserRemoteConfigList.class);
|
||||
|
||||
assertThat(configuration.getConfig()).hasSize(11);
|
||||
|
||||
final UserRemoteConfig linkedConfig0 = configuration.getConfig().get(7);
|
||||
assertThat(linkedConfig0.getName()).isEqualTo("linked.config.0");
|
||||
|
||||
final UserRemoteConfig linkedConfig1 = configuration.getConfig().get(8);
|
||||
assertThat(linkedConfig1.getName()).isEqualTo("linked.config.1");
|
||||
|
||||
final UserRemoteConfig unlinkedConfig = configuration.getConfig().get(9);
|
||||
assertThat(unlinkedConfig.getName()).isEqualTo("unlinked.config");
|
||||
|
||||
assertThat(linkedConfig0.isEnabled() == linkedConfig1.isEnabled()).isTrue();
|
||||
allUnlinkedConfigsMatched &= (linkedConfig0.isEnabled() == unlinkedConfig.isEnabled());
|
||||
}
|
||||
assertThat(allUnlinkedConfigsMatched).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveConfigUnauthorized() {
|
||||
Response response = resources.getJerseyTest()
|
||||
.target("/v1/config/")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD))
|
||||
.get();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(401);
|
||||
|
||||
verifyNoMoreInteractions(remoteConfigsManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMath() throws NoSuchAlgorithmException {
|
||||
List<RemoteConfig> remoteConfigList = remoteConfigsManager.getAll();
|
||||
Map<String, Integer> enabledMap = new HashMap<>();
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA256");
|
||||
int iterations = 100000;
|
||||
Random random = new Random(9424242L); // the seed value doesn't matter so much as it's constant to make the test not flaky
|
||||
|
||||
for (int i=0;i<iterations;i++) {
|
||||
for (RemoteConfig config : remoteConfigList) {
|
||||
int count = enabledMap.getOrDefault(config.getName(), 0);
|
||||
|
||||
if (RemoteConfigControllerV1.isInBucket(digest, AuthHelper.getRandomUUID(random), config.getName().getBytes(), config.getPercentage(), new HashSet<>())) {
|
||||
count++;
|
||||
}
|
||||
|
||||
enabledMap.put(config.getName(), count);
|
||||
}
|
||||
}
|
||||
|
||||
for (RemoteConfig config : remoteConfigList) {
|
||||
double targetNumber = iterations * (config.getPercentage() / 100.0);
|
||||
double variance = targetNumber * 0.01;
|
||||
|
||||
assertThat(enabledMap.get(config.getName())).isBetween((int) (targetNumber - variance),
|
||||
(int) (targetNumber + variance));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user