/* * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.textsecuregcm; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3Client; import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.jdbi3.strategies.DefaultNameStrategy; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.DeserializationFeature; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.eclipse.jetty.servlets.CrossOriginFilter; import org.jdbi.v3.core.Jdbi; import org.signal.zkgroup.ServerSecretParams; import org.signal.zkgroup.auth.ServerZkAuthOperations; import org.signal.zkgroup.profiles.ServerZkProfileOperations; import org.whispersystems.dispatch.DispatchManager; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.CertificateGenerator; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccountAuthenticator; import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.controllers.*; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle; import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper; import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge; import org.whispersystems.textsecuregcm.metrics.FileDescriptorGauge; import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge; import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge; import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge; import org.whispersystems.textsecuregcm.providers.RedisClientFactory; import org.whispersystems.textsecuregcm.providers.RedisHealthCheck; import org.whispersystems.textsecuregcm.push.APNSender; import org.whispersystems.textsecuregcm.push.ApnFallbackManager; import org.whispersystems.textsecuregcm.push.GCMSender; import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.ReceiptSender; import org.whispersystems.textsecuregcm.push.WebsocketSender; import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient; import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool; import org.whispersystems.textsecuregcm.s3.PolicySigner; import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; import org.whispersystems.textsecuregcm.sms.SmsSender; import org.whispersystems.textsecuregcm.sms.TwilioSmsSender; import org.whispersystems.textsecuregcm.sqs.DirectoryQueue; import org.whispersystems.textsecuregcm.storage.*; import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener; import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler; 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.VacuumCommand; import org.whispersystems.textsecuregcm.workers.ZkParamsCommand; import org.whispersystems.websocket.WebSocketResourceProviderFactory; import org.whispersystems.websocket.setup.WebSocketEnvironment; import javax.servlet.DispatcherType; import javax.servlet.FilterRegistration; import javax.servlet.ServletRegistration; import java.security.Security; import java.util.EnumSet; import java.util.List; import java.util.Optional; import static com.codahale.metrics.MetricRegistry.name; import io.dropwizard.Application; import io.dropwizard.auth.AuthFilter; import io.dropwizard.auth.PolymorphicAuthDynamicFeature; import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; import io.dropwizard.auth.basic.BasicCredentialAuthFilter; import io.dropwizard.auth.basic.BasicCredentials; import io.dropwizard.db.DataSourceFactory; import io.dropwizard.db.PooledDataSourceFactory; import io.dropwizard.jdbi3.JdbiFactory; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; public class WhisperServerService extends Application { static { Security.addProvider(new BouncyCastleProvider()); } @Override public void initialize(Bootstrap bootstrap) { bootstrap.addCommand(new VacuumCommand()); bootstrap.addCommand(new DeleteUserCommand()); bootstrap.addCommand(new CertificateCommand()); bootstrap.addCommand(new ZkParamsCommand()); bootstrap.addBundle(new NameableMigrationsBundle("keysdb", "keysdb.xml") { @Override public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) { return configuration.getKeysDatabase(); } }); bootstrap.addBundle(new NameableMigrationsBundle("accountdb", "accountsdb.xml") { @Override public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) { return configuration.getAccountsDatabaseConfiguration(); } }); bootstrap.addBundle(new NameableMigrationsBundle("messagedb", "messagedb.xml") { @Override public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) { return configuration.getMessageStoreConfiguration(); } }); bootstrap.addBundle(new NameableMigrationsBundle("abusedb", "abusedb.xml") { @Override public PooledDataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) { return configuration.getAbuseDatabaseConfiguration(); } }); } @Override public String getName() { return "whisper-server"; } @Override public void run(WhisperServerConfiguration config, Environment environment) throws Exception { SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics()); environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); JdbiFactory jdbiFactory = new JdbiFactory(DefaultNameStrategy.CHECK_EMPTY); Jdbi accountJdbi = jdbiFactory.build(environment, config.getAccountsDatabaseConfiguration(), "accountdb"); Jdbi keysJdbi = jdbiFactory.build(environment, config.getKeysDatabase(), "keysdb"); Jdbi messageJdbi = jdbiFactory.build(environment, config.getMessageStoreConfiguration(), "messagedb" ); Jdbi abuseJdbi = jdbiFactory.build(environment, config.getAbuseDatabaseConfiguration(), "abusedb" ); FaultTolerantDatabase keysDatabase = new FaultTolerantDatabase("keys_database", keysJdbi, config.getKeysDatabase().getCircuitBreakerConfiguration()); FaultTolerantDatabase accountDatabase = new FaultTolerantDatabase("accounts_database", accountJdbi, config.getAccountsDatabaseConfiguration().getCircuitBreakerConfiguration()); FaultTolerantDatabase messageDatabase = new FaultTolerantDatabase("message_database", messageJdbi, config.getMessageStoreConfiguration().getCircuitBreakerConfiguration()); FaultTolerantDatabase abuseDatabase = new FaultTolerantDatabase("abuse_database", abuseJdbi, config.getAbuseDatabaseConfiguration().getCircuitBreakerConfiguration()); Accounts accounts = new Accounts(accountDatabase); PendingAccounts pendingAccounts = new PendingAccounts(accountDatabase); PendingDevices pendingDevices = new PendingDevices (accountDatabase); Usernames usernames = new Usernames(accountDatabase); ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase); Profiles profiles = new Profiles(accountDatabase); Keys keys = new Keys(keysDatabase); Messages messages = new Messages(messageDatabase); AbusiveHostRules abusiveHostRules = new AbusiveHostRules(abuseDatabase); 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 messagesClientFactory = new RedisClientFactory("message_cache", config.getMessageCacheConfiguration().getRedisConfiguration().getUrl(), config.getMessageCacheConfiguration().getRedisConfiguration().getReplicaUrls(), config.getMessageCacheConfiguration().getRedisConfiguration().getCircuitBreakerConfiguration()); RedisClientFactory pushSchedulerClientFactory = new RedisClientFactory("push_scheduler_cache", config.getPushScheduler().getUrl(), config.getPushScheduler().getReplicaUrls(), config.getPushScheduler().getCircuitBreakerConfiguration()); ReplicatedJedisPool cacheClient = cacheClientFactory.getRedisClientPool(); ReplicatedJedisPool directoryClient = directoryClientFactory.getRedisClientPool(); ReplicatedJedisPool messagesClient = messagesClientFactory.getRedisClientPool(); ReplicatedJedisPool pushSchedulerClient = pushSchedulerClientFactory.getRedisClientPool(); DirectoryManager directory = new DirectoryManager(directoryClient); DirectoryQueue directoryQueue = new DirectoryQueue(config.getDirectoryConfiguration().getSqsConfiguration()); PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheClient); PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, cacheClient ); AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient); UsernamesManager usernamesManager = new UsernamesManager(usernames, reservedUsernames, cacheClient); ProfilesManager profilesManager = new ProfilesManager(profiles, cacheClient); MessagesCache messagesCache = new MessagesCache(messagesClient, messages, accountsManager, config.getMessageCacheConfiguration().getPersistDelayMinutes()); MessagesManager messagesManager = new MessagesManager(messages, messagesCache); DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager); DispatchManager dispatchManager = new DispatchManager(cacheClientFactory, Optional.of(deadLetterHandler)); PubSubManager pubSubManager = new PubSubManager(cacheClient, dispatchManager); APNSender apnSender = new APNSender(accountsManager, config.getApnConfiguration()); GCMSender gcmSender = new GCMSender(accountsManager, config.getGcmConfiguration().getApiKey()); WebsocketSender websocketSender = new WebsocketSender(messagesManager, pubSubManager); RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheClient); AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager); DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager); ExternalServiceCredentialGenerator directoryCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(), config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret(), true); ExternalServiceCredentialGenerator storageCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getSecureStorageServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false); ExternalServiceCredentialGenerator backupCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getSecureBackupServiceConfiguration().getUserAuthenticationTokenSharedSecret(), new byte[0], false); ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushSchedulerClient, apnSender, accountsManager); TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration()); SmsSender smsSender = new SmsSender(twilioSmsSender); PushSender pushSender = new PushSender(apnFallbackManager, gcmSender, apnSender, websocketSender, config.getPushConfiguration().getQueueSize()); ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender); TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration()); RecaptchaClient recaptchaClient = new RecaptchaClient(config.getRecaptchaConfiguration().getSecret()); DirectoryReconciliationClient directoryReconciliationClient = new DirectoryReconciliationClient(config.getDirectoryConfiguration().getDirectoryServerConfiguration()); ActiveUserCounter activeUserCounter = new ActiveUserCounter(config.getMetricsFactory(), cacheClient); DirectoryReconciler directoryReconciler = new DirectoryReconciler(directoryReconciliationClient, directory); AccountCleaner accountCleaner = new AccountCleaner(accountsManager, directoryQueue); PushFeedbackProcessor pushFeedbackProcessor = new PushFeedbackProcessor(accountsManager, directoryQueue); List accountDatabaseCrawlerListeners = List.of(pushFeedbackProcessor, activeUserCounter, directoryReconciler, accountCleaner); AccountDatabaseCrawlerCache accountDatabaseCrawlerCache = new AccountDatabaseCrawlerCache(cacheClient); AccountDatabaseCrawler accountDatabaseCrawler = new AccountDatabaseCrawler(accountsManager, accountDatabaseCrawlerCache, accountDatabaseCrawlerListeners, config.getAccountDatabaseCrawlerConfiguration().getChunkSize(), config.getAccountDatabaseCrawlerConfiguration().getChunkIntervalMs()); messagesCache.setPubSubManager(pubSubManager, pushSender); apnSender.setApnFallbackManager(apnFallbackManager); environment.lifecycle().manage(apnFallbackManager); environment.lifecycle().manage(pubSubManager); environment.lifecycle().manage(pushSender); environment.lifecycle().manage(messagesCache); environment.lifecycle().manage(accountDatabaseCrawler); AWSCredentials credentials = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret()); AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials); AmazonS3 cdnS3Client = AmazonS3Client.builder().withCredentials(credentialsProvider).withRegion(config.getCdnConfiguration().getRegion()).build(); PostPolicyGenerator cdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket(), config.getCdnConfiguration().getAccessKey()); PolicySigner cdnPolicySigner = new PolicySigner(config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion()); ServerSecretParams zkSecretParams = new ServerSecretParams(config.getZkConfig().getServerSecret()); ServerZkProfileOperations zkProfileOperations = new ServerZkProfileOperations(zkSecretParams); ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams); boolean isZkEnabled = config.getZkConfig().isEnabled(); AttachmentControllerV1 attachmentControllerV1 = new AttachmentControllerV1(rateLimiters, config.getAttachmentsConfiguration().getAccessKey(), config.getAttachmentsConfiguration().getAccessSecret(), config.getAttachmentsConfiguration().getBucket() ); AttachmentControllerV2 attachmentControllerV2 = new AttachmentControllerV2(rateLimiters, config.getAttachmentsConfiguration().getAccessKey(), config.getAttachmentsConfiguration().getAccessSecret(), config.getAttachmentsConfiguration().getRegion(), config.getAttachmentsConfiguration().getBucket()); KeysController keysController = new KeysController(rateLimiters, keys, accountsManager, directoryQueue); 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); StickerController stickerController = new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket()); AuthFilter accountAuthFilter = new BasicCredentialAuthFilter.Builder().setAuthenticator(accountAuthenticator).buildAuthFilter (); AuthFilter disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder().setAuthenticator(disabledPermittedAccountAuthenticator).buildAuthFilter(); environment.jersey().register(new PolymorphicAuthDynamicFeature<>(ImmutableMap.of(Account.class, accountAuthFilter, DisabledPermittedAccount.class, disabledPermittedAccountAuthFilter))); environment.jersey().register(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class))); 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 ProvisioningController(rateLimiters, pushSender)); 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())); environment.jersey().register(new SecureStorageController(storageCredentialsGenerator)); environment.jersey().register(new SecureBackupController(backupCredentialsGenerator)); environment.jersey().register(attachmentControllerV1); environment.jersey().register(attachmentControllerV2); environment.jersey().register(keysController); environment.jersey().register(messageController); environment.jersey().register(profileController); environment.jersey().register(stickerController); /// WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config.getWebSocketConfiguration(), 90000); webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(accountAuthenticator)); webSocketEnvironment.setConnectListener(new AuthenticatedConnectListener(pushSender, receiptSender, messagesManager, pubSubManager, apnFallbackManager)); webSocketEnvironment.jersey().register(new KeepAliveController(pubSubManager)); webSocketEnvironment.jersey().register(messageController); webSocketEnvironment.jersey().register(profileController); webSocketEnvironment.jersey().register(attachmentControllerV1); webSocketEnvironment.jersey().register(attachmentControllerV2); WebSocketEnvironment provisioningEnvironment = new WebSocketEnvironment(environment, webSocketEnvironment.getRequestLog(), 60000); provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(pubSubManager)); provisioningEnvironment.jersey().register(new KeepAliveController(pubSubManager)); WebSocketResourceProviderFactory webSocketServlet = new WebSocketResourceProviderFactory(webSocketEnvironment ); WebSocketResourceProviderFactory provisioningServlet = new WebSocketResourceProviderFactory(provisioningEnvironment); ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", webSocketServlet ); ServletRegistration.Dynamic provisioning = environment.servlets().addServlet("Provisioning", provisioningServlet); websocket.addMapping("/v1/websocket/"); websocket.setAsyncSupported(true); provisioning.addMapping("/v1/websocket/provisioning/"); provisioning.setAsyncSupported(true); webSocketServlet.start(); provisioningServlet.start(); FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class); filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*"); filter.setInitParameter("allowedOrigins", "*"); filter.setInitParameter("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin,X-Signal-Agent"); filter.setInitParameter("allowedMethods", "GET,PUT,POST,DELETE,OPTIONS"); filter.setInitParameter("preflightMaxAge", "5184000"); filter.setInitParameter("allowCredentials", "true"); /// environment.healthChecks().register("directory", new RedisHealthCheck(directoryClient)); environment.healthChecks().register("cache", new RedisHealthCheck(cacheClient)); environment.jersey().register(new IOExceptionMapper()); environment.jersey().register(new RateLimitExceededExceptionMapper()); environment.jersey().register(new InvalidWebsocketAddressExceptionMapper()); environment.jersey().register(new DeviceLimitExceededExceptionMapper()); environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge()); environment.metrics().register(name(FreeMemoryGauge.class, "free_memory"), new FreeMemoryGauge()); environment.metrics().register(name(NetworkSentGauge.class, "bytes_sent"), new NetworkSentGauge()); environment.metrics().register(name(NetworkReceivedGauge.class, "bytes_received"), new NetworkReceivedGauge()); environment.metrics().register(name(FileDescriptorGauge.class, "fd_count"), new FileDescriptorGauge()); } public static void main(String[] args) throws Exception { new WhisperServerService().run(args); } }