diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index e19e5155e..56d859fcd 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -171,6 +171,7 @@ import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener; import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator; import org.whispersystems.textsecuregcm.workers.CertificateCommand; import org.whispersystems.textsecuregcm.workers.DeleteUserCommand; +import org.whispersystems.textsecuregcm.workers.DescribeUserCommand; import org.whispersystems.textsecuregcm.workers.GetRedisCommandStatsCommand; import org.whispersystems.textsecuregcm.workers.GetRedisSlowlogCommand; import org.whispersystems.textsecuregcm.workers.SetCrawlerAccelerationTask; @@ -194,6 +195,7 @@ public class WhisperServerService extends Application("accountdb", "accountsdb.xml") { @Override diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/DescribeUserCommand.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/DescribeUserCommand.java new file mode 100644 index 000000000..ac0f93386 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/DescribeUserCommand.java @@ -0,0 +1,222 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import static com.codahale.metrics.MetricRegistry.name; + +import com.amazonaws.ClientConfiguration; +import com.amazonaws.auth.InstanceProfileCredentialsProvider; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; +import com.amazonaws.services.dynamodbv2.document.DynamoDB; +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 com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.datatype.jsr310.JSR310Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.dropwizard.Application; +import io.dropwizard.cli.ConfiguredCommand; +import io.dropwizard.cli.EnvironmentCommand; +import io.dropwizard.jdbi3.JdbiFactory; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import io.lettuce.core.resource.ClientResources; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.jdbi.v3.core.Jdbi; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.auth.AmbiguousIdentifier; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator; +import org.whispersystems.textsecuregcm.metrics.PushLatencyManager; +import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; +import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; +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.Device; +import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; +import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase; +import org.whispersystems.textsecuregcm.storage.KeysDynamoDb; +import org.whispersystems.textsecuregcm.storage.MessagesCache; +import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb; +import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.storage.Profiles; +import org.whispersystems.textsecuregcm.storage.ProfilesManager; +import org.whispersystems.textsecuregcm.storage.ReservedUsernames; +import org.whispersystems.textsecuregcm.storage.Usernames; +import org.whispersystems.textsecuregcm.storage.UsernamesManager; + +public class DescribeUserCommand extends EnvironmentCommand { + + private static class AccountSummary { + @JsonProperty + private final UUID uuid; + + @JsonProperty + private final String number; + + @JsonProperty + private final String currentProfileVersion; + + @JsonProperty + private final boolean discoverableByPhoneNumber; + + @JsonProperty + private final List devices; + + @JsonProperty + private final boolean hasSecureStorageManifest; + + public AccountSummary(final Account account, final boolean hasSecureStorageManifest) { + this.uuid = account.getUuid(); + this.number = account.getNumber(); + this.currentProfileVersion = account.getCurrentProfileVersion().orElse(null); + this.discoverableByPhoneNumber = account.isDiscoverableByPhoneNumber(); + this.devices = account.getDevices().stream().map(DeviceSummary::new).collect(Collectors.toList()); + this.hasSecureStorageManifest = hasSecureStorageManifest; + } + } + + private static class DeviceSummary { + @JsonProperty + private final long id; + + @JsonProperty + private final String name; + + @JsonProperty + private final String gcmToken; + + @JsonProperty + private final String apnToken; + + @JsonProperty + private final String voipApnToken; + + @JsonProperty + private final boolean fetchesMessages; + + @JsonProperty + private final LocalDate created; + + @JsonProperty + private final LocalDate lastSeen; + + @JsonProperty + private final DeviceCapabilities capabilities; + + public DeviceSummary(final Device device) { + this.id = device.getId(); + this.name = device.getName(); + this.gcmToken = device.getGcmId(); + this.apnToken = device.getApnId(); + this.voipApnToken = device.getVoipApnId(); + this.fetchesMessages = device.getFetchesMessages(); + this.created = LocalDate.ofInstant(Instant.ofEpochMilli(device.getCreated()), ZoneId.systemDefault()); + this.lastSeen = LocalDate.ofInstant(Instant.ofEpochMilli(device.getLastSeen()), ZoneId.systemDefault()); + + this.capabilities = device.getCapabilities(); + } + } + + public DescribeUserCommand() { + super(new Application<>() { + @Override + public void run(WhisperServerConfiguration configuration, Environment environment) { + } + }, "describe-user", "Print a summary of a user's account for debugging purposes"); + } + + @Override + public void configure(final Subparser subparser) { + super.configure(subparser); + + subparser.addArgument("-u", "--user") + .dest("user") + .type(String.class) + .required(true) + .help("The user to describe"); + } + + @Override + protected void run(Environment environment, Namespace namespace, WhisperServerConfiguration configuration) throws Exception { + + environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + JdbiFactory jdbiFactory = new JdbiFactory(); + Jdbi accountJdbi = jdbiFactory.build(environment, configuration.getAccountsDatabaseConfiguration(), "accountdb"); + FaultTolerantDatabase accountDatabase = new FaultTolerantDatabase("account_database_delete_user", accountJdbi, configuration.getAccountsDatabaseConfiguration().getCircuitBreakerConfiguration()); + ClientResources redisClusterClientResources = ClientResources.builder().build(); + + AmazonDynamoDBClientBuilder clientBuilder = AmazonDynamoDBClientBuilder + .standard() + .withRegion(configuration.getMessageDynamoDbConfiguration().getRegion()) + .withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) configuration.getMessageDynamoDbConfiguration().getClientExecutionTimeout().toMillis())) + .withRequestTimeout((int) configuration.getMessageDynamoDbConfiguration().getClientRequestTimeout().toMillis())) + .withCredentials(InstanceProfileCredentialsProvider.getInstance()); + + AmazonDynamoDBClientBuilder keysDynamoDbClientBuilder = AmazonDynamoDBClientBuilder + .standard() + .withRegion(configuration.getKeysDynamoDbConfiguration().getRegion()) + .withClientConfiguration(new ClientConfiguration().withClientExecutionTimeout(((int) configuration.getKeysDynamoDbConfiguration().getClientExecutionTimeout().toMillis())) + .withRequestTimeout((int) configuration.getKeysDynamoDbConfiguration().getClientRequestTimeout().toMillis())) + .withCredentials(InstanceProfileCredentialsProvider.getInstance()); + + DynamoDB messageDynamoDb = new DynamoDB(clientBuilder.build()); + DynamoDB preKeysDynamoDb = new DynamoDB(keysDynamoDbClientBuilder.build()); + + FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", configuration.getCacheClusterConfiguration(), redisClusterClientResources); + + ExecutorService keyspaceNotificationDispatchExecutor = environment.lifecycle().executorService(name(getClass(), "keyspaceNotification-%d")).maxThreads(4).build(); + ExecutorService storageServiceExecutor = environment.lifecycle().executorService(name(getClass(), "storageService-%d")).maxThreads(8).minThreads(1).build(); + + ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(configuration.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false); + + SecureStorageClient storageClient = new SecureStorageClient(storageCredentialsGenerator, storageServiceExecutor, configuration.getSecureStorageServiceConfiguration()); + Accounts accounts = new Accounts(accountDatabase); + Usernames usernames = new Usernames(accountDatabase); + Profiles profiles = new Profiles(accountDatabase); + ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase); + KeysDynamoDb keysDynamoDb = new KeysDynamoDb(preKeysDynamoDb, configuration.getKeysDynamoDbConfiguration().getTableName()); + MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(messageDynamoDb, configuration.getMessageDynamoDbConfiguration().getTableName(), configuration.getMessageDynamoDbConfiguration().getTimeToLive()); + 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()); + UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheCluster); + ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster); + MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, pushLatencyManager); + AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager); + + accountsManager.get(new AmbiguousIdentifier(namespace.getString("user"))).ifPresentOrElse(account -> { + final boolean hasStoredManifest = storageClient.hasStoredData(account.getUuid()).join(); + + try { + final ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()) + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + System.out.println(objectMapper.writeValueAsString(new AccountSummary(account, hasStoredManifest))); + } catch (final JsonProcessingException e) { + // This should never happen if we're going straight to a string + throw new RuntimeException(e); + } + }, + () -> System.err.println("User not found: " + namespace.getString("user"))); + } +}