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);
}