From 14cff958e97e60a1a759133aa86bd0d52fc6b5b2 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Thu, 18 Nov 2021 15:39:46 -0500 Subject: [PATCH] Migrate challenge-issuing rate limiters to the abusive message filter --- abusive-message-filter | 2 +- pom.xml | 6 + service/pom.xml | 1 - .../textsecuregcm/WhisperServerService.java | 30 ++-- .../abuse/RateLimitChallengeListener.java | 24 +++ .../abuse/RateLimitChallengeType.java | 12 ++ ...ynamicRateLimitChallengeConfiguration.java | 7 - .../DynamicRateLimitsConfiguration.java | 14 -- .../controllers/KeysController.java | 36 +---- .../controllers/MessageController.java | 26 ---- .../limits/DynamicRateLimiters.java | 63 +------- .../limits/PreKeyRateLimiter.java | 79 ---------- .../limits/RateLimitChallengeManager.java | 64 ++------ .../RateLimitChallengeOptionManager.java | 65 ++++++++ .../limits/RateLimitResetMetricsManager.java | 45 ------ .../limits/UnsealedSenderRateLimiter.java | 115 --------------- .../RateLimitChallengeExceptionMapper.java | 10 +- .../dynamic/DynamicConfigurationTest.java | 6 +- .../controllers/MessageControllerTest.java | 78 +--------- .../limits/PreKeyRateLimiterTest.java | 66 --------- .../limits/RateLimitChallengeManagerTest.java | 133 ++--------------- .../RateLimitChallengeOptionManagerTest.java | 139 ++++++++++++++++++ .../RateLimitResetMetricsManagerTest.java | 61 -------- .../limits/UnsealedSenderRateLimiterTest.java | 120 --------------- .../tests/controllers/KeysControllerTest.java | 59 +------- .../tests/limits/DynamicRateLimitsTest.java | 32 ++-- 26 files changed, 311 insertions(+), 982 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/abuse/RateLimitChallengeListener.java create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/abuse/RateLimitChallengeType.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/limits/PreKeyRateLimiter.java create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManager.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitResetMetricsManager.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/limits/UnsealedSenderRateLimiter.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/limits/PreKeyRateLimiterTest.java create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManagerTest.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitResetMetricsManagerTest.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/limits/UnsealedSenderRateLimiterTest.java diff --git a/abusive-message-filter b/abusive-message-filter index 6a74e85e4..82b7b374c 160000 --- a/abusive-message-filter +++ b/abusive-message-filter @@ -1 +1 @@ -Subproject commit 6a74e85e41d706e48865f45cfcb41208c28c7e44 +Subproject commit 82b7b374c57cf993f9a188d7a5ee7e7be83beb36 diff --git a/pom.xml b/pom.xml index 7bd9aca6c..d37ca7b85 100644 --- a/pom.xml +++ b/pom.xml @@ -253,6 +253,12 @@ gson ${gson.version} + + org.signal + embedded-redis + 0.8.1 + test + diff --git a/service/pom.xml b/service/pom.xml index f932df5ca..e5147f1bf 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -420,7 +420,6 @@ org.signal embedded-redis - 0.8.1 test diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 90936e452..b30958ff8 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -66,6 +66,7 @@ import org.slf4j.LoggerFactory; import org.whispersystems.dispatch.DispatchManager; import org.whispersystems.textsecuregcm.abuse.AbusiveMessageFilter; import org.whispersystems.textsecuregcm.abuse.FilterAbusiveMessages; +import org.whispersystems.textsecuregcm.abuse.RateLimitChallengeListener; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.CertificateGenerator; @@ -109,12 +110,10 @@ import org.whispersystems.textsecuregcm.filters.ContentLengthFilter; import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter; import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter; import org.whispersystems.textsecuregcm.limits.DynamicRateLimiters; -import org.whispersystems.textsecuregcm.limits.PreKeyRateLimiter; import org.whispersystems.textsecuregcm.limits.PushChallengeManager; import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager; -import org.whispersystems.textsecuregcm.limits.RateLimitResetMetricsManager; +import org.whispersystems.textsecuregcm.limits.RateLimitChallengeOptionManager; import org.whispersystems.textsecuregcm.limits.RateLimiters; -import org.whispersystems.textsecuregcm.limits.UnsealedSenderRateLimiter; import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle; import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper; import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper; @@ -492,11 +491,6 @@ public class WhisperServerService extends Application commonControllers = Lists.newArrayList( new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket()), @@ -662,8 +657,8 @@ public class WhisperServerService extends Application dynamicConfigurationManager; - private final AtomicReference unsealedSenderCardinalityLimiter; - private final AtomicReference unsealedIpLimiter; private final AtomicReference rateLimitResetLimiter; private final AtomicReference recaptchaChallengeAttemptLimiter; private final AtomicReference recaptchaChallengeSuccessLimiter; private final AtomicReference pushChallengeAttemptLimiter; private final AtomicReference pushChallengeSuccessLimiter; - private final AtomicReference dailyPreKeysLimiter; public DynamicRateLimiters(final FaultTolerantRedisCluster rateLimitCluster, final DynamicConfigurationManager dynamicConfigurationManager) { @@ -33,18 +29,6 @@ public class DynamicRateLimiters { this.cacheCluster = rateLimitCluster; this.dynamicConfigurationManager = dynamicConfigurationManager; - this.dailyPreKeysLimiter = new AtomicReference<>( - createDailyPreKeysLimiter(this.cacheCluster, - this.dynamicConfigurationManager.getConfiguration().getLimits().getDailyPreKeys())); - - this.unsealedSenderCardinalityLimiter = new AtomicReference<>(createUnsealedSenderCardinalityLimiter( - this.cacheCluster, - this.dynamicConfigurationManager.getConfiguration().getLimits().getUnsealedSenderNumber())); - - this.unsealedIpLimiter = new AtomicReference<>( - createUnsealedIpLimiter(this.cacheCluster, - this.dynamicConfigurationManager.getConfiguration().getLimits().getUnsealedSenderIp())); - this.rateLimitResetLimiter = new AtomicReference<>( createRateLimitResetLimiter(this.cacheCluster, this.dynamicConfigurationManager.getConfiguration().getLimits().getRateLimitReset())); @@ -64,26 +48,6 @@ public class DynamicRateLimiters { this.dynamicConfigurationManager.getConfiguration().getLimits().getPushChallengeSuccess())); } - public CardinalityRateLimiter getUnsealedSenderCardinalityLimiter() { - CardinalityRateLimitConfiguration currentConfiguration = dynamicConfigurationManager.getConfiguration().getLimits() - .getUnsealedSenderNumber(); - - return this.unsealedSenderCardinalityLimiter.updateAndGet(rateLimiter -> { - if (rateLimiter.hasConfiguration(currentConfiguration)) { - return rateLimiter; - } else { - return createUnsealedSenderCardinalityLimiter(cacheCluster, currentConfiguration); - } - }); - } - - public RateLimiter getUnsealedIpLimiter() { - return updateAndGetRateLimiter( - unsealedIpLimiter, - dynamicConfigurationManager.getConfiguration().getLimits().getUnsealedSenderIp(), - this::createUnsealedIpLimiter); - } - public RateLimiter getRateLimitResetLimiter() { return updateAndGetRateLimiter( rateLimitResetLimiter, @@ -119,13 +83,6 @@ public class DynamicRateLimiters { this::createPushChallengeSuccessLimiter); } - public RateLimiter getDailyPreKeysLimiter() { - return updateAndGetRateLimiter( - dailyPreKeysLimiter, - dynamicConfigurationManager.getConfiguration().getLimits().getDailyPreKeys(), - this::createDailyPreKeysLimiter); - } - private RateLimiter updateAndGetRateLimiter(final AtomicReference rateLimiter, RateLimitConfiguration currentConfiguration, BiFunction rateLimitFactory) { @@ -139,17 +96,6 @@ public class DynamicRateLimiters { }); } - private CardinalityRateLimiter createUnsealedSenderCardinalityLimiter(FaultTolerantRedisCluster cacheCluster, - CardinalityRateLimitConfiguration configuration) { - return new CardinalityRateLimiter(cacheCluster, "unsealedSender", configuration.getTtl(), - configuration.getMaxCardinality()); - } - - private RateLimiter createUnsealedIpLimiter(FaultTolerantRedisCluster cacheCluster, - RateLimitConfiguration configuration) { - return createLimiter(cacheCluster, configuration, "unsealedIp"); - } - public RateLimiter createRateLimitResetLimiter(FaultTolerantRedisCluster cacheCluster, RateLimitConfiguration configuration) { return createLimiter(cacheCluster, configuration, "rateLimitReset"); @@ -175,11 +121,6 @@ public class DynamicRateLimiters { return createLimiter(cacheCluster, configuration, "pushChallengeSuccess"); } - public RateLimiter createDailyPreKeysLimiter(FaultTolerantRedisCluster cacheCluster, - RateLimitConfiguration configuration) { - return createLimiter(cacheCluster, configuration, "dailyPreKeys"); - } - private RateLimiter createLimiter(FaultTolerantRedisCluster cacheCluster, RateLimitConfiguration configuration, String name) { return new RateLimiter(cacheCluster, name, diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/limits/PreKeyRateLimiter.java b/service/src/main/java/org/whispersystems/textsecuregcm/limits/PreKeyRateLimiter.java deleted file mode 100644 index 854c8a756..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/limits/PreKeyRateLimiter.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.limits; - -import static com.codahale.metrics.MetricRegistry.name; - -import io.dropwizard.util.Duration; -import io.micrometer.core.instrument.Metrics; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; -import org.whispersystems.textsecuregcm.storage.Account; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; -import org.whispersystems.textsecuregcm.util.Util; - -public class PreKeyRateLimiter { - - private static final String RATE_LIMIT_RESET_COUNTER_NAME = name(PreKeyRateLimiter.class, "reset"); - private static final String RATE_LIMITED_PREKEYS_COUNTER_NAME = name(PreKeyRateLimiter.class, "rateLimited"); - private static final String RATE_LIMITED_PREKEYS_TOTAL_ACCOUNTS_COUNTER_NAME = name(PreKeyRateLimiter.class, "rateLimitedTotal"); - private static final String RATE_LIMITED_PREKEYS_ACCOUNTS_ENFORCED_COUNTER_NAME = name(PreKeyRateLimiter.class, "rateLimitedAccountsEnforced"); - private static final String RATE_LIMITED_PREKEYS_ACCOUNTS_UNENFORCED_COUNTER_NAME = name(PreKeyRateLimiter.class, "rateLimitedAccountsUnenforced"); - - private static final String RATE_LIMITED_ACCOUNTS_HLL_KEY = "PreKeyRateLimiter::rateLimitedAccounts"; - private static final String RATE_LIMITED_ACCOUNTS_ENFORCED_HLL_KEY = "PreKeyRateLimiter::rateLimitedAccounts::enforced"; - private static final String RATE_LIMITED_ACCOUNTS_UNENFORCED_HLL_KEY = "PreKeyRateLimiter::rateLimitedAccounts::unenforced"; - private static final long RATE_LIMITED_ACCOUNTS_HLL_TTL_SECONDS = Duration.days(1).toSeconds(); - - private final DynamicRateLimiters rateLimiters; - private final DynamicConfigurationManager dynamicConfigurationManager; - private final RateLimitResetMetricsManager metricsManager; - - public PreKeyRateLimiter(final DynamicRateLimiters rateLimiters, - final DynamicConfigurationManager dynamicConfigurationManager, - final RateLimitResetMetricsManager metricsManager) { - this.rateLimiters = rateLimiters; - this.dynamicConfigurationManager = dynamicConfigurationManager; - this.metricsManager = metricsManager; - - metricsManager.initializeFunctionCounters(RATE_LIMITED_PREKEYS_TOTAL_ACCOUNTS_COUNTER_NAME, - RATE_LIMITED_ACCOUNTS_HLL_KEY); - metricsManager.initializeFunctionCounters(RATE_LIMITED_PREKEYS_ACCOUNTS_ENFORCED_COUNTER_NAME, - RATE_LIMITED_ACCOUNTS_ENFORCED_HLL_KEY); - metricsManager.initializeFunctionCounters(RATE_LIMITED_PREKEYS_ACCOUNTS_UNENFORCED_COUNTER_NAME, - RATE_LIMITED_ACCOUNTS_UNENFORCED_HLL_KEY); - } - - public void validate(final Account account) throws RateLimitExceededException { - - try { - rateLimiters.getDailyPreKeysLimiter().validate(account.getUuid()); - } catch (final RateLimitExceededException e) { - - final boolean enforceLimit = dynamicConfigurationManager.getConfiguration() - .getRateLimitChallengeConfiguration().isPreKeyLimitEnforced(); - - metricsManager.recordMetrics(account, enforceLimit, - RATE_LIMITED_PREKEYS_COUNTER_NAME, - enforceLimit ? RATE_LIMITED_ACCOUNTS_ENFORCED_HLL_KEY : RATE_LIMITED_ACCOUNTS_UNENFORCED_HLL_KEY, - RATE_LIMITED_ACCOUNTS_HLL_KEY, - RATE_LIMITED_ACCOUNTS_HLL_TTL_SECONDS - ); - - if (enforceLimit) { - throw e; - } - } - } - - public void handleRateLimitReset(final Account account) { - - rateLimiters.getDailyPreKeysLimiter().clear(account.getUuid()); - - Metrics.counter(RATE_LIMIT_RESET_COUNTER_NAME, "countryCode", Util.getCountryCode(account.getNumber())) - .increment(); - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManager.java index a28878cf2..3ca7e361a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManager.java @@ -2,35 +2,26 @@ package org.whispersystems.textsecuregcm.limits; import static com.codahale.metrics.MetricRegistry.name; -import com.vdurmont.semver4j.Semver; import io.micrometer.core.instrument.Metrics; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Optional; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.abuse.RateLimitChallengeListener; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient; import org.whispersystems.textsecuregcm.storage.Account; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.util.Util; -import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; -import org.whispersystems.textsecuregcm.util.ua.UserAgent; -import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; public class RateLimitChallengeManager { private final PushChallengeManager pushChallengeManager; private final RecaptchaClient recaptchaClient; - private final PreKeyRateLimiter preKeyRateLimiter; - private final UnsealedSenderRateLimiter unsealedSenderRateLimiter; - private final DynamicRateLimiters rateLimiters; - private final DynamicConfigurationManager dynamicConfigurationManager; - public static final String OPTION_RECAPTCHA = "recaptcha"; - public static final String OPTION_PUSH_CHALLENGE = "pushChallenge"; + private final List rateLimitChallengeListeners = + Collections.synchronizedList(new ArrayList<>()); private static final String RECAPTCHA_ATTEMPT_COUNTER_NAME = name(RateLimitChallengeManager.class, "recaptcha", "attempt"); private static final String RESET_RATE_LIMIT_EXCEEDED_COUNTER_NAME = name(RateLimitChallengeManager.class, "resetRateLimitExceeded"); @@ -41,17 +32,15 @@ public class RateLimitChallengeManager { public RateLimitChallengeManager( final PushChallengeManager pushChallengeManager, final RecaptchaClient recaptchaClient, - final PreKeyRateLimiter preKeyRateLimiter, - final UnsealedSenderRateLimiter unsealedSenderRateLimiter, - final DynamicRateLimiters rateLimiters, - final DynamicConfigurationManager dynamicConfigurationManager) { + final DynamicRateLimiters rateLimiters) { this.pushChallengeManager = pushChallengeManager; this.recaptchaClient = recaptchaClient; - this.preKeyRateLimiter = preKeyRateLimiter; - this.unsealedSenderRateLimiter = unsealedSenderRateLimiter; this.rateLimiters = rateLimiters; - this.dynamicConfigurationManager = dynamicConfigurationManager; + } + + public void addListener(final RateLimitChallengeListener rateLimitChallengeListener) { + rateLimitChallengeListeners.add(rateLimitChallengeListener); } public void answerPushChallenge(final Account account, final String challenge) throws RateLimitExceededException { @@ -92,40 +81,7 @@ public class RateLimitChallengeManager { throw e; } - preKeyRateLimiter.handleRateLimitReset(account); - unsealedSenderRateLimiter.handleRateLimitReset(account); - } - - public boolean isClientBelowMinimumVersion(final String userAgent) { - try { - final UserAgent client = UserAgentUtil.parseUserAgentString(userAgent); - final Optional minimumClientVersion = dynamicConfigurationManager.getConfiguration() - .getRateLimitChallengeConfiguration() - .getMinimumSupportedVersion(client.getPlatform()); - - return minimumClientVersion.map(version -> version.isGreaterThan(client.getVersion())) - .orElse(true); - } catch (final UnrecognizedUserAgentException ignored) { - return false; - } - } - - public List getChallengeOptions(final Account account) { - final List options = new ArrayList<>(2); - - if (rateLimiters.getRecaptchaChallengeAttemptLimiter().hasAvailablePermits(account.getUuid(), 1) && - rateLimiters.getRecaptchaChallengeSuccessLimiter().hasAvailablePermits(account.getUuid(), 1)) { - - options.add(OPTION_RECAPTCHA); - } - - if (rateLimiters.getPushChallengeAttemptLimiter().hasAvailablePermits(account.getUuid(), 1) && - rateLimiters.getPushChallengeSuccessLimiter().hasAvailablePermits(account.getUuid(), 1)) { - - options.add(OPTION_PUSH_CHALLENGE); - } - - return options; + rateLimitChallengeListeners.forEach(listener -> listener.handleRateLimitChallengeAnswered(account)); } public void sendPushChallenge(final Account account) throws NotPushRegisteredException { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManager.java new file mode 100644 index 000000000..92e561783 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManager.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import com.vdurmont.semver4j.Semver; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; +import org.whispersystems.textsecuregcm.util.ua.UserAgent; +import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class RateLimitChallengeOptionManager { + + private final DynamicRateLimiters rateLimiters; + private final DynamicConfigurationManager dynamicConfigurationManager; + + public static final String OPTION_RECAPTCHA = "recaptcha"; + public static final String OPTION_PUSH_CHALLENGE = "pushChallenge"; + + public RateLimitChallengeOptionManager(final DynamicRateLimiters rateLimiters, + final DynamicConfigurationManager dynamicConfigurationManager) { + + this.rateLimiters = rateLimiters; + this.dynamicConfigurationManager = dynamicConfigurationManager; + } + + public boolean isClientBelowMinimumVersion(final String userAgent) { + try { + final UserAgent client = UserAgentUtil.parseUserAgentString(userAgent); + final Optional minimumClientVersion = dynamicConfigurationManager.getConfiguration() + .getRateLimitChallengeConfiguration() + .getMinimumSupportedVersion(client.getPlatform()); + + return minimumClientVersion.map(version -> version.isGreaterThan(client.getVersion())) + .orElse(true); + } catch (final UnrecognizedUserAgentException ignored) { + return false; + } + } + + public List getChallengeOptions(final Account account) { + final List options = new ArrayList<>(2); + + if (rateLimiters.getRecaptchaChallengeAttemptLimiter().hasAvailablePermits(account.getUuid(), 1) && + rateLimiters.getRecaptchaChallengeSuccessLimiter().hasAvailablePermits(account.getUuid(), 1)) { + + options.add(OPTION_RECAPTCHA); + } + + if (rateLimiters.getPushChallengeAttemptLimiter().hasAvailablePermits(account.getUuid(), 1) && + rateLimiters.getPushChallengeSuccessLimiter().hasAvailablePermits(account.getUuid(), 1)) { + + options.add(OPTION_PUSH_CHALLENGE); + } + + return options; + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitResetMetricsManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitResetMetricsManager.java deleted file mode 100644 index 56fcef6e5..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimitResetMetricsManager.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.whispersystems.textsecuregcm.limits; - -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.FunctionCounter; -import io.micrometer.core.instrument.MeterRegistry; -import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; -import org.whispersystems.textsecuregcm.storage.Account; - -public class RateLimitResetMetricsManager { - - private final FaultTolerantRedisCluster metricsCluster; - private final MeterRegistry meterRegistry; - - public RateLimitResetMetricsManager( - final FaultTolerantRedisCluster metricsCluster, final MeterRegistry meterRegistry) { - this.metricsCluster = metricsCluster; - this.meterRegistry = meterRegistry; - } - - void initializeFunctionCounters(String counterKey, String hllKey) { - FunctionCounter - .builder(counterKey, this, manager -> manager.getCount(hllKey)) - .register(meterRegistry); - } - - Long getCount(final String hllKey) { - return metricsCluster.withCluster(conn -> conn.sync().pfcount(hllKey)); - } - - void recordMetrics(Account account, boolean enforced, String counterKey, String hllEnforcedKey, String hllTotalKey, - long hllTtl) { - - Counter.builder(counterKey) - .tag("enforced", String.valueOf(enforced)) - .register(meterRegistry) - .increment(); - - metricsCluster.useCluster(connection -> { - connection.sync().pfadd(hllEnforcedKey, account.getUuid().toString()); - connection.sync().expire(hllEnforcedKey, hllTtl); - connection.sync().pfadd(hllTotalKey, account.getUuid().toString()); - connection.sync().expire(hllTotalKey, hllTtl); - }); - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/limits/UnsealedSenderRateLimiter.java b/service/src/main/java/org/whispersystems/textsecuregcm/limits/UnsealedSenderRateLimiter.java deleted file mode 100644 index 9d6cbb27b..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/limits/UnsealedSenderRateLimiter.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.limits; - -import static com.codahale.metrics.MetricRegistry.name; - -import io.dropwizard.util.Duration; -import io.lettuce.core.SetArgs; -import io.micrometer.core.instrument.Metrics; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitsConfiguration; -import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; -import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; -import org.whispersystems.textsecuregcm.storage.Account; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; -import org.whispersystems.textsecuregcm.util.Util; - -public class UnsealedSenderRateLimiter { - - private final DynamicRateLimiters rateLimiters; - private final FaultTolerantRedisCluster rateLimitCluster; - private final DynamicConfigurationManager dynamicConfigurationManager; - private final RateLimitResetMetricsManager metricsManager; - - private static final String RATE_LIMIT_RESET_COUNTER_NAME = name(UnsealedSenderRateLimiter.class, "reset"); - private static final String RATE_LIMITED_UNSEALED_SENDER_COUNTER_NAME = name(UnsealedSenderRateLimiter.class, "rateLimited"); - private static final String RATE_LIMITED_UNSEALED_SENDER_ACCOUNTS_TOTAL_COUNTER_NAME = name(UnsealedSenderRateLimiter.class, "rateLimitedAccountsTotal"); - private static final String RATE_LIMITED_UNSEALED_SENDER_ACCOUNTS_ENFORCED_COUNTER_NAME = name(UnsealedSenderRateLimiter.class, "rateLimitedAccountsEnforced"); - private static final String RATE_LIMITED_UNSEALED_SENDER_ACCOUNTS_UNENFORCED_COUNTER_NAME = name(UnsealedSenderRateLimiter.class, "rateLimitedAccountsUnenforced"); - - private static final String RATE_LIMITED_ACCOUNTS_HLL_KEY = "UnsealedSenderRateLimiter::rateLimitedAccounts::total"; - private static final String RATE_LIMITED_ACCOUNTS_ENFORCED_HLL_KEY = "UnsealedSenderRateLimiter::rateLimitedAccounts::enforced"; - private static final String RATE_LIMITED_ACCOUNTS_UNENFORCED_HLL_KEY = "UnsealedSenderRateLimiter::rateLimitedAccounts::unenforced"; - private static final long RATE_LIMITED_ACCOUNTS_HLL_TTL_SECONDS = Duration.days(1).toSeconds(); - - - public UnsealedSenderRateLimiter(final DynamicRateLimiters rateLimiters, - final FaultTolerantRedisCluster rateLimitCluster, - final DynamicConfigurationManager dynamicConfigurationManager, - final RateLimitResetMetricsManager metricsManager) { - - this.rateLimiters = rateLimiters; - this.rateLimitCluster = rateLimitCluster; - this.dynamicConfigurationManager = dynamicConfigurationManager; - this.metricsManager = metricsManager; - - metricsManager.initializeFunctionCounters(RATE_LIMITED_UNSEALED_SENDER_ACCOUNTS_TOTAL_COUNTER_NAME, - RATE_LIMITED_ACCOUNTS_HLL_KEY); - metricsManager.initializeFunctionCounters(RATE_LIMITED_UNSEALED_SENDER_ACCOUNTS_ENFORCED_COUNTER_NAME, - RATE_LIMITED_ACCOUNTS_ENFORCED_HLL_KEY); - metricsManager.initializeFunctionCounters(RATE_LIMITED_UNSEALED_SENDER_ACCOUNTS_UNENFORCED_COUNTER_NAME, - RATE_LIMITED_ACCOUNTS_UNENFORCED_HLL_KEY); - } - - public void validate(final Account sender, final Account destination) throws RateLimitExceededException { - final int maxCardinality = rateLimitCluster.withCluster(connection -> { - final String cardinalityString = connection.sync().get(getMaxCardinalityKey(sender)); - - return cardinalityString != null - ? Integer.parseInt(cardinalityString) - : dynamicConfigurationManager.getConfiguration().getLimits().getUnsealedSenderDefaultCardinalityLimit(); - }); - - try { - rateLimiters.getUnsealedSenderCardinalityLimiter() - .validate(sender.getUuid().toString(), destination.getUuid().toString(), maxCardinality); - } catch (final RateLimitExceededException e) { - - final boolean enforceLimit = dynamicConfigurationManager.getConfiguration() - .getRateLimitChallengeConfiguration().isUnsealedSenderLimitEnforced(); - - metricsManager.recordMetrics(sender, enforceLimit, RATE_LIMITED_UNSEALED_SENDER_COUNTER_NAME, - enforceLimit ? RATE_LIMITED_ACCOUNTS_ENFORCED_HLL_KEY : RATE_LIMITED_ACCOUNTS_UNENFORCED_HLL_KEY, - RATE_LIMITED_ACCOUNTS_HLL_KEY, - RATE_LIMITED_ACCOUNTS_HLL_TTL_SECONDS - ); - - if (enforceLimit) { - throw e; - } - } - } - - public void handleRateLimitReset(final Account account) { - rateLimitCluster.useCluster(connection -> { - final CardinalityRateLimiter unsealedSenderCardinalityLimiter = rateLimiters.getUnsealedSenderCardinalityLimiter(); - final DynamicRateLimitsConfiguration rateLimitsConfiguration = - dynamicConfigurationManager.getConfiguration().getLimits(); - - final long ttl; - { - final long remainingTtl = unsealedSenderCardinalityLimiter.getRemainingTtl(account.getUuid().toString()); - ttl = remainingTtl > 0 ? remainingTtl : unsealedSenderCardinalityLimiter.getInitialTtl().toSeconds(); - } - - final String key = getMaxCardinalityKey(account); - - connection.sync().set(key, - String.valueOf(rateLimitsConfiguration.getUnsealedSenderDefaultCardinalityLimit()), - SetArgs.Builder.nx().ex(ttl)); - - connection.sync().incrby(key, rateLimitsConfiguration.getUnsealedSenderPermitIncrement()); - }); - - Metrics.counter(RATE_LIMIT_RESET_COUNTER_NAME, - "countryCode", Util.getCountryCode(account.getNumber())).increment(); - } - - private static String getMaxCardinalityKey(final Account account) { - return "max_unsealed_sender_cardinality::" + account.getUuid(); - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/mappers/RateLimitChallengeExceptionMapper.java b/service/src/main/java/org/whispersystems/textsecuregcm/mappers/RateLimitChallengeExceptionMapper.java index bf1fbbafd..2d8a47f05 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/mappers/RateLimitChallengeExceptionMapper.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/mappers/RateLimitChallengeExceptionMapper.java @@ -10,21 +10,21 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import org.whispersystems.textsecuregcm.entities.RateLimitChallenge; import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException; -import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager; +import org.whispersystems.textsecuregcm.limits.RateLimitChallengeOptionManager; public class RateLimitChallengeExceptionMapper implements ExceptionMapper { - private final RateLimitChallengeManager rateLimitChallengeManager; + private final RateLimitChallengeOptionManager rateLimitChallengeOptionManager; - public RateLimitChallengeExceptionMapper(final RateLimitChallengeManager rateLimitChallengeManager) { - this.rateLimitChallengeManager = rateLimitChallengeManager; + public RateLimitChallengeExceptionMapper(final RateLimitChallengeOptionManager rateLimitChallengeOptionManager) { + this.rateLimitChallengeOptionManager = rateLimitChallengeOptionManager; } @Override public Response toResponse(final RateLimitChallengeException exception) { return Response.status(428) .entity(new RateLimitChallenge(UUID.randomUUID().toString(), - rateLimitChallengeManager.getChallengeOptions(exception.getAccount()))) + rateLimitChallengeOptionManager.getChallengeOptions(exception.getAccount()))) .header("Retry-After", exception.getRetryAfter().toSeconds()) .build(); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java index c405e8ac3..bc1807ba3 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java @@ -355,14 +355,13 @@ class DynamicConfigurationTest { DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); assertThat(emptyConfig.getRateLimitChallengeConfiguration().getClientSupportedVersions()).isEmpty(); - assertThat(emptyConfig.getRateLimitChallengeConfiguration().isPreKeyLimitEnforced()).isFalse(); assertThat(emptyConfig.getRateLimitChallengeConfiguration().isUnsealedSenderLimitEnforced()).isFalse(); } { final String rateLimitChallengeConfig = """ rateLimitChallenge: - preKeyLimitEnforced: true + unsealedSenderLimitEnforced: true clientSupportedVersions: IOS: 5.1.0 ANDROID: 5.2.0 @@ -378,8 +377,7 @@ class DynamicConfigurationTest { assertThat(clientSupportedVersions.get(ClientPlatform.IOS)).isEqualTo(new Semver("5.1.0")); assertThat(clientSupportedVersions.get(ClientPlatform.ANDROID)).isEqualTo(new Semver("5.2.0")); assertThat(clientSupportedVersions.get(ClientPlatform.DESKTOP)).isEqualTo(new Semver("5.0.0")); - assertThat(rateLimitChallengeConfiguration.isPreKeyLimitEnforced()).isTrue(); - assertThat(rateLimitChallengeConfiguration.isUnsealedSenderLimitEnforced()).isFalse(); + assertThat(rateLimitChallengeConfiguration.isUnsealedSenderLimitEnforced()).isTrue(); } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java index 08e86097c..55f28932c 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java @@ -18,7 +18,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -35,7 +34,6 @@ import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; import io.dropwizard.testing.junit5.ResourceExtension; import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; -import java.time.Duration; import java.util.Base64; import java.util.Collection; import java.util.HashSet; @@ -58,7 +56,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; @@ -69,14 +66,10 @@ import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope; import org.whispersystems.textsecuregcm.entities.MismatchedDevices; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity; import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList; -import org.whispersystems.textsecuregcm.entities.RateLimitChallenge; import org.whispersystems.textsecuregcm.entities.SignedPreKey; import org.whispersystems.textsecuregcm.entities.StaleDevices; -import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; -import org.whispersystems.textsecuregcm.limits.UnsealedSenderRateLimiter; -import org.whispersystems.textsecuregcm.mappers.RateLimitChallengeExceptionMapper; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.push.ApnFallbackManager; import org.whispersystems.textsecuregcm.push.MessageSender; @@ -113,9 +106,7 @@ class MessageControllerTest { private static final MessagesManager messagesManager = mock(MessagesManager.class); private static final RateLimiters rateLimiters = mock(RateLimiters.class); private static final RateLimiter rateLimiter = mock(RateLimiter.class); - private static final UnsealedSenderRateLimiter unsealedSenderRateLimiter = mock(UnsealedSenderRateLimiter.class); private static final ApnFallbackManager apnFallbackManager = mock(ApnFallbackManager.class); - private static final RateLimitChallengeManager rateLimitChallengeManager = mock(RateLimitChallengeManager.class); private static final ReportMessageManager reportMessageManager = mock(ReportMessageManager.class); private static final ExecutorService multiRecipientMessageExecutor = mock(ExecutorService.class); @@ -126,11 +117,9 @@ class MessageControllerTest { .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) .addProvider(RateLimitExceededExceptionMapper.class) - .addProvider(new RateLimitChallengeExceptionMapper(rateLimitChallengeManager)) .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) .addResource(new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, - messagesManager, unsealedSenderRateLimiter, apnFallbackManager, - rateLimitChallengeManager, reportMessageManager, multiRecipientMessageExecutor)) + messagesManager, apnFallbackManager, reportMessageManager, multiRecipientMessageExecutor)) .build(); @BeforeEach @@ -179,9 +168,7 @@ class MessageControllerTest { messagesManager, rateLimiters, rateLimiter, - unsealedSenderRateLimiter, apnFallbackManager, - rateLimitChallengeManager, reportMessageManager ); } @@ -282,69 +269,6 @@ class MessageControllerTest { assertThat("Bad request", response.getStatus(), is(equalTo(422))); } - @ParameterizedTest - @CsvSource({"true, true, 413", "true, false, 428", "false, false, 200"}) - void testUnsealedSenderCardinalityRateLimited(final boolean rateLimited, final boolean legacyClient, - final int expectedStatusCode) throws Exception { - - if (rateLimited) { - doThrow(new RateLimitExceededException(Duration.ofHours(1))) - .when(unsealedSenderRateLimiter).validate(eq(AuthHelper.VALID_ACCOUNT), eq(internationalAccount)); - - when(rateLimitChallengeManager.isClientBelowMinimumVersion(anyString())) - .thenReturn(legacyClient); - } - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/messages/%s", INTERNATIONAL_UUID)) - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .header("User-Agent", "Signal-Android/5.6.4 Android/30") - .put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class), - MediaType.APPLICATION_JSON_TYPE)); - - if (rateLimited) { - assertThat("Error Response", response.getStatus(), is(equalTo(expectedStatusCode))); - } else { - assertThat("Good Response", response.getStatus(), is(equalTo(expectedStatusCode))); - } - - verify(messageSender, rateLimited ? never() : times(1)).sendMessage(any(), any(), any(), anyBoolean()); - } - - @Test - void testRateLimitResetRequirement() throws Exception { - - Duration retryAfter = Duration.ofMinutes(1); - doThrow(new RateLimitExceededException(retryAfter)) - .when(unsealedSenderRateLimiter).validate(any(), any()); - - when(rateLimitChallengeManager.isClientBelowMinimumVersion("Signal-Android/5.1.2 Android/30")).thenReturn(false); - when(rateLimitChallengeManager.getChallengeOptions(AuthHelper.VALID_ACCOUNT)) - .thenReturn( - List.of(RateLimitChallengeManager.OPTION_PUSH_CHALLENGE, RateLimitChallengeManager.OPTION_RECAPTCHA)); - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/messages/%s", INTERNATIONAL_UUID)) - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .header("User-Agent", "Signal-Android/5.1.2 Android/30") - .put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class), - MediaType.APPLICATION_JSON_TYPE)); - - assertEquals(428, response.getStatus()); - - RateLimitChallenge rateLimitChallenge = response.readEntity(RateLimitChallenge.class); - - assertFalse(rateLimitChallenge.getToken().isBlank()); - assertFalse(rateLimitChallenge.getOptions().isEmpty()); - assertTrue(rateLimitChallenge.getOptions().contains("recaptcha")); - assertTrue(rateLimitChallenge.getOptions().contains("pushChallenge")); - assertEquals(retryAfter.toSeconds(), Long.parseLong(response.getHeaderString("Retry-After"))); - } - @Test void testSingleDeviceCurrentUnidentified() throws Exception { Response response = diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/limits/PreKeyRateLimiterTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/limits/PreKeyRateLimiterTest.java deleted file mode 100644 index 1debbf287..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/limits/PreKeyRateLimiterTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.whispersystems.textsecuregcm.limits; - -import static org.junit.Assert.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.UUID; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitChallengeConfiguration; -import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; -import org.whispersystems.textsecuregcm.storage.Account; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; - -class PreKeyRateLimiterTest { - - private Account account; - - private PreKeyRateLimiter preKeyRateLimiter; - - private DynamicRateLimitChallengeConfiguration rateLimitChallengeConfiguration; - private RateLimiter dailyPreKeyLimiter; - - @BeforeEach - void setup() { - final DynamicRateLimiters rateLimiters = mock(DynamicRateLimiters.class); - - dailyPreKeyLimiter = mock(RateLimiter.class); - when(rateLimiters.getDailyPreKeysLimiter()).thenReturn(dailyPreKeyLimiter); - - final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); - rateLimitChallengeConfiguration = mock(DynamicRateLimitChallengeConfiguration.class); - final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); - - when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); - when(dynamicConfiguration.getRateLimitChallengeConfiguration()).thenReturn(rateLimitChallengeConfiguration); - - preKeyRateLimiter = new PreKeyRateLimiter(rateLimiters, dynamicConfigurationManager, mock(RateLimitResetMetricsManager.class)); - - account = mock(Account.class); - when(account.getNumber()).thenReturn("+18005551111"); - when(account.getUuid()).thenReturn(UUID.randomUUID()); - } - - @Test - void enforcementConfiguration() throws RateLimitExceededException { - - doThrow(RateLimitExceededException.class) - .when(dailyPreKeyLimiter).validate(any(UUID.class)); - - when(rateLimitChallengeConfiguration.isPreKeyLimitEnforced()).thenReturn(false); - - preKeyRateLimiter.validate(account); - - when(rateLimitChallengeConfiguration.isPreKeyLimitEnforced()).thenReturn(true); - - assertThrows(RateLimitExceededException.class, () -> preKeyRateLimiter.validate(account)); - - when(rateLimitChallengeConfiguration.isPreKeyLimitEnforced()).thenReturn(false); - - preKeyRateLimiter.validate(account); - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManagerTest.java index ac1e82303..f3de728da 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeManagerTest.java @@ -1,41 +1,28 @@ package org.whispersystems.textsecuregcm.limits; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -import com.vdurmont.semver4j.Semver; import java.util.List; -import java.util.Optional; import java.util.UUID; -import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitChallengeConfiguration; +import org.whispersystems.textsecuregcm.abuse.RateLimitChallengeListener; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient; import org.whispersystems.textsecuregcm.storage.Account; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; -import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; class RateLimitChallengeManagerTest { private PushChallengeManager pushChallengeManager; private RecaptchaClient recaptchaClient; - private PreKeyRateLimiter preKeyRateLimiter; - private UnsealedSenderRateLimiter unsealedSenderRateLimiter; - private DynamicRateLimitChallengeConfiguration rateLimitChallengeConfiguration; private DynamicRateLimiters rateLimiters; + private RateLimitChallengeListener rateLimitChallengeListener; private RateLimitChallengeManager rateLimitChallengeManager; @@ -43,24 +30,15 @@ class RateLimitChallengeManagerTest { void setUp() { pushChallengeManager = mock(PushChallengeManager.class); recaptchaClient = mock(RecaptchaClient.class); - preKeyRateLimiter = mock(PreKeyRateLimiter.class); - unsealedSenderRateLimiter = mock(UnsealedSenderRateLimiter.class); rateLimiters = mock(DynamicRateLimiters.class); - - final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); - final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); - rateLimitChallengeConfiguration = mock(DynamicRateLimitChallengeConfiguration.class); - - when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); - when(dynamicConfiguration.getRateLimitChallengeConfiguration()).thenReturn(rateLimitChallengeConfiguration); + rateLimitChallengeListener = mock(RateLimitChallengeListener.class); rateLimitChallengeManager = new RateLimitChallengeManager( pushChallengeManager, recaptchaClient, - preKeyRateLimiter, - unsealedSenderRateLimiter, - rateLimiters, - dynamicConfigurationManager); + rateLimiters); + + rateLimitChallengeManager.addListener(rateLimitChallengeListener); } @ParameterizedTest @@ -78,11 +56,9 @@ class RateLimitChallengeManagerTest { rateLimitChallengeManager.answerPushChallenge(account, "challenge"); if (successfulChallenge) { - verify(preKeyRateLimiter).handleRateLimitReset(account); - verify(unsealedSenderRateLimiter).handleRateLimitReset(account); + verify(rateLimitChallengeListener).handleRateLimitChallengeAnswered(account); } else { - verifyNoInteractions(preKeyRateLimiter); - verifyNoInteractions(unsealedSenderRateLimiter); + verifyNoInteractions(rateLimitChallengeListener); } } @@ -102,98 +78,9 @@ class RateLimitChallengeManagerTest { rateLimitChallengeManager.answerRecaptchaChallenge(account, "captcha", "10.0.0.1"); if (successfulChallenge) { - verify(preKeyRateLimiter).handleRateLimitReset(account); - verify(unsealedSenderRateLimiter).handleRateLimitReset(account); + verify(rateLimitChallengeListener).handleRateLimitChallengeAnswered(account); } else { - verifyNoInteractions(preKeyRateLimiter); - verifyNoInteractions(unsealedSenderRateLimiter); + verifyNoInteractions(rateLimitChallengeListener); } } - - @ParameterizedTest - @MethodSource - void isClientBelowMinimumVersion(final String userAgent, final boolean expectBelowMinimumVersion) { - when(rateLimitChallengeConfiguration.getMinimumSupportedVersion(any())).thenReturn(Optional.empty()); - when(rateLimitChallengeConfiguration.getMinimumSupportedVersion(ClientPlatform.ANDROID)) - .thenReturn(Optional.of(new Semver("5.6.0"))); - when(rateLimitChallengeConfiguration.getMinimumSupportedVersion(ClientPlatform.DESKTOP)) - .thenReturn(Optional.of(new Semver("5.0.0-beta.2"))); - - assertEquals(expectBelowMinimumVersion, rateLimitChallengeManager.isClientBelowMinimumVersion(userAgent)); - } - - private static Stream isClientBelowMinimumVersion() { - return Stream.of( - Arguments.of("Signal-Android/5.1.2 Android/30", true), - Arguments.of("Signal-Android/5.6.0 Android/30", false), - Arguments.of("Signal-Android/5.11.1 Android/30", false), - Arguments.of("Signal-Desktop/5.0.0-beta.3 macOS/11", false), - Arguments.of("Signal-Desktop/5.0.0-beta.1 Windows/3.1", true), - Arguments.of("Signal-Desktop/5.2.0 Debian/11", false), - Arguments.of("Signal-iOS/5.1.2 iOS/12.2", true), - Arguments.of("anything-else", false) - ); - } - - @ParameterizedTest - @MethodSource - void getChallengeOptions(final boolean captchaAttemptPermitted, - final boolean captchaSuccessPermitted, - final boolean pushAttemptPermitted, - final boolean pushSuccessPermitted, - final boolean expectCaptcha, - final boolean expectPushChallenge) { - - final RateLimiter recaptchaChallengeAttemptLimiter = mock(RateLimiter.class); - final RateLimiter recaptchaChallengeSuccessLimiter = mock(RateLimiter.class); - final RateLimiter pushChallengeAttemptLimiter = mock(RateLimiter.class); - final RateLimiter pushChallengeSuccessLimiter = mock(RateLimiter.class); - - when(rateLimiters.getRecaptchaChallengeAttemptLimiter()).thenReturn(recaptchaChallengeAttemptLimiter); - when(rateLimiters.getRecaptchaChallengeSuccessLimiter()).thenReturn(recaptchaChallengeSuccessLimiter); - when(rateLimiters.getPushChallengeAttemptLimiter()).thenReturn(pushChallengeAttemptLimiter); - when(rateLimiters.getPushChallengeSuccessLimiter()).thenReturn(pushChallengeSuccessLimiter); - - when(recaptchaChallengeAttemptLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(captchaAttemptPermitted); - when(recaptchaChallengeSuccessLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(captchaSuccessPermitted); - when(pushChallengeAttemptLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(pushAttemptPermitted); - when(pushChallengeSuccessLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(pushSuccessPermitted); - - final int expectedLength = (expectCaptcha ? 1 : 0) + (expectPushChallenge ? 1 : 0); - - final Account account = mock(Account.class); - when(account.getUuid()).thenReturn(UUID.randomUUID()); - - final List options = rateLimitChallengeManager.getChallengeOptions(account); - assertEquals(expectedLength, options.size()); - - if (expectCaptcha) { - assertTrue(options.contains(RateLimitChallengeManager.OPTION_RECAPTCHA)); - } - - if (expectPushChallenge) { - assertTrue(options.contains(RateLimitChallengeManager.OPTION_PUSH_CHALLENGE)); - } - } - - private static Stream getChallengeOptions() { - return Stream.of( - Arguments.of(false, false, false, false, false, false), - Arguments.of(false, false, false, true, false, false), - Arguments.of(false, false, true, false, false, false), - Arguments.of(false, false, true, true, false, true), - Arguments.of(false, true, false, false, false, false), - Arguments.of(false, true, false, true, false, false), - Arguments.of(false, true, true, false, false, false), - Arguments.of(false, true, true, true, false, true), - Arguments.of(true, false, false, false, false, false), - Arguments.of(true, false, false, true, false, false), - Arguments.of(true, false, true, false, false, false), - Arguments.of(true, false, true, true, false, true), - Arguments.of(true, true, false, false, true, false), - Arguments.of(true, true, false, true, true, false), - Arguments.of(true, true, true, false, true, false), - Arguments.of(true, true, true, true, true, true) - ); - } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManagerTest.java new file mode 100644 index 000000000..df893be0f --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitChallengeOptionManagerTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2013-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.limits; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.vdurmont.semver4j.Semver; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitChallengeConfiguration; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; + +class RateLimitChallengeOptionManagerTest { + + private DynamicRateLimitChallengeConfiguration rateLimitChallengeConfiguration; + private DynamicRateLimiters rateLimiters; + + private RateLimitChallengeOptionManager rateLimitChallengeOptionManager; + + @BeforeEach + void setUp() { + rateLimiters = mock(DynamicRateLimiters.class); + + final DynamicConfigurationManager dynamicConfigurationManager = + mock(DynamicConfigurationManager.class); + + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + rateLimitChallengeConfiguration = mock(DynamicRateLimitChallengeConfiguration.class); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + when(dynamicConfiguration.getRateLimitChallengeConfiguration()).thenReturn(rateLimitChallengeConfiguration); + + rateLimitChallengeOptionManager = new RateLimitChallengeOptionManager(rateLimiters, dynamicConfigurationManager); + } + + @ParameterizedTest + @MethodSource + void isClientBelowMinimumVersion(final String userAgent, final boolean expectBelowMinimumVersion) { + when(rateLimitChallengeConfiguration.getMinimumSupportedVersion(any())).thenReturn(Optional.empty()); + when(rateLimitChallengeConfiguration.getMinimumSupportedVersion(ClientPlatform.ANDROID)) + .thenReturn(Optional.of(new Semver("5.6.0"))); + when(rateLimitChallengeConfiguration.getMinimumSupportedVersion(ClientPlatform.DESKTOP)) + .thenReturn(Optional.of(new Semver("5.0.0-beta.2"))); + + assertEquals(expectBelowMinimumVersion, rateLimitChallengeOptionManager.isClientBelowMinimumVersion(userAgent)); + } + + private static Stream isClientBelowMinimumVersion() { + return Stream.of( + Arguments.of("Signal-Android/5.1.2 Android/30", true), + Arguments.of("Signal-Android/5.6.0 Android/30", false), + Arguments.of("Signal-Android/5.11.1 Android/30", false), + Arguments.of("Signal-Desktop/5.0.0-beta.3 macOS/11", false), + Arguments.of("Signal-Desktop/5.0.0-beta.1 Windows/3.1", true), + Arguments.of("Signal-Desktop/5.2.0 Debian/11", false), + Arguments.of("Signal-iOS/5.1.2 iOS/12.2", true), + Arguments.of("anything-else", false) + ); + } + + @ParameterizedTest + @MethodSource + void getChallengeOptions(final boolean captchaAttemptPermitted, + final boolean captchaSuccessPermitted, + final boolean pushAttemptPermitted, + final boolean pushSuccessPermitted, + final boolean expectCaptcha, + final boolean expectPushChallenge) { + + final RateLimiter recaptchaChallengeAttemptLimiter = mock(RateLimiter.class); + final RateLimiter recaptchaChallengeSuccessLimiter = mock(RateLimiter.class); + final RateLimiter pushChallengeAttemptLimiter = mock(RateLimiter.class); + final RateLimiter pushChallengeSuccessLimiter = mock(RateLimiter.class); + + when(rateLimiters.getRecaptchaChallengeAttemptLimiter()).thenReturn(recaptchaChallengeAttemptLimiter); + when(rateLimiters.getRecaptchaChallengeSuccessLimiter()).thenReturn(recaptchaChallengeSuccessLimiter); + when(rateLimiters.getPushChallengeAttemptLimiter()).thenReturn(pushChallengeAttemptLimiter); + when(rateLimiters.getPushChallengeSuccessLimiter()).thenReturn(pushChallengeSuccessLimiter); + + when(recaptchaChallengeAttemptLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(captchaAttemptPermitted); + when(recaptchaChallengeSuccessLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(captchaSuccessPermitted); + when(pushChallengeAttemptLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(pushAttemptPermitted); + when(pushChallengeSuccessLimiter.hasAvailablePermits(any(UUID.class), anyInt())).thenReturn(pushSuccessPermitted); + + final int expectedLength = (expectCaptcha ? 1 : 0) + (expectPushChallenge ? 1 : 0); + + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(UUID.randomUUID()); + + final List options = rateLimitChallengeOptionManager.getChallengeOptions(account); + assertEquals(expectedLength, options.size()); + + if (expectCaptcha) { + assertTrue(options.contains(RateLimitChallengeOptionManager.OPTION_RECAPTCHA)); + } + + if (expectPushChallenge) { + assertTrue(options.contains(RateLimitChallengeOptionManager.OPTION_PUSH_CHALLENGE)); + } + } + + private static Stream getChallengeOptions() { + return Stream.of( + Arguments.of(false, false, false, false, false, false), + Arguments.of(false, false, false, true, false, false), + Arguments.of(false, false, true, false, false, false), + Arguments.of(false, false, true, true, false, true), + Arguments.of(false, true, false, false, false, false), + Arguments.of(false, true, false, true, false, false), + Arguments.of(false, true, true, false, false, false), + Arguments.of(false, true, true, true, false, true), + Arguments.of(true, false, false, false, false, false), + Arguments.of(true, false, false, true, false, false), + Arguments.of(true, false, true, false, false, false), + Arguments.of(true, false, true, true, false, true), + Arguments.of(true, true, false, false, true, false), + Arguments.of(true, true, false, true, true, false), + Arguments.of(true, true, true, false, true, false), + Arguments.of(true, true, true, true, true, true) + ); + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitResetMetricsManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitResetMetricsManagerTest.java deleted file mode 100644 index cfc56fc7a..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/limits/RateLimitResetMetricsManagerTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.whispersystems.textsecuregcm.limits; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import io.dropwizard.util.Duration; -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import java.util.UUID; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; -import org.whispersystems.textsecuregcm.storage.Account; - -class RateLimitResetMetricsManagerTest { - - @RegisterExtension - static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); - - private RateLimitResetMetricsManager metricsManager; - private SimpleMeterRegistry meterRegistry; - - @BeforeEach - void setUp() { - meterRegistry = new SimpleMeterRegistry(); - metricsManager = new RateLimitResetMetricsManager(REDIS_CLUSTER_EXTENSION.getRedisCluster(), meterRegistry); - } - - @Test - void testRecordMetrics() { - - final Account firstAccount = mock(Account.class); - when(firstAccount.getUuid()).thenReturn(UUID.randomUUID()); - final Account secondAccount = mock(Account.class); - when(secondAccount.getUuid()).thenReturn(UUID.randomUUID()); - - metricsManager.recordMetrics(firstAccount, true, "counter", "enforced", "total", Duration.hours(1).toSeconds()); - metricsManager.recordMetrics(firstAccount, true, "counter", "enforced", "total", Duration.hours(1).toSeconds()); - metricsManager.recordMetrics(secondAccount, false, "counter", "unenforced", "total", Duration.hours(1).toSeconds()); - - final double counterTotal = meterRegistry.get("counter").counters().stream() - .map(Counter::count) - .reduce(Double::sum) - .orElseThrow(); - assertEquals(3, counterTotal, 0.0); - - final long enforcedCount = REDIS_CLUSTER_EXTENSION.getRedisCluster() - .withCluster(conn -> conn.sync().pfcount("enforced")); - assertEquals(1L, enforcedCount); - - final long unenforcedCount = REDIS_CLUSTER_EXTENSION.getRedisCluster() - .withCluster(conn -> conn.sync().pfcount("unenforced")); - assertEquals(1L, unenforcedCount); - - final long total = REDIS_CLUSTER_EXTENSION.getRedisCluster().withCluster(conn -> conn.sync().pfcount("total")); - assertEquals(2L, total); - - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/limits/UnsealedSenderRateLimiterTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/limits/UnsealedSenderRateLimiterTest.java deleted file mode 100644 index bcfb102a6..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/limits/UnsealedSenderRateLimiterTest.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2021 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.limits; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.time.Duration; -import java.util.UUID; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitChallengeConfiguration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitsConfiguration; -import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; -import org.whispersystems.textsecuregcm.redis.RedisClusterExtension; -import org.whispersystems.textsecuregcm.storage.Account; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; - -class UnsealedSenderRateLimiterTest { - - @RegisterExtension - static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); - - private Account sender; - private Account firstDestination; - private Account secondDestination; - - private UnsealedSenderRateLimiter unsealedSenderRateLimiter; - - private DynamicRateLimitChallengeConfiguration rateLimitChallengeConfiguration; - - @BeforeEach - void setUp() throws Exception { - - final DynamicRateLimiters rateLimiters = mock(DynamicRateLimiters.class); - final CardinalityRateLimiter cardinalityRateLimiter = - new CardinalityRateLimiter(REDIS_CLUSTER_EXTENSION.getRedisCluster(), "test", Duration.ofDays(1), 1); - - when(rateLimiters.getUnsealedSenderCardinalityLimiter()).thenReturn(cardinalityRateLimiter); - when(rateLimiters.getRateLimitResetLimiter()).thenReturn(mock(RateLimiter.class)); - - final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); - final DynamicRateLimitsConfiguration rateLimitsConfiguration = mock(DynamicRateLimitsConfiguration.class); - rateLimitChallengeConfiguration = mock(DynamicRateLimitChallengeConfiguration.class); - final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); - - when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); - when(dynamicConfiguration.getLimits()).thenReturn(rateLimitsConfiguration); - when(rateLimitsConfiguration.getUnsealedSenderDefaultCardinalityLimit()).thenReturn(1); - when(rateLimitsConfiguration.getUnsealedSenderPermitIncrement()).thenReturn(1); - when(dynamicConfiguration.getRateLimitChallengeConfiguration()).thenReturn(rateLimitChallengeConfiguration); - when(rateLimitChallengeConfiguration.isUnsealedSenderLimitEnforced()).thenReturn(true); - - unsealedSenderRateLimiter = new UnsealedSenderRateLimiter(rateLimiters, REDIS_CLUSTER_EXTENSION.getRedisCluster(), - dynamicConfigurationManager, - mock(RateLimitResetMetricsManager.class)); - - sender = mock(Account.class); - when(sender.getNumber()).thenReturn("+18005551111"); - when(sender.getUuid()).thenReturn(UUID.randomUUID()); - - firstDestination = mock(Account.class); - when(firstDestination.getNumber()).thenReturn("+18005552222"); - when(firstDestination.getUuid()).thenReturn(UUID.randomUUID()); - - secondDestination = mock(Account.class); - when(secondDestination.getNumber()).thenReturn("+18005553333"); - when(secondDestination.getUuid()).thenReturn(UUID.randomUUID()); - } - - @Test - void validate() throws RateLimitExceededException { - unsealedSenderRateLimiter.validate(sender, firstDestination); - - assertThrows(RateLimitExceededException.class, () -> unsealedSenderRateLimiter.validate(sender, secondDestination)); - - unsealedSenderRateLimiter.validate(sender, firstDestination); - } - - @Test - void handleRateLimitReset() throws RateLimitExceededException { - unsealedSenderRateLimiter.validate(sender, firstDestination); - - assertThrows(RateLimitExceededException.class, () -> unsealedSenderRateLimiter.validate(sender, secondDestination)); - - unsealedSenderRateLimiter.handleRateLimitReset(sender); - unsealedSenderRateLimiter.validate(sender, firstDestination); - unsealedSenderRateLimiter.validate(sender, secondDestination); - } - - @Test - void enforcementConfiguration() throws RateLimitExceededException { - - when(rateLimitChallengeConfiguration.isUnsealedSenderLimitEnforced()).thenReturn(false); - - unsealedSenderRateLimiter.validate(sender, firstDestination); - unsealedSenderRateLimiter.validate(sender, secondDestination); - - when(rateLimitChallengeConfiguration.isUnsealedSenderLimitEnforced()).thenReturn(true); - - final Account thirdDestination = mock(Account.class); - when(thirdDestination.getNumber()).thenReturn("+18005554444"); - when(thirdDestination.getUuid()).thenReturn(UUID.randomUUID()); - - assertThrows(RateLimitExceededException.class, () -> unsealedSenderRateLimiter.validate(sender, thirdDestination)); - - when(rateLimitChallengeConfiguration.isUnsealedSenderLimitEnforced()).thenReturn(false); - - final Account fourthDestination = mock(Account.class); - when(fourthDestination.getNumber()).thenReturn("+18005555555"); - when(fourthDestination.getUuid()).thenReturn(UUID.randomUUID()); - unsealedSenderRateLimiter.validate(sender, fourthDestination); - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeysControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeysControllerTest.java index 8b64039d0..ad287b7c8 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeysControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeysControllerTest.java @@ -52,11 +52,9 @@ import org.whispersystems.textsecuregcm.entities.PreKeyResponse; import org.whispersystems.textsecuregcm.entities.PreKeyState; import org.whispersystems.textsecuregcm.entities.RateLimitChallenge; import org.whispersystems.textsecuregcm.entities.SignedPreKey; -import org.whispersystems.textsecuregcm.limits.PreKeyRateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; -import org.whispersystems.textsecuregcm.mappers.RateLimitChallengeExceptionMapper; import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; @@ -97,8 +95,6 @@ class KeysControllerTest { private final static Keys KEYS = mock(Keys.class ); private final static AccountsManager accounts = mock(AccountsManager.class ); - private final static PreKeyRateLimiter preKeyRateLimiter = mock(PreKeyRateLimiter.class ); - private final static RateLimitChallengeManager rateLimitChallengeManager = mock(RateLimitChallengeManager.class ); private final static Account existsAccount = mock(Account.class ); private static final RateLimiters rateLimiters = mock(RateLimiters.class); @@ -109,10 +105,8 @@ class KeysControllerTest { .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of( AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) - .addResource(new RateLimitChallengeExceptionMapper(rateLimitChallengeManager)) .addResource(new ServerRejectedExceptionMapper()) - .addResource( - new KeysController(rateLimiters, KEYS, accounts, preKeyRateLimiter, rateLimitChallengeManager)) + .addResource(new KeysController(rateLimiters, KEYS, accounts)) .build(); @BeforeEach @@ -190,11 +184,9 @@ class KeysControllerTest { reset( KEYS, accounts, - preKeyRateLimiter, existsAccount, rateLimiters, - rateLimiter, - rateLimitChallengeManager + rateLimiter ); clearInvocations(AuthHelper.VALID_DEVICE); @@ -582,51 +574,4 @@ class KeysControllerTest { verify(AuthHelper.DISABLED_DEVICE).setSignedPreKey(eq(signedPreKey)); verify(accounts).update(eq(AuthHelper.DISABLED_ACCOUNT), any()); } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void testRateLimitChallenge(boolean clientBelowMinimumVersion) throws RateLimitExceededException { - - Duration retryAfter = Duration.ofMinutes(1); - doThrow(new RateLimitExceededException(retryAfter)) - .when(preKeyRateLimiter).validate(any()); - - when( - rateLimitChallengeManager.isClientBelowMinimumVersion("Signal-Android/5.1.2 Android/30")).thenReturn( - clientBelowMinimumVersion); - when(rateLimitChallengeManager.getChallengeOptions(AuthHelper.VALID_ACCOUNT)) - .thenReturn( - List.of(RateLimitChallengeManager.OPTION_PUSH_CHALLENGE, RateLimitChallengeManager.OPTION_RECAPTCHA)); - - Response result = resources.getJerseyTest() - .target(String.format("/v2/keys/%s/*", EXISTS_UUID.toString())) - .request() - .header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("1337".getBytes())) - .header("User-Agent", "Signal-Android/5.1.2 Android/30") - .get(); - - // unidentified access should not be rate limited - assertThat(result.getStatus()).isEqualTo(200); - - result = resources.getJerseyTest() - .target(String.format("/v2/keys/%s/*", EXISTS_UUID.toString())) - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .header("User-Agent", "Signal-Android/5.1.2 Android/30") - .get(); - - if (clientBelowMinimumVersion) { - assertThat(result.getStatus()).isEqualTo(508); - } else { - assertThat(result.getStatus()).isEqualTo(428); - - RateLimitChallenge rateLimitChallenge = result.readEntity(RateLimitChallenge.class); - - assertThat(rateLimitChallenge.getToken()).isNotBlank(); - assertThat(rateLimitChallenge.getOptions()).isNotEmpty(); - assertThat(rateLimitChallenge.getOptions()).contains("recaptcha"); - assertThat(rateLimitChallenge.getOptions()).contains("pushChallenge"); - assertThat(Long.parseLong(result.getHeaderString("Retry-After"))).isEqualTo(retryAfter.toSeconds()); - } - } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/limits/DynamicRateLimitsTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/limits/DynamicRateLimitsTest.java index abe6585ff..338a6423b 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/limits/DynamicRateLimitsTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/limits/DynamicRateLimitsTest.java @@ -6,14 +6,11 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import java.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration; import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration.RateLimitConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicRateLimitsConfiguration; -import org.whispersystems.textsecuregcm.limits.CardinalityRateLimiter; import org.whispersystems.textsecuregcm.limits.DynamicRateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster; @@ -38,11 +35,11 @@ class DynamicRateLimitsTest { void testUnchangingConfiguration() { DynamicRateLimiters rateLimiters = new DynamicRateLimiters(redisCluster, dynamicConfig); - RateLimiter limiter = rateLimiters.getUnsealedIpLimiter(); + RateLimiter limiter = rateLimiters.getRateLimitResetLimiter(); - assertThat(limiter.getBucketSize()).isEqualTo(dynamicConfig.getConfiguration().getLimits().getUnsealedSenderIp().getBucketSize()); - assertThat(limiter.getLeakRatePerMinute()).isEqualTo(dynamicConfig.getConfiguration().getLimits().getUnsealedSenderIp().getLeakRatePerMinute()); - assertSame(rateLimiters.getUnsealedIpLimiter(), limiter); + assertThat(limiter.getBucketSize()).isEqualTo(dynamicConfig.getConfiguration().getLimits().getRateLimitReset().getBucketSize()); + assertThat(limiter.getLeakRatePerMinute()).isEqualTo(dynamicConfig.getConfiguration().getLimits().getRateLimitReset().getLeakRatePerMinute()); + assertSame(rateLimiters.getRateLimitResetLimiter(), limiter); } @Test @@ -51,33 +48,30 @@ class DynamicRateLimitsTest { DynamicRateLimitsConfiguration limitsConfiguration = mock(DynamicRateLimitsConfiguration.class); when(configuration.getLimits()).thenReturn(limitsConfiguration); - when(limitsConfiguration.getUnsealedSenderNumber()).thenReturn(new RateLimitsConfiguration.CardinalityRateLimitConfiguration(10, Duration.ofHours(1))); when(limitsConfiguration.getRecaptchaChallengeAttempt()).thenReturn(new RateLimitConfiguration()); when(limitsConfiguration.getRecaptchaChallengeSuccess()).thenReturn(new RateLimitConfiguration()); when(limitsConfiguration.getPushChallengeAttempt()).thenReturn(new RateLimitConfiguration()); when(limitsConfiguration.getPushChallengeSuccess()).thenReturn(new RateLimitConfiguration()); - when(limitsConfiguration.getDailyPreKeys()).thenReturn(new RateLimitConfiguration()); - final RateLimitConfiguration initialRateLimitConfiguration = new RateLimitConfiguration(4, 1.0); - when(limitsConfiguration.getUnsealedSenderIp()).thenReturn(initialRateLimitConfiguration); + final RateLimitConfiguration initialRateLimitConfiguration = new RateLimitConfiguration(4, 1); when(limitsConfiguration.getRateLimitReset()).thenReturn(initialRateLimitConfiguration); when(dynamicConfig.getConfiguration()).thenReturn(configuration); DynamicRateLimiters rateLimiters = new DynamicRateLimiters(redisCluster, dynamicConfig); - CardinalityRateLimiter limiter = rateLimiters.getUnsealedSenderCardinalityLimiter(); + RateLimiter limiter = rateLimiters.getRateLimitResetLimiter(); - assertThat(limiter.getDefaultMaxCardinality()).isEqualTo(10); - assertThat(limiter.getInitialTtl()).isEqualTo(Duration.ofHours(1)); - assertSame(rateLimiters.getUnsealedSenderCardinalityLimiter(), limiter); + assertThat(limiter.getBucketSize()).isEqualTo(4); + assertThat(limiter.getLeakRatePerMinute()).isEqualTo(1); + assertSame(rateLimiters.getRateLimitResetLimiter(), limiter); - when(limitsConfiguration.getUnsealedSenderNumber()).thenReturn(new RateLimitsConfiguration.CardinalityRateLimitConfiguration(20, Duration.ofHours(2))); + when(limitsConfiguration.getRateLimitReset()).thenReturn(new RateLimitConfiguration(17, 19)); - CardinalityRateLimiter changed = rateLimiters.getUnsealedSenderCardinalityLimiter(); + RateLimiter changed = rateLimiters.getRateLimitResetLimiter(); - assertThat(changed.getDefaultMaxCardinality()).isEqualTo(20); - assertThat(changed.getInitialTtl()).isEqualTo(Duration.ofHours(2)); + assertThat(changed.getBucketSize()).isEqualTo(17); + assertThat(changed.getLeakRatePerMinute()).isEqualTo(19); assertNotSame(limiter, changed); }