Support for getting/setting remote config variables
This commit is contained in:
parent
9d77f8dcd2
commit
08a70664f4
|
@ -171,6 +171,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private ZkConfig zkConfig;
|
private ZkConfig zkConfig;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private RemoteConfigConfiguration remoteConfig;
|
||||||
|
|
||||||
private Map<String, String> transparentDataIndex = new HashMap<>();
|
private Map<String, String> transparentDataIndex = new HashMap<>();
|
||||||
|
|
||||||
public RecaptchaConfiguration getRecaptchaConfiguration() {
|
public RecaptchaConfiguration getRecaptchaConfiguration() {
|
||||||
|
@ -299,4 +304,7 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
return zkConfig;
|
return zkConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RemoteConfigConfiguration getRemoteConfigConfiguration() {
|
||||||
|
return remoteConfig;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,6 +179,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
Keys keys = new Keys(keysDatabase);
|
Keys keys = new Keys(keysDatabase);
|
||||||
Messages messages = new Messages(messageDatabase);
|
Messages messages = new Messages(messageDatabase);
|
||||||
AbusiveHostRules abusiveHostRules = new AbusiveHostRules(abuseDatabase);
|
AbusiveHostRules abusiveHostRules = new AbusiveHostRules(abuseDatabase);
|
||||||
|
RemoteConfigs remoteConfigs = new RemoteConfigs(accountDatabase);
|
||||||
|
|
||||||
RedisClientFactory cacheClientFactory = new RedisClientFactory("main_cache", config.getCacheConfiguration().getUrl(), config.getCacheConfiguration().getReplicaUrls(), config.getCacheConfiguration().getCircuitBreakerConfiguration());
|
RedisClientFactory cacheClientFactory = new RedisClientFactory("main_cache", config.getCacheConfiguration().getUrl(), config.getCacheConfiguration().getReplicaUrls(), config.getCacheConfiguration().getCircuitBreakerConfiguration());
|
||||||
RedisClientFactory directoryClientFactory = new RedisClientFactory("directory_cache", config.getDirectoryConfiguration().getRedisConfiguration().getUrl(), config.getDirectoryConfiguration().getRedisConfiguration().getReplicaUrls(), config.getDirectoryConfiguration().getRedisConfiguration().getCircuitBreakerConfiguration());
|
RedisClientFactory directoryClientFactory = new RedisClientFactory("directory_cache", config.getDirectoryConfiguration().getRedisConfiguration().getUrl(), config.getDirectoryConfiguration().getRedisConfiguration().getReplicaUrls(), config.getDirectoryConfiguration().getRedisConfiguration().getCircuitBreakerConfiguration());
|
||||||
|
@ -199,6 +200,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheClient);
|
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheClient);
|
||||||
MessagesCache messagesCache = new MessagesCache(messagesClient, messages, accountsManager, config.getMessageCacheConfiguration().getPersistDelayMinutes());
|
MessagesCache messagesCache = new MessagesCache(messagesClient, messages, accountsManager, config.getMessageCacheConfiguration().getPersistDelayMinutes());
|
||||||
MessagesManager messagesManager = new MessagesManager(messages, messagesCache);
|
MessagesManager messagesManager = new MessagesManager(messages, messagesCache);
|
||||||
|
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||||
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager);
|
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager);
|
||||||
DispatchManager dispatchManager = new DispatchManager(cacheClientFactory, Optional.of(deadLetterHandler));
|
DispatchManager dispatchManager = new DispatchManager(cacheClientFactory, Optional.of(deadLetterHandler));
|
||||||
PubSubManager pubSubManager = new PubSubManager(cacheClient, dispatchManager);
|
PubSubManager pubSubManager = new PubSubManager(cacheClient, dispatchManager);
|
||||||
|
@ -245,6 +247,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
environment.lifecycle().manage(pushSender);
|
environment.lifecycle().manage(pushSender);
|
||||||
environment.lifecycle().manage(messagesCache);
|
environment.lifecycle().manage(messagesCache);
|
||||||
environment.lifecycle().manage(accountDatabaseCrawler);
|
environment.lifecycle().manage(accountDatabaseCrawler);
|
||||||
|
environment.lifecycle().manage(remoteConfigsManager);
|
||||||
|
|
||||||
AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret());
|
AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret());
|
||||||
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
|
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
|
||||||
|
@ -263,6 +266,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, apnFallbackManager);
|
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, apnFallbackManager);
|
||||||
ProfileController profileController = new ProfileController(rateLimiters, accountsManager, profilesManager, usernamesManager, cdnS3Client, cdnPolicyGenerator, cdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations, isZkEnabled);
|
ProfileController profileController = new ProfileController(rateLimiters, accountsManager, profilesManager, usernamesManager, cdnS3Client, cdnPolicyGenerator, cdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations, isZkEnabled);
|
||||||
StickerController stickerController = new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket());
|
StickerController stickerController = new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket());
|
||||||
|
RemoteConfigController remoteConfigController = new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().getAuthorizedTokens());
|
||||||
|
|
||||||
AuthFilter<BasicCredentials, Account> accountAuthFilter = new BasicCredentialAuthFilter.Builder<Account>().setAuthenticator(accountAuthenticator).buildAuthFilter ();
|
AuthFilter<BasicCredentials, Account> accountAuthFilter = new BasicCredentialAuthFilter.Builder<Account>().setAuthenticator(accountAuthenticator).buildAuthFilter ();
|
||||||
AuthFilter<BasicCredentials, DisabledPermittedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAccount>().setAuthenticator(disabledPermittedAccountAuthenticator).buildAuthFilter();
|
AuthFilter<BasicCredentials, DisabledPermittedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAccount>().setAuthenticator(disabledPermittedAccountAuthenticator).buildAuthFilter();
|
||||||
|
@ -285,6 +289,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
environment.jersey().register(messageController);
|
environment.jersey().register(messageController);
|
||||||
environment.jersey().register(profileController);
|
environment.jersey().register(profileController);
|
||||||
environment.jersey().register(stickerController);
|
environment.jersey().register(stickerController);
|
||||||
|
environment.jersey().register(remoteConfigController);
|
||||||
|
|
||||||
///
|
///
|
||||||
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config.getWebSocketConfiguration(), 90000);
|
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config.getWebSocketConfiguration(), 90000);
|
||||||
|
@ -295,6 +300,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
webSocketEnvironment.jersey().register(profileController);
|
webSocketEnvironment.jersey().register(profileController);
|
||||||
webSocketEnvironment.jersey().register(attachmentControllerV1);
|
webSocketEnvironment.jersey().register(attachmentControllerV1);
|
||||||
webSocketEnvironment.jersey().register(attachmentControllerV2);
|
webSocketEnvironment.jersey().register(attachmentControllerV2);
|
||||||
|
webSocketEnvironment.jersey().register(remoteConfigController);
|
||||||
|
|
||||||
WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, webSocketEnvironment.getRequestLog(), 60000);
|
WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, webSocketEnvironment.getRequestLog(), 60000);
|
||||||
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager));
|
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager));
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class RemoteConfigConfiguration {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
private List<String> authorizedTokens = new LinkedList<>();
|
||||||
|
|
||||||
|
public List<String> getAuthorizedTokens() {
|
||||||
|
return authorizedTokens;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
|
import com.codahale.metrics.annotation.Timed;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Conversions;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.DELETE;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.HeaderParam;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.Auth;
|
||||||
|
|
||||||
|
@Path("/v1/config")
|
||||||
|
public class RemoteConfigController {
|
||||||
|
|
||||||
|
private final RemoteConfigsManager remoteConfigsManager;
|
||||||
|
private final List<String> configAuthTokens;
|
||||||
|
|
||||||
|
public RemoteConfigController(RemoteConfigsManager remoteConfigsManager, List<String> configAuthTokens) {
|
||||||
|
this.remoteConfigsManager = remoteConfigsManager;
|
||||||
|
this.configAuthTokens = configAuthTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@GET
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public UserRemoteConfigList getAll(@Auth Account account) {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA1");
|
||||||
|
byte[] number = account.getNumber().getBytes();
|
||||||
|
|
||||||
|
return new UserRemoteConfigList(remoteConfigsManager.getAll().stream().map(config -> new UserRemoteConfig(config.getName(),
|
||||||
|
isInBucket(digest, number,
|
||||||
|
config.getName().getBytes(),
|
||||||
|
config.getPercentage())))
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public void set(@HeaderParam("Config-Token") String configToken, @Valid RemoteConfig config) {
|
||||||
|
if (!isAuthorized(configToken)) {
|
||||||
|
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteConfigsManager.set(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@DELETE
|
||||||
|
@Path("/{name}")
|
||||||
|
public void delete(@HeaderParam("Config-Token") String configToken, @PathParam("name") String name) {
|
||||||
|
if (!isAuthorized(configToken)) {
|
||||||
|
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteConfigsManager.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public static boolean isInBucket(MessageDigest digest, byte[] user, byte[] configName, int configPercentage) {
|
||||||
|
digest.update(user);
|
||||||
|
|
||||||
|
byte[] hash = digest.digest(configName);
|
||||||
|
int bucket = (int)(Math.abs(Conversions.byteArrayToLong(hash)) % 100);
|
||||||
|
|
||||||
|
return bucket < configPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||||
|
private boolean isAuthorized(String configToken) {
|
||||||
|
return configAuthTokens.stream().anyMatch(authorized -> MessageDigest.isEqual(authorized.getBytes(), configToken.getBytes()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class UserRemoteConfig {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
public UserRemoteConfig() {}
|
||||||
|
|
||||||
|
public UserRemoteConfig(String name, boolean enabled) {
|
||||||
|
this.name = name;
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class UserRemoteConfigList {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<UserRemoteConfig> config;
|
||||||
|
|
||||||
|
public UserRemoteConfigList() {}
|
||||||
|
|
||||||
|
public UserRemoteConfigList(List<UserRemoteConfig> config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserRemoteConfig> getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Max;
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
public class RemoteConfig {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@Pattern(regexp = "[A-Za-z0-9\\.]+")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
@Min(0)
|
||||||
|
@Max(100)
|
||||||
|
private int percentage;
|
||||||
|
|
||||||
|
public RemoteConfig() {}
|
||||||
|
|
||||||
|
public RemoteConfig(String name, int percentage) {
|
||||||
|
this.name = name;
|
||||||
|
this.percentage = percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPercentage() {
|
||||||
|
return percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
|
import com.codahale.metrics.MetricRegistry;
|
||||||
|
import com.codahale.metrics.SharedMetricRegistries;
|
||||||
|
import com.codahale.metrics.Timer;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.jdbi.v3.core.transaction.TransactionIsolationLevel;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.mappers.AccountRowMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.mappers.RemoteConfigRowMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Constants;
|
||||||
|
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
|
||||||
|
public class RemoteConfigs {
|
||||||
|
|
||||||
|
public static final String ID = "id";
|
||||||
|
public static final String NAME = "name";
|
||||||
|
public static final String PERCENTAGE = "percentage";
|
||||||
|
|
||||||
|
private static final ObjectMapper mapper = SystemMapper.getMapper();
|
||||||
|
|
||||||
|
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||||
|
private final Timer setTimer = metricRegistry.timer(name(Accounts.class, "set" ));
|
||||||
|
private final Timer getAllTimer = metricRegistry.timer(name(Accounts.class, "getAll"));
|
||||||
|
private final Timer deleteTimer = metricRegistry.timer(name(Accounts.class, "delete"));
|
||||||
|
|
||||||
|
private final FaultTolerantDatabase database;
|
||||||
|
|
||||||
|
public RemoteConfigs(FaultTolerantDatabase database) {
|
||||||
|
this.database = database;
|
||||||
|
this.database.getDatabase().registerRowMapper(new RemoteConfigRowMapper());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(RemoteConfig remoteConfig) {
|
||||||
|
database.use(jdbi -> jdbi.useHandle(handle -> {
|
||||||
|
try (Timer.Context ignored = setTimer.time()) {
|
||||||
|
handle.createUpdate("INSERT INTO remote_config (" + NAME + ", " + PERCENTAGE + ") VALUES (:name, :percentage) ON CONFLICT(" + NAME + ") DO UPDATE SET " + PERCENTAGE + " = EXCLUDED." + PERCENTAGE)
|
||||||
|
.bind("name", remoteConfig.getName())
|
||||||
|
.bind("percentage", remoteConfig.getPercentage())
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RemoteConfig> getAll() {
|
||||||
|
return database.with(jdbi -> jdbi.withHandle(handle -> {
|
||||||
|
try (Timer.Context ignored = getAllTimer.time()) {
|
||||||
|
return handle.createQuery("SELECT * FROM remote_config")
|
||||||
|
.mapTo(RemoteConfig.class)
|
||||||
|
.list();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String name) {
|
||||||
|
database.use(jdbi -> jdbi.useHandle(handle -> {
|
||||||
|
try (Timer.Context ignored = deleteTimer.time()) {
|
||||||
|
handle.createUpdate("DELETE FROM remote_config WHERE " + NAME + " = :name")
|
||||||
|
.bind("name", name)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package org.whispersystems.textsecuregcm.storage;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import io.dropwizard.lifecycle.Managed;
|
||||||
|
|
||||||
|
public class RemoteConfigsManager implements Managed {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(RemoteConfigsManager.class);
|
||||||
|
|
||||||
|
private final RemoteConfigs remoteConfigs;
|
||||||
|
private final long sleepInterval;
|
||||||
|
|
||||||
|
private AtomicReference<List<RemoteConfig>> cachedConfigs = new AtomicReference<>(new LinkedList<>());
|
||||||
|
|
||||||
|
public RemoteConfigsManager(RemoteConfigs remoteConfigs) {
|
||||||
|
this(remoteConfigs, TimeUnit.SECONDS.toMillis(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public RemoteConfigsManager(RemoteConfigs remoteConfigs, long sleepInterval) {
|
||||||
|
this.remoteConfigs = remoteConfigs;
|
||||||
|
this.sleepInterval = sleepInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
this.cachedConfigs.set(remoteConfigs.getAll());
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
this.cachedConfigs.set(remoteConfigs.getAll());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.warn("Error updating remote configs cache", t);
|
||||||
|
}
|
||||||
|
|
||||||
|
Util.sleep(sleepInterval);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RemoteConfig> getAll() {
|
||||||
|
return cachedConfigs.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(RemoteConfig config) {
|
||||||
|
remoteConfigs.set(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String name) {
|
||||||
|
remoteConfigs.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.whispersystems.textsecuregcm.storage.mappers;
|
||||||
|
|
||||||
|
import org.jdbi.v3.core.mapper.RowMapper;
|
||||||
|
import org.jdbi.v3.core.statement.StatementContext;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfigs;
|
||||||
|
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
|
||||||
|
public class RemoteConfigRowMapper implements RowMapper<RemoteConfig> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RemoteConfig map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||||
|
return new RemoteConfig(rs.getString(RemoteConfigs.NAME), rs.getInt(RemoteConfigs.PERCENTAGE));
|
||||||
|
}
|
||||||
|
}
|
|
@ -273,4 +273,24 @@
|
||||||
</createIndex>
|
</createIndex>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="12" author="moxie">
|
||||||
|
<createTable tableName="remote_config">
|
||||||
|
<column name="id" type="bigint" autoIncrement="true">
|
||||||
|
<constraints nullable="false" primaryKey="true"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="name" type="text">
|
||||||
|
<constraints nullable="false" unique="true"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="percentage" type="int">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
|
||||||
|
<column name="uuids" type="text []">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
@ -0,0 +1,207 @@
|
||||||
|
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.RemoteConfigController;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
|
|
||||||
|
import javax.ws.rs.client.Entity;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
||||||
|
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||||
|
import static org.assertj.core.api.Java6Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
public class RemoteConfigControllerTest {
|
||||||
|
|
||||||
|
private RemoteConfigsManager remoteConfigsManager = mock(RemoteConfigsManager.class);
|
||||||
|
private List<String> remoteConfigsAuth = new LinkedList<>() {{
|
||||||
|
add("foo");
|
||||||
|
add("bar");
|
||||||
|
}};
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final ResourceTestRule resources = ResourceTestRule.builder()
|
||||||
|
.addProvider(AuthHelper.getAuthFilter())
|
||||||
|
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)))
|
||||||
|
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||||
|
.addProvider(new DeviceLimitExceededExceptionMapper())
|
||||||
|
.addResource(new RemoteConfigController(remoteConfigsManager, remoteConfigsAuth))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() throws Exception {
|
||||||
|
when(remoteConfigsManager.getAll()).thenReturn(new LinkedList<>() {{
|
||||||
|
add(new RemoteConfig("android.stickers", 25));
|
||||||
|
add(new RemoteConfig("ios.stickers", 50));
|
||||||
|
add(new RemoteConfig("always.true", 100));
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRetrieveConfig() {
|
||||||
|
UserRemoteConfigList configuration = resources.getJerseyTest()
|
||||||
|
.target("/v1/config/")
|
||||||
|
.request()
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
|
||||||
|
.get(UserRemoteConfigList.class);
|
||||||
|
|
||||||
|
verify(remoteConfigsManager, times(1)).getAll();
|
||||||
|
|
||||||
|
assertThat(configuration.getConfig().size()).isEqualTo(3);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRetrieveConfigUnauthorized() {
|
||||||
|
Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/config/")
|
||||||
|
.request()
|
||||||
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(401);
|
||||||
|
|
||||||
|
verifyNoMoreInteractions(remoteConfigsManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetConfig() {
|
||||||
|
Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/config")
|
||||||
|
.request()
|
||||||
|
.header("Config-Token", "foo")
|
||||||
|
.put(Entity.entity(new RemoteConfig("android.stickers", 88), MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
|
||||||
|
ArgumentCaptor<RemoteConfig> captor = ArgumentCaptor.forClass(RemoteConfig.class);
|
||||||
|
|
||||||
|
verify(remoteConfigsManager, times(1)).set(captor.capture());
|
||||||
|
|
||||||
|
assertThat(captor.getValue().getName()).isEqualTo("android.stickers");
|
||||||
|
assertThat(captor.getValue().getPercentage()).isEqualTo(88);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetConfigUnauthorized() {
|
||||||
|
Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/config")
|
||||||
|
.request()
|
||||||
|
.header("Config-Token", "baz")
|
||||||
|
.put(Entity.entity(new RemoteConfig("android.stickers", 88), MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(401);
|
||||||
|
|
||||||
|
verifyNoMoreInteractions(remoteConfigsManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetConfigBadName() {
|
||||||
|
Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/config")
|
||||||
|
.request()
|
||||||
|
.header("Config-Token", "foo")
|
||||||
|
.put(Entity.entity(new RemoteConfig("android-stickers", 88), MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(422);
|
||||||
|
|
||||||
|
verifyNoMoreInteractions(remoteConfigsManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetConfigEmptyName() {
|
||||||
|
Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/config")
|
||||||
|
.request()
|
||||||
|
.header("Config-Token", "foo")
|
||||||
|
.put(Entity.entity(new RemoteConfig("", 88), MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(422);
|
||||||
|
|
||||||
|
verifyNoMoreInteractions(remoteConfigsManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDelete() {
|
||||||
|
Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/config/android.stickers")
|
||||||
|
.request()
|
||||||
|
.header("Config-Token", "foo")
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
|
||||||
|
verify(remoteConfigsManager, times(1)).delete("android.stickers");
|
||||||
|
verifyNoMoreInteractions(remoteConfigsManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeleteUnauthorized() {
|
||||||
|
Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/config/android.stickers")
|
||||||
|
.request()
|
||||||
|
.header("Config-Token", "baz")
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(401);
|
||||||
|
|
||||||
|
verifyNoMoreInteractions(remoteConfigsManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMath() throws NoSuchAlgorithmException {
|
||||||
|
List<RemoteConfig> remoteConfigList = remoteConfigsManager.getAll();
|
||||||
|
Map<String, Integer> enabledMap = new HashMap<>();
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA1");
|
||||||
|
int iterations = 100000;
|
||||||
|
|
||||||
|
for (int i=0;i<iterations;i++) {
|
||||||
|
for (RemoteConfig config : remoteConfigList) {
|
||||||
|
int count = enabledMap.getOrDefault(config.getName(), 0);
|
||||||
|
int random = new SecureRandom().nextInt(iterations);
|
||||||
|
|
||||||
|
if (RemoteConfigController.isInBucket(digest, ("+121322" + String.format("%05d", random)).getBytes(), config.getName().getBytes(), config.getPercentage())) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package org.whispersystems.textsecuregcm.tests.storage;
|
||||||
|
|
||||||
|
import com.opentable.db.postgres.embedded.LiquibasePreparer;
|
||||||
|
import com.opentable.db.postgres.junit.EmbeddedPostgresRules;
|
||||||
|
import com.opentable.db.postgres.junit.PreparedDbRule;
|
||||||
|
import org.jdbi.v3.core.Jdbi;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfigs;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Java6Assertions.assertThat;
|
||||||
|
|
||||||
|
public class RemoteConfigsManagerTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase(LiquibasePreparer.forClasspathLocation("accountsdb.xml"));
|
||||||
|
|
||||||
|
private RemoteConfigsManager remoteConfigs;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
RemoteConfigs remoteConfigs = new RemoteConfigs(new FaultTolerantDatabase("remote_configs-test", Jdbi.create(db.getTestDatabase()), new CircuitBreakerConfiguration()));
|
||||||
|
this.remoteConfigs = new RemoteConfigsManager(remoteConfigs, 500);
|
||||||
|
this.remoteConfigs.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdate() throws InterruptedException {
|
||||||
|
remoteConfigs.set(new RemoteConfig("android.stickers", 50));
|
||||||
|
remoteConfigs.set(new RemoteConfig("ios.stickers", 50));
|
||||||
|
remoteConfigs.set(new RemoteConfig("ios.stickers", 75));
|
||||||
|
|
||||||
|
Thread.sleep(501);
|
||||||
|
|
||||||
|
List<RemoteConfig> results = remoteConfigs.getAll();
|
||||||
|
|
||||||
|
assertThat(results.size()).isEqualTo(2);
|
||||||
|
assertThat(results.get(0).getName()).isEqualTo("android.stickers");
|
||||||
|
assertThat(results.get(0).getPercentage()).isEqualTo(50);
|
||||||
|
assertThat(results.get(1).getName()).isEqualTo("ios.stickers");
|
||||||
|
assertThat(results.get(1).getPercentage()).isEqualTo(75);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package org.whispersystems.textsecuregcm.tests.storage;
|
||||||
|
|
||||||
|
import com.opentable.db.postgres.embedded.LiquibasePreparer;
|
||||||
|
import com.opentable.db.postgres.junit.EmbeddedPostgresRules;
|
||||||
|
import com.opentable.db.postgres.junit.PreparedDbRule;
|
||||||
|
import org.jdbi.v3.core.Jdbi;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfigs;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
|
|
||||||
|
public class RemoteConfigsTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase(LiquibasePreparer.forClasspathLocation("accountsdb.xml"));
|
||||||
|
|
||||||
|
private RemoteConfigs remoteConfigs;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
this.remoteConfigs = new RemoteConfigs(new FaultTolerantDatabase("remote_configs-test", Jdbi.create(db.getTestDatabase()), new CircuitBreakerConfiguration()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStore() throws SQLException {
|
||||||
|
remoteConfigs.set(new RemoteConfig("android.stickers", 50));
|
||||||
|
|
||||||
|
List<RemoteConfig> configs = remoteConfigs.getAll();
|
||||||
|
|
||||||
|
assertThat(configs.size()).isEqualTo(1);
|
||||||
|
assertThat(configs.get(0).getName()).isEqualTo("android.stickers");
|
||||||
|
assertThat(configs.get(0).getPercentage()).isEqualTo(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdate() throws SQLException {
|
||||||
|
remoteConfigs.set(new RemoteConfig("android.stickers", 50));
|
||||||
|
remoteConfigs.set(new RemoteConfig("ios.stickers", 50));
|
||||||
|
remoteConfigs.set(new RemoteConfig("ios.stickers", 75));
|
||||||
|
|
||||||
|
List<RemoteConfig> configs = remoteConfigs.getAll();
|
||||||
|
|
||||||
|
assertThat(configs.size()).isEqualTo(2);
|
||||||
|
assertThat(configs.get(0).getName()).isEqualTo("android.stickers");
|
||||||
|
assertThat(configs.get(0).getPercentage()).isEqualTo(50);
|
||||||
|
assertThat(configs.get(1).getName()).isEqualTo("ios.stickers");
|
||||||
|
assertThat(configs.get(1).getPercentage()).isEqualTo(75);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDelete() {
|
||||||
|
remoteConfigs.set(new RemoteConfig("android.stickers", 50));
|
||||||
|
remoteConfigs.set(new RemoteConfig("ios.stickers", 50));
|
||||||
|
remoteConfigs.set(new RemoteConfig("ios.stickers", 75));
|
||||||
|
remoteConfigs.delete("android.stickers");
|
||||||
|
|
||||||
|
List<RemoteConfig> configs = remoteConfigs.getAll();
|
||||||
|
|
||||||
|
assertThat(configs.size()).isEqualTo(1);
|
||||||
|
assertThat(configs.get(0).getName()).isEqualTo("ios.stickers");
|
||||||
|
assertThat(configs.get(0).getPercentage()).isEqualTo(75);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue