Decommission the old directory cache.

This commit is contained in:
Jon Chambers 2021-01-11 16:27:04 -05:00 committed by Jon Chambers
parent 9cd121c8f6
commit 71510a8199
20 changed files with 34 additions and 831 deletions

View File

@ -50,9 +50,6 @@ cacheCluster: # Redis server configuration for cache cluster
- redis://redis.example.com:6379/
directory:
redis: # Redis server configuration for directory cluster
url:
replicaUrls:
client: # Configuration for interfacing with Contact Discovery Service cluster
userAuthenticationTokenSharedSecret: # hex-encoded secret shared with CDS used to generate auth tokens for Signal users
userAuthenticationTokenUserIdSecret: # hex-encoded secret shared among Signal-Servers to obscure user phone numbers from CDS

View File

@ -93,7 +93,6 @@ import org.whispersystems.textsecuregcm.metrics.PushLatencyManager;
import org.whispersystems.textsecuregcm.metrics.TrafficSource;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
import org.whispersystems.textsecuregcm.providers.RedisClusterHealthCheck;
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
import org.whispersystems.textsecuregcm.push.APNSender;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
@ -119,7 +118,6 @@ import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerListener;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ActiveUserCounter;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciler;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
@ -301,11 +299,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
FeatureFlags featureFlags = new FeatureFlags(accountDatabase);
RedisClientFactory pubSubClientFactory = new RedisClientFactory("pubsub_cache", config.getPubsubCacheConfiguration().getUrl(), config.getPubsubCacheConfiguration().getReplicaUrls(), config.getPubsubCacheConfiguration().getCircuitBreakerConfiguration());
RedisClientFactory directoryClientFactory = new RedisClientFactory("directory_cache", config.getDirectoryConfiguration().getRedisConfiguration().getUrl(), config.getDirectoryConfiguration().getRedisConfiguration().getReplicaUrls(), config.getDirectoryConfiguration().getRedisConfiguration().getCircuitBreakerConfiguration());
RedisClientFactory pushSchedulerClientFactory = new RedisClientFactory("push_scheduler_cache", config.getPushScheduler().getUrl(), config.getPushScheduler().getReplicaUrls(), config.getPushScheduler().getCircuitBreakerConfiguration());
ReplicatedJedisPool pubsubClient = pubSubClientFactory.getRedisClientPool();
ReplicatedJedisPool directoryClient = directoryClientFactory.getRedisClientPool();
ReplicatedJedisPool pushSchedulerClient = pushSchedulerClientFactory.getRedisClientPool();
ClientResources generalCacheClientResources = ClientResources.builder().build();
@ -339,7 +335,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager);
ClientPresenceManager clientPresenceManager = new ClientPresenceManager(clientPresenceCluster, recurringJobExecutor, keyspaceNotificationDispatchExecutor);
DirectoryManager directory = new DirectoryManager(directoryClient);
DirectoryQueue directoryQueue = new DirectoryQueue(config.getDirectoryConfiguration().getSqsConfiguration());
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheCluster);
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager(pendingDevices, cacheCluster);
@ -348,7 +343,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster, keyspaceNotificationDispatchExecutor);
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster);
MessagesManager messagesManager = new MessagesManager(messages, messagesDynamoDb, messagesCache, pushLatencyManager, experimentEnrollmentManager);
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
FeatureFlagsManager featureFlagsManager = new FeatureFlagsManager(featureFlags, recurringJobExecutor);
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(accountsManager, messagesManager);
@ -385,7 +380,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
accountDatabaseCrawlerListeners.add(new ActiveUserCounter(config.getMetricsFactory(), cacheCluster));
for (DirectoryServerConfiguration directoryServerConfiguration : config.getDirectoryConfiguration().getDirectoryServerConfiguration()) {
final DirectoryReconciliationClient directoryReconciliationClient = new DirectoryReconciliationClient(directoryServerConfiguration);
final DirectoryReconciler directoryReconciler = new DirectoryReconciler(directoryServerConfiguration.getReplicationName(), directoryServerConfiguration.isReplicationPrimary(), directoryReconciliationClient, directory);
final DirectoryReconciler directoryReconciler = new DirectoryReconciler(directoryServerConfiguration.getReplicationName(), directoryReconciliationClient);
accountDatabaseCrawlerListeners.add(directoryReconciler);
}
accountDatabaseCrawlerListeners.add(new AccountCleaner(accountsManager));
@ -441,7 +436,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, usernamesManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender, backupCredentialsGenerator));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices()));
environment.jersey().register(new DirectoryController(rateLimiters, directory, directoryCredentialsGenerator));
environment.jersey().register(new DirectoryController(directoryCredentialsGenerator));
environment.jersey().register(new ProvisioningController(rateLimiters, provisioningManager));
environment.jersey().register(new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()), zkAuthOperations, isZkEnabled));
environment.jersey().register(new VoiceVerificationController(config.getVoiceVerificationConfiguration().getUrl(), config.getVoiceVerificationConfiguration().getLocales()));
@ -498,7 +493,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
///
environment.healthChecks().register("directory", new RedisHealthCheck(directoryClient));
environment.healthChecks().register("cacheCluster", new RedisClusterHealthCheck(cacheCluster));
environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge(3, TimeUnit.SECONDS));

View File

@ -12,11 +12,6 @@ import java.util.List;
public class DirectoryConfiguration {
@JsonProperty
@NotNull
@Valid
private RedisConfiguration redis;
@JsonProperty
@NotNull
@Valid
@ -32,10 +27,6 @@ public class DirectoryConfiguration {
@Valid
private List<DirectoryServerConfiguration> server;
public RedisConfiguration getRedisConfiguration() {
return redis;
}
public SqsConfiguration getSqsConfiguration() {
return sqs;
}

View File

@ -13,9 +13,6 @@ public class DirectoryServerConfiguration {
@JsonProperty
private String replicationName;
@JsonProperty
private boolean replicationPrimary;
@NotEmpty
@JsonProperty
private String replicationUrl;
@ -32,10 +29,6 @@ public class DirectoryServerConfiguration {
return replicationName;
}
public boolean isReplicationPrimary() {
return replicationPrimary;
}
public String getReplicationUrl() {
return replicationUrl;
}

View File

@ -35,12 +35,6 @@ public class RateLimitsConfiguration {
@JsonProperty
private RateLimitConfiguration attachments = new RateLimitConfiguration(50, 50);
@JsonProperty
private RateLimitConfiguration contactQueries = new RateLimitConfiguration(50000, 50000);
@JsonProperty
private RateLimitConfiguration contactIpQueries = new RateLimitConfiguration(200, (100.0 / 60.0));
@JsonProperty
private RateLimitConfiguration prekeys = new RateLimitConfiguration(3, 1.0 / 10.0);
@ -88,14 +82,6 @@ public class RateLimitsConfiguration {
return prekeys;
}
public RateLimitConfiguration getContactQueries() {
return contactQueries;
}
public RateLimitConfiguration getContactIpQueries() {
return contactIpQueries;
}
public RateLimitConfiguration getAttachments() {
return attachments;
}

View File

@ -4,92 +4,28 @@
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.auth.Auth;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.DirectoryFeedbackRequest;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.Constants;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
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 javax.ws.rs.core.Response.Status;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.Auth;
@Path("/v1/directory")
public class DirectoryController {
private static final String[] FEEDBACK_STATUSES = {
"ok",
"mismatch",
"attestation-error",
"unexpected-error",
};
private static final String[] FRONTED_REGIONS = {"+20", "+971", "+968", "+974"};
private final Logger logger = LoggerFactory.getLogger(DirectoryController.class);
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Histogram contactsHistogram = metricRegistry.histogram(name(getClass(), "contacts"));
private final Meter contactsMeter = metricRegistry.meter(name(getClass(), "contactRate"));
private final Map<String, Meter> iosFeedbackMeters = new HashMap<String, Meter>() {{
for (String status : FEEDBACK_STATUSES) {
put(status, metricRegistry.meter(name(DirectoryController.class, "feedback-v2", "ios", status)));
}
}};
private final Map<String, Meter> androidFeedbackMeters = new HashMap<String, Meter>() {{
for (String status : FEEDBACK_STATUSES) {
put(status, metricRegistry.meter(name(DirectoryController.class, "feedback-v2", "android", status)));
}
}};
private final Map<String, Meter> unknownFeedbackMeters = new HashMap<String, Meter>() {{
for (String status : FEEDBACK_STATUSES) {
put(status, metricRegistry.meter(name(DirectoryController.class, "feedback-v2", "unknown", status)));
}
}};
private final RateLimiters rateLimiters;
private final DirectoryManager directory;
private final ExternalServiceCredentialGenerator directoryServiceTokenGenerator;
public DirectoryController(RateLimiters rateLimiters,
DirectoryManager directory,
ExternalServiceCredentialGenerator userTokenGenerator)
{
this.directory = directory;
this.rateLimiters = rateLimiters;
public DirectoryController(ExternalServiceCredentialGenerator userTokenGenerator) {
this.directoryServiceTokenGenerator = userTokenGenerator;
}
@ -105,29 +41,8 @@ public class DirectoryController {
@Path("/feedback-v3/{status}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response setFeedback(@Auth Account account,
@PathParam("status") String status,
@Valid DirectoryFeedbackRequest request)
{
Map<String, Meter> platformFeedbackMeters = unknownFeedbackMeters;
Optional<Device> masterDevice = account.getMasterDevice();
if (masterDevice.isPresent()) {
if (masterDevice.get().getApnId() != null) {
platformFeedbackMeters = iosFeedbackMeters;
} else if (masterDevice.get().getGcmId() != null) {
platformFeedbackMeters = androidFeedbackMeters;
}
}
Optional<Meter> meter = Optional.ofNullable(platformFeedbackMeters.get(status));
if (meter.isPresent()) {
meter.get().mark();
return Response.ok().build();
} else {
return Response.status(Status.NOT_FOUND).build();
}
public Response setFeedback(@Auth Account account) {
return Response.ok().build();
}
@ -135,9 +50,7 @@ public class DirectoryController {
@GET
@Path("/{token}")
@Produces(MediaType.APPLICATION_JSON)
public Response getTokenPresence(@Auth Account account, @PathParam("token") String token)
throws RateLimitExceededException
{
public Response getTokenPresence(@Auth Account account) {
return Response.status(429).build();
}
@ -146,15 +59,7 @@ public class DirectoryController {
@Path("/tokens")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response getContactIntersection(@Auth Account account,
@HeaderParam("X-Forwarded-For") String forwardedFor,
@Valid ClientContactTokens contacts)
throws RateLimitExceededException
{
public Response getContactIntersection(@Auth Account account) {
return Response.status(429).build();
}
private byte[] decodeToken(String encoded) throws IOException {
return Base64.decodeWithoutPadding(encoded.replace('-', '+').replace('_', '/'));
}
}

View File

@ -1,96 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import java.util.Arrays;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class ClientContact {
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@JsonProperty
private byte[] token;
@JsonProperty
private boolean voice;
@JsonProperty
private boolean video;
private String relay;
private boolean inactive;
public ClientContact(byte[] token, String relay, boolean voice, boolean video) {
this.token = token;
this.relay = relay;
this.voice = voice;
this.video = video;
}
public ClientContact() {}
public byte[] getToken() {
return token;
}
public String getRelay() {
return relay;
}
public void setRelay(String relay) {
this.relay = relay;
}
public boolean isInactive() {
return inactive;
}
public void setInactive(boolean inactive) {
this.inactive = inactive;
}
public boolean isVoice() {
return voice;
}
public void setVoice(boolean voice) {
this.voice = voice;
}
public boolean isVideo() {
return video;
}
public void setVideo(boolean video) {
this.video = video;
}
@Override
public boolean equals(Object other) {
if (other == null) return false;
if (!(other instanceof ClientContact)) return false;
ClientContact that = (ClientContact)other;
return
Arrays.equals(this.token, that.token) &&
this.inactive == that.inactive &&
this.voice == that.voice &&
this.video == that.video &&
(this.relay == null ? (that.relay == null) : this.relay.equals(that.relay));
}
public int hashCode() {
return Arrays.hashCode(this.token);
}
}

View File

@ -1,28 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotNull;
import java.util.List;
public class ClientContactTokens {
@NotNull
@JsonProperty
private List<String> contacts;
public List<String> getContacts() {
return contacts;
}
public ClientContactTokens() {}
public ClientContactTokens(List<String> contacts) {
this.contacts = contacts;
}
}

View File

@ -1,29 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.LinkedList;
import java.util.List;
public class ClientContacts {
@JsonProperty
private List<ClientContact> contacts;
public ClientContacts(List<ClientContact> contacts) {
if (contacts != null) this.contacts = contacts;
else this.contacts = new LinkedList<>();
}
public ClientContacts() {
this.contacts = new LinkedList<>();
}
public List<ClientContact> getContacts() {
return contacts;
}
}

View File

@ -1,31 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.Size;
import javax.validation.valueextraction.Unwrapping;
import java.util.Optional;
public class DirectoryFeedbackRequest {
@Size(max = 1024, payload = {Unwrapping.Unwrap.class})
@JsonProperty
private Optional<String> reason;
public DirectoryFeedbackRequest() {
}
public DirectoryFeedbackRequest(Optional<String> reason) {
this.reason = reason;
}
public Optional<String> getReason() {
return reason;
}
}

View File

@ -25,8 +25,6 @@ public class RateLimiters {
private final RateLimiter pinLimiter;
private final RateLimiter attachmentLimiter;
private final RateLimiter contactsLimiter;
private final RateLimiter contactsIpLimiter;
private final RateLimiter preKeysLimiter;
private final RateLimiter messagesLimiter;
@ -86,14 +84,6 @@ public class RateLimiters {
config.getAttachments().getBucketSize(),
config.getAttachments().getLeakRatePerMinute());
this.contactsLimiter = new RateLimiter(cacheCluster, "contactsQuery",
config.getContactQueries().getBucketSize(),
config.getContactQueries().getLeakRatePerMinute());
this.contactsIpLimiter = new RateLimiter(cacheCluster, "contactsIpQuery",
config.getContactIpQueries().getBucketSize(),
config.getContactIpQueries().getLeakRatePerMinute());
this.preKeysLimiter = new RateLimiter(cacheCluster, "prekeys",
config.getPreKeys().getBucketSize(),
config.getPreKeys().getLeakRatePerMinute());
@ -174,14 +164,6 @@ public class RateLimiters {
return preKeysLimiter;
}
public RateLimiter getContactsLimiter() {
return contactsLimiter;
}
public RateLimiter getContactsIpLimiter() {
return contactsIpLimiter;
}
public RateLimiter getAttachmentLimiter() {
return this.attachmentLimiter;
}

View File

@ -16,7 +16,6 @@ import io.micrometer.core.instrument.Metrics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.util.Constants;
@ -53,7 +52,6 @@ public class AccountsManager {
private final Accounts accounts;
private final FaultTolerantRedisCluster cacheCluster;
private final DirectoryManager directory;
private final DirectoryQueue directoryQueue;
private final KeysDynamoDb keysDynamoDb;
private final MessagesManager messagesManager;
@ -73,9 +71,8 @@ public class AccountsManager {
}
}
public AccountsManager(Accounts accounts, DirectoryManager directory, FaultTolerantRedisCluster cacheCluster, final DirectoryQueue directoryQueue, final KeysDynamoDb keysDynamoDb, final MessagesManager messagesManager, final UsernamesManager usernamesManager, final ProfilesManager profilesManager) {
public AccountsManager(Accounts accounts, FaultTolerantRedisCluster cacheCluster, final DirectoryQueue directoryQueue, final KeysDynamoDb keysDynamoDb, final MessagesManager messagesManager, final UsernamesManager usernamesManager, final ProfilesManager profilesManager) {
this.accounts = accounts;
this.directory = directory;
this.cacheCluster = cacheCluster;
this.directoryQueue = directoryQueue;
this.keysDynamoDb = keysDynamoDb;
@ -89,7 +86,6 @@ public class AccountsManager {
try (Timer.Context ignored = createTimer.time()) {
boolean freshUser = databaseCreate(account);
redisSet(account);
updateDirectory(account);
return freshUser;
}
@ -99,7 +95,6 @@ public class AccountsManager {
try (Timer.Context ignored = updateTimer.time()) {
redisSet(account);
databaseUpdate(account);
updateDirectory(account);
}
}
@ -148,7 +143,6 @@ public class AccountsManager {
try (final Timer.Context ignored = deleteTimer.time()) {
usernamesManager.delete(account.getUuid());
directoryQueue.deleteAccount(account);
directory.remove(account.getNumber());
profilesManager.deleteAll(account.getUuid());
keysDynamoDb.delete(account);
messagesManager.clear(account.getNumber(), account.getUuid());
@ -162,16 +156,6 @@ public class AccountsManager {
.increment();
}
private void updateDirectory(Account account) {
if (account.isEnabled()) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, true, true);
directory.add(clientContact);
} else {
directory.remove(account.getNumber());
}
}
private String getAccountMapKey(String number) {
return "AccountMap::" + number;
}

View File

@ -1,204 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import org.whispersystems.textsecuregcm.util.IterablePair;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
public class DirectoryManager {
private final Logger logger = LoggerFactory.getLogger(DirectoryManager.class);
private static final byte[] DIRECTORY_KEY = {'d', 'i', 'r', 'e', 'c', 't', 'o', 'r', 'y'};
private final ObjectMapper objectMapper;
private final ReplicatedJedisPool redisPool;
public DirectoryManager(ReplicatedJedisPool redisPool) {
this.redisPool = redisPool;
this.objectMapper = new ObjectMapper();
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public void remove(String number) {
remove(Util.getContactToken(number));
}
public void remove(BatchOperationHandle handle, String number) {
remove(handle, Util.getContactToken(number));
}
public void remove(byte[] token) {
try (Jedis jedis = redisPool.getWriteResource()) {
jedis.hdel(DIRECTORY_KEY, token);
}
}
public void remove(BatchOperationHandle handle, byte[] token) {
Pipeline pipeline = handle.pipeline;
pipeline.hdel(DIRECTORY_KEY, token);
}
public void add(ClientContact contact) {
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isVoice(), contact.isVideo());
try (Jedis jedis = redisPool.getWriteResource()) {
jedis.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue));
} catch (JsonProcessingException e) {
logger.warn("JSON Serialization", e);
}
}
public void add(BatchOperationHandle handle, ClientContact contact) {
try {
Pipeline pipeline = handle.pipeline;
TokenValue tokenValue = new TokenValue(contact.getRelay(), contact.isVoice(), contact.isVideo());
pipeline.hset(DIRECTORY_KEY, contact.getToken(), objectMapper.writeValueAsBytes(tokenValue));
} catch (JsonProcessingException e) {
logger.warn("JSON Serialization", e);
}
}
public PendingClientContact get(BatchOperationHandle handle, byte[] token) {
Pipeline pipeline = handle.pipeline;
return new PendingClientContact(objectMapper, token, pipeline.hget(DIRECTORY_KEY, token));
}
public Optional<ClientContact> get(byte[] token) {
try (Jedis jedis = redisPool.getWriteResource()) {
byte[] result = jedis.hget(DIRECTORY_KEY, token);
if (result == null) {
return Optional.empty();
}
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.voice, tokenValue.video));
} catch (IOException e) {
logger.warn("JSON Error", e);
return Optional.empty();
}
}
public List<ClientContact> get(List<byte[]> tokens) {
try (Jedis jedis = redisPool.getWriteResource()) {
Pipeline pipeline = jedis.pipelined();
List<Response<byte[]>> futures = new LinkedList<>();
List<ClientContact> results = new LinkedList<>();
try {
for (byte[] token : tokens) {
futures.add(pipeline.hget(DIRECTORY_KEY, token));
}
} finally {
pipeline.sync();
}
IterablePair<byte[], Response<byte[]>> lists = new IterablePair<>(tokens, futures);
for (Pair<byte[], Response<byte[]>> pair : lists) {
try {
if (pair.second().get() != null) {
TokenValue tokenValue = objectMapper.readValue(pair.second().get(), TokenValue.class);
ClientContact clientContact = new ClientContact(pair.first(), tokenValue.relay, tokenValue.voice, tokenValue.video);
results.add(clientContact);
}
} catch (IOException e) {
logger.warn("Deserialization Problem: ", e);
}
}
return results;
}
}
public BatchOperationHandle startBatchOperation() {
Jedis jedis = redisPool.getWriteResource();
return new BatchOperationHandle(jedis, jedis.pipelined());
}
public void stopBatchOperation(BatchOperationHandle handle) {
Pipeline pipeline = handle.pipeline;
Jedis jedis = handle.jedis;
pipeline.sync();
jedis.close();
}
public static class BatchOperationHandle {
public final Pipeline pipeline;
public final Jedis jedis;
public BatchOperationHandle(Jedis jedis, Pipeline pipeline) {
this.pipeline = pipeline;
this.jedis = jedis;
}
}
private static class TokenValue {
@JsonProperty(value = "r")
private String relay;
@JsonProperty(value = "v")
private boolean voice;
@JsonProperty(value = "w")
private boolean video;
public TokenValue() {}
public TokenValue(String relay, boolean voice, boolean video) {
this.relay = relay;
this.voice = voice;
this.video = video;
}
}
public static class PendingClientContact {
private final ObjectMapper objectMapper;
private final byte[] token;
private final Response<byte[]> response;
PendingClientContact(ObjectMapper objectMapper, byte[] token, Response<byte[]> response) {
this.objectMapper = objectMapper;
this.token = token;
this.response = response;
}
public Optional<ClientContact> get() throws IOException {
byte[] result = response.get();
if (result == null) {
return Optional.empty();
}
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.voice, tokenValue.video));
}
}
}

View File

@ -10,15 +10,11 @@ import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationRequest;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationResponse;
import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import javax.ws.rs.ProcessingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -32,17 +28,11 @@ public class DirectoryReconciler extends AccountDatabaseCrawlerListener {
private static final Logger logger = LoggerFactory.getLogger(DirectoryReconciler.class);
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final String name;
private final boolean primary;
private final DirectoryManager directoryManager;
private final DirectoryReconciliationClient reconciliationClient;
private final Timer sendChunkTimer;
private final Meter sendChunkErrorMeter;
public DirectoryReconciler(String name, boolean primary, DirectoryReconciliationClient reconciliationClient, DirectoryManager directoryManager) {
this.name = name;
this.primary = primary;
this.directoryManager = directoryManager;
public DirectoryReconciler(String name, DirectoryReconciliationClient reconciliationClient) {
this.reconciliationClient = reconciliationClient;
sendChunkTimer = metricRegistry.timer(name(DirectoryReconciler.class, name, "sendChunk"));
sendChunkErrorMeter = metricRegistry.meter(name(DirectoryReconciler.class, name, "sendChunkError"));
@ -59,11 +49,6 @@ public class DirectoryReconciler extends AccountDatabaseCrawlerListener {
@Override
protected void onCrawlChunk(Optional<UUID> fromUuid, List<Account> chunkAccounts) throws AccountDatabaseCrawlerRestartException {
if (primary) {
updateDirectoryCache(chunkAccounts);
}
DirectoryReconciliationRequest request = createChunkRequest(fromUuid, chunkAccounts);
DirectoryReconciliationResponse response = sendChunk(request);
if (response.getStatus() == DirectoryReconciliationResponse.Status.MISSING) {
@ -71,25 +56,6 @@ public class DirectoryReconciler extends AccountDatabaseCrawlerListener {
}
}
private void updateDirectoryCache(List<Account> accounts) {
BatchOperationHandle batchOperation = directoryManager.startBatchOperation();
try {
for (Account account : accounts) {
if (account.isEnabled() && account.isDiscoverableByPhoneNumber()) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, true, true);
directoryManager.add(batchOperation, clientContact);
} else {
directoryManager.remove(batchOperation, account.getNumber());
}
}
} finally {
directoryManager.stopBatchOperation(batchOperation);
}
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private DirectoryReconciliationRequest createChunkRequest(Optional<UUID> fromUuid, List<Account> accounts) {
List<DirectoryReconciliationRequest.User> users = new ArrayList<>(accounts.size());

View File

@ -23,14 +23,11 @@ import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.metrics.PushLatencyManager;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
@ -121,18 +118,16 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
KeysDynamoDb keysDynamoDb = new KeysDynamoDb(preKeysDynamoDb, configuration.getKeysDynamoDbConfiguration().getTableName());
Messages messages = new Messages(messageDatabase);
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb, configuration.getMessageDynamoDbConfiguration().getTableName(), configuration.getMessageDynamoDbConfiguration().getTimeToLive());
ReplicatedJedisPool redisClient = new RedisClientFactory("directory_cache_delete_command", configuration.getDirectoryConfiguration().getRedisConfiguration().getUrl(), configuration.getDirectoryConfiguration().getRedisConfiguration().getReplicaUrls(), configuration.getDirectoryConfiguration().getRedisConfiguration().getCircuitBreakerConfiguration()).getRedisClientPool();
FaultTolerantRedisCluster messageInsertCacheCluster = new FaultTolerantRedisCluster("message_insert_cluster", configuration.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClusterClientResources);
FaultTolerantRedisCluster messageReadDeleteCluster = new FaultTolerantRedisCluster("message_read_delete_cluster", configuration.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClusterClientResources);
FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", configuration.getMetricsClusterConfiguration(), redisClusterClientResources);
MessagesCache messagesCache = new MessagesCache(messageInsertCacheCluster, messageReadDeleteCluster, keyspaceNotificationDispatchExecutor);
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster);
DirectoryQueue directoryQueue = new DirectoryQueue (configuration.getDirectoryConfiguration().getSqsConfiguration());
DirectoryManager directory = new DirectoryManager(redisClient );
UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster);
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster);
MessagesManager messagesManager = new MessagesManager(messages, messagesDynamoDb, messagesCache, pushLatencyManager, new ExperimentEnrollmentManager(dynamicConfigurationManager));
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
for (String user: users) {
Optional<Account> account = accountsManager.get(user);

View File

@ -6,75 +6,45 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.collect.ImmutableSet;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
import org.whispersystems.textsecuregcm.entities.DirectoryFeedbackRequest;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.Response.Status.Family;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Collections;
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyListOf;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class DirectoryControllerTest {
private final RateLimiters rateLimiters = mock(RateLimiters.class );
private final RateLimiter rateLimiter = mock(RateLimiter.class );
private final RateLimiter ipLimiter = mock(RateLimiter.class );
private final DirectoryManager directoryManager = mock(DirectoryManager.class );
private final ExternalServiceCredentialGenerator directoryCredentialsGenerator = mock(ExternalServiceCredentialGenerator.class);
private final ExternalServiceCredentials validCredentials = new ExternalServiceCredentials("username", "password");
private final ExternalServiceCredentials validCredentials = new ExternalServiceCredentials("username", "password");
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new DirectoryController(rateLimiters,
directoryManager,
directoryCredentialsGenerator))
.addResource(new DirectoryController(directoryCredentialsGenerator))
.build();
@Before
public void setup() throws Exception {
when(rateLimiters.getContactsLimiter()).thenReturn(rateLimiter);
when(rateLimiters.getContactsIpLimiter()).thenReturn(ipLimiter);
when(directoryManager.get(anyListOf(byte[].class))).thenAnswer(new Answer<List<byte[]>>() {
@Override
public List<byte[]> answer(InvocationOnMock invocationOnMock) throws Throwable {
List<byte[]> query = (List<byte[]>) invocationOnMock.getArguments()[0];
List<byte[]> response = new LinkedList<>(query);
response.remove(0);
return response;
}
});
public void setup() {
when(directoryCredentialsGenerator.generateFor(eq(AuthHelper.VALID_NUMBER))).thenReturn(validCredentials);
}
@ -85,65 +55,10 @@ public class DirectoryControllerTest {
.target("/v1/directory/feedback-v3/ok")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new DirectoryFeedbackRequest(Optional.of("test reason")), MediaType.APPLICATION_JSON_TYPE));
.put(Entity.json("{\"reason\": \"test reason\"}"));
assertThat(response.getStatusInfo().getFamily()).isEqualTo(Family.SUCCESSFUL);
}
@Test
public void testNotFoundFeedback() {
Response response =
resources.getJerseyTest()
.target("/v1/directory/feedback-v3/test-not-found")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new DirectoryFeedbackRequest(Optional.of("test reason")), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatusInfo()).isEqualTo(Status.NOT_FOUND);
}
@Test
public void testFeedbackEmptyRequest() {
Response response =
resources.getJerseyTest()
.target("/v1/directory/feedback-v3/mismatch")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.json(""));
assertThat(response.getStatusInfo().getFamily()).isEqualTo(Family.SUCCESSFUL);
}
@Test
public void testFeedbackNoReason() {
Response response =
resources.getJerseyTest()
.target("/v1/directory/feedback-v3/mismatch")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new DirectoryFeedbackRequest(Optional.empty()), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatusInfo().getFamily()).isEqualTo(Family.SUCCESSFUL);
}
@Test
public void testFeedbackEmptyReason() {
Response response =
resources.getJerseyTest()
.target("/v1/directory/feedback-v3/mismatch")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new DirectoryFeedbackRequest(Optional.of("")), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatusInfo().getFamily()).isEqualTo(Family.SUCCESSFUL);
}
@Test
public void testFeedbackTooLargeReason() {
Response response =
resources.getJerseyTest()
.target("/v1/directory/feedback-v3/mismatch")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new DirectoryFeedbackRequest(Optional.of(new String(new char[102400]))), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatusInfo().getFamily()).isEqualTo(Family.CLIENT_ERROR);
}
@Test
public void testGetAuthToken() {
ExternalServiceCredentials token =
@ -169,16 +84,7 @@ public class DirectoryControllerTest {
@Test
public void testContactIntersection() throws Exception {
List<String> tokens = new LinkedList<String>() {{
add(Base64.encodeBytes("foo".getBytes()));
add(Base64.encodeBytes("bar".getBytes()));
add(Base64.encodeBytes("baz".getBytes()));
}};
List<String> expectedResponse = new LinkedList<>(tokens);
expectedResponse.remove(0);
public void testContactIntersection() {
Response response =
resources.getJerseyTest()
.target("/v1/directory/tokens/")
@ -187,7 +93,7 @@ public class DirectoryControllerTest {
AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER,
AuthHelper.VALID_PASSWORD))
.header("X-Forwarded-For", "192.168.1.1, 1.1.1.1")
.put(Entity.entity(new ClientContactTokens(tokens), MediaType.APPLICATION_JSON_TYPE));
.put(Entity.entity(Collections.emptyMap(), MediaType.APPLICATION_JSON_TYPE));
assertThat(response.getStatus()).isEqualTo(429);

View File

@ -1,55 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.tests.entities;
import org.junit.Test;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.util.Util;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.*;
public class ClientContactTest {
@Test
public void serializeToJSON() throws Exception {
byte[] token = Util.getContactToken("+14152222222");
ClientContact contact = new ClientContact(token, null, false, false);
ClientContact contactWithRelay = new ClientContact(token, "whisper", false, false);
ClientContact contactWithRelayVox = new ClientContact(token, "whisper", true, false);
ClientContact contactWithRelayVid = new ClientContact(token, "whisper", true, true);
assertThat("Basic Contact Serialization works",
asJson(contact),
is(equalTo(jsonFixture("fixtures/contact.json"))));
assertThat("Contact Relay Serialization works",
asJson(contactWithRelay),
is(equalTo(jsonFixture("fixtures/contact.relay.json"))));
assertThat("Contact Relay Vox Serializaton works",
asJson(contactWithRelayVox),
is(equalTo(jsonFixture("fixtures/contact.relay.voice.json"))));
assertThat("Contact Relay Video Serializaton works",
asJson(contactWithRelayVid),
is(equalTo(jsonFixture("fixtures/contact.relay.video.json"))));
}
@Test
public void deserializeFromJSON() throws Exception {
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
"whisper", false, false);
assertThat("a ClientContact can be deserialized from JSON",
fromJson(jsonFixture("fixtures/contact.relay.json"), ClientContact.class),
is(contact));
}
}

View File

@ -6,9 +6,7 @@
package org.whispersystems.textsecuregcm.tests.entities;
import org.junit.Test;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.util.Util;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
@ -17,16 +15,6 @@ import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.*;
public class PreKeyTest {
@Test
public void deserializeFromJSONV() throws Exception {
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
"whisper", false, false);
assertThat("a ClientContact can be deserialized from JSON",
fromJson(jsonFixture("fixtures/contact.relay.json"), ClientContact.class),
is(contact));
}
@Test
public void serializeToJSONV2() throws Exception {
PreKey preKey = new PreKey(1234, "test");

View File

@ -13,7 +13,6 @@ import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
@ -42,7 +41,6 @@ public class AccountsManagerTest {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Accounts accounts = mock(Accounts.class);
DirectoryManager directoryManager = mock(DirectoryManager.class);
DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
KeysDynamoDb keysDynamoDb = mock(KeysDynamoDb.class);
MessagesManager messagesManager = mock(MessagesManager.class);
@ -54,7 +52,7 @@ public class AccountsManagerTest {
when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(uuid.toString());
when(commands.get(eq("Account3::" + uuid.toString()))).thenReturn("{\"number\": \"+14152222222\", \"name\": \"test\"}");
AccountsManager accountsManager = new AccountsManager(accounts, directoryManager, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
Optional<Account> account = accountsManager.get("+14152222222");
assertTrue(account.isPresent());
@ -72,7 +70,6 @@ public class AccountsManagerTest {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Accounts accounts = mock(Accounts.class);
DirectoryManager directoryManager = mock(DirectoryManager.class);
DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
KeysDynamoDb keysDynamoDb = mock(KeysDynamoDb.class);
MessagesManager messagesManager = mock(MessagesManager.class);
@ -83,7 +80,7 @@ public class AccountsManagerTest {
when(commands.get(eq("Account3::" + uuid.toString()))).thenReturn("{\"number\": \"+14152222222\", \"name\": \"test\"}");
AccountsManager accountsManager = new AccountsManager(accounts, directoryManager, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
Optional<Account> account = accountsManager.get(uuid);
assertTrue(account.isPresent());
@ -102,7 +99,6 @@ public class AccountsManagerTest {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Accounts accounts = mock(Accounts.class);
DirectoryManager directoryManager = mock(DirectoryManager.class);
DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
KeysDynamoDb keysDynamoDb = mock(KeysDynamoDb.class);
MessagesManager messagesManager = mock(MessagesManager.class);
@ -114,7 +110,7 @@ public class AccountsManagerTest {
when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(null);
when(accounts.get(eq("+14152222222"))).thenReturn(Optional.of(account));
AccountsManager accountsManager = new AccountsManager(accounts, directoryManager, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
Optional<Account> retrieved = accountsManager.get("+14152222222");
assertTrue(retrieved.isPresent());
@ -134,7 +130,6 @@ public class AccountsManagerTest {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Accounts accounts = mock(Accounts.class);
DirectoryManager directoryManager = mock(DirectoryManager.class);
DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
KeysDynamoDb keysDynamoDb = mock(KeysDynamoDb.class);
MessagesManager messagesManager = mock(MessagesManager.class);
@ -146,7 +141,7 @@ public class AccountsManagerTest {
when(commands.get(eq("Account3::" + uuid))).thenReturn(null);
when(accounts.get(eq(uuid))).thenReturn(Optional.of(account));
AccountsManager accountsManager = new AccountsManager(accounts, directoryManager, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
Optional<Account> retrieved = accountsManager.get(uuid);
assertTrue(retrieved.isPresent());
@ -166,7 +161,6 @@ public class AccountsManagerTest {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Accounts accounts = mock(Accounts.class);
DirectoryManager directoryManager = mock(DirectoryManager.class);
DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
KeysDynamoDb keysDynamoDb = mock(KeysDynamoDb.class);
MessagesManager messagesManager = mock(MessagesManager.class);
@ -178,7 +172,7 @@ public class AccountsManagerTest {
when(commands.get(eq("AccountMap::+14152222222"))).thenThrow(new RedisException("Connection lost!"));
when(accounts.get(eq("+14152222222"))).thenReturn(Optional.of(account));
AccountsManager accountsManager = new AccountsManager(accounts, directoryManager, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
Optional<Account> retrieved = accountsManager.get("+14152222222");
assertTrue(retrieved.isPresent());
@ -198,7 +192,6 @@ public class AccountsManagerTest {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Accounts accounts = mock(Accounts.class);
DirectoryManager directoryManager = mock(DirectoryManager.class);
DirectoryQueue directoryQueue = mock(DirectoryQueue.class);
KeysDynamoDb keysDynamoDb = mock(KeysDynamoDb.class);
MessagesManager messagesManager = mock(MessagesManager.class);
@ -210,7 +203,7 @@ public class AccountsManagerTest {
when(commands.get(eq("Account3::" + uuid))).thenThrow(new RedisException("Connection lost!"));
when(accounts.get(eq(uuid))).thenReturn(Optional.of(account));
AccountsManager accountsManager = new AccountsManager(accounts, directoryManager, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager);
Optional<Account> retrieved = accountsManager.get(uuid);
assertTrue(retrieved.isPresent());

View File

@ -8,16 +8,12 @@ package org.whispersystems.textsecuregcm.tests.storage;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationRequest;
import org.whispersystems.textsecuregcm.entities.DirectoryReconciliationResponse;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerRestartException;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciler;
import org.whispersystems.textsecuregcm.storage.DirectoryReconciliationClient;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Arrays;
import java.util.Optional;
@ -25,7 +21,6 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
public class DirectoryReconcilerTest {
@ -39,10 +34,8 @@ public class DirectoryReconcilerTest {
private final Account activeAccount = mock(Account.class);
private final Account inactiveAccount = mock(Account.class);
private final Account undiscoverableAccount = mock(Account.class);
private final BatchOperationHandle batchOperationHandle = mock(BatchOperationHandle.class);
private final DirectoryManager directoryManager = mock(DirectoryManager.class);
private final DirectoryReconciliationClient reconciliationClient = mock(DirectoryReconciliationClient.class);
private final DirectoryReconciler directoryReconciler = new DirectoryReconciler("test", true, reconciliationClient, directoryManager);
private final DirectoryReconciler directoryReconciler = new DirectoryReconciler("test", reconciliationClient);
private final DirectoryReconciliationResponse successResponse = new DirectoryReconciliationResponse(DirectoryReconciliationResponse.Status.OK);
@ -60,7 +53,6 @@ public class DirectoryReconcilerTest {
when(undiscoverableAccount.getNumber()).thenReturn(UNDISCOVERABLE_NUMBER);
when(undiscoverableAccount.isEnabled()).thenReturn(true);
when(undiscoverableAccount.isDiscoverableByPhoneNumber()).thenReturn(false);
when(directoryManager.startBatchOperation()).thenReturn(batchOperationHandle);
}
@Test
@ -68,38 +60,12 @@ public class DirectoryReconcilerTest {
when(reconciliationClient.sendChunk(any())).thenReturn(successResponse);
directoryReconciler.timeAndProcessCrawlChunk(Optional.of(VALID_UUID), Arrays.asList(activeAccount, inactiveAccount, undiscoverableAccount));
verify(activeAccount, atLeastOnce()).getUuid();
verify(activeAccount, atLeastOnce()).getNumber();
verify(activeAccount, atLeastOnce()).isEnabled();
verify(activeAccount, atLeastOnce()).isDiscoverableByPhoneNumber();
verify(inactiveAccount, atLeastOnce()).getNumber();
verify(inactiveAccount, atLeastOnce()).isEnabled();
verify(undiscoverableAccount, atLeastOnce()).getUuid();
verify(undiscoverableAccount, atLeastOnce()).getNumber();
verify(undiscoverableAccount, atLeastOnce()).isEnabled();
verify(undiscoverableAccount, atLeastOnce()).isDiscoverableByPhoneNumber();
ArgumentCaptor<DirectoryReconciliationRequest> request = ArgumentCaptor.forClass(DirectoryReconciliationRequest.class);
verify(reconciliationClient, times(1)).sendChunk(request.capture());
assertThat(request.getValue().getFromUuid()).isEqualTo(VALID_UUID);
assertThat(request.getValue().getToUuid()).isEqualTo(UNDISCOVERABLE_UUID);
assertThat(request.getValue().getUsers()).isEqualTo(Arrays.asList(new DirectoryReconciliationRequest.User(VALID_UUID, VALID_NUMBERRR)));
ArgumentCaptor<ClientContact> addedContact = ArgumentCaptor.forClass(ClientContact.class);
verify(directoryManager, times(1)).startBatchOperation();
verify(directoryManager, times(1)).add(eq(batchOperationHandle), addedContact.capture());
verify(directoryManager, times(1)).remove(eq(batchOperationHandle), eq(INACTIVE_NUMBERRR));
verify(directoryManager, times(1)).remove(eq(batchOperationHandle), eq(UNDISCOVERABLE_NUMBER));
verify(directoryManager, times(1)).stopBatchOperation(eq(batchOperationHandle));
assertThat(addedContact.getValue().getToken()).isEqualTo(Util.getContactToken(VALID_NUMBERRR));
verifyNoMoreInteractions(activeAccount);
verifyNoMoreInteractions(inactiveAccount);
verifyNoMoreInteractions(batchOperationHandle);
verifyNoMoreInteractions(directoryManager);
verifyNoMoreInteractions(reconciliationClient);
}
}