mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-22 08:58:04 +01:00
Migrate challenge-issuing rate limiters to the abusive message filter
This commit is contained in:
committed by
Jon Chambers
parent
9628f147f1
commit
14cff958e9
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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<DynamicConfiguration> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<DynamicConfiguration> 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<Arguments> 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<String> 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<Arguments> 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DynamicConfiguration> 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<Arguments> 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<String> 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<Arguments> 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user